1 #----------------------------------------------------------------------------
2 # Name: wxPython.lib.intctrl.py
5 # Copyright: (c) 2003 by Will Sadkin
7 # License: wxWindows license
8 #----------------------------------------------------------------------------
10 # This was written to provide a standard integer edit control for wxPython.
12 # wxIntCtrl permits integer (long) values to be retrieved or set via
13 # .GetValue() and .SetValue(), and provides an EVT_INT() event function
14 # for trapping changes to the control.
16 # It supports negative integers as well as the naturals, and does not
17 # permit leading zeros or an empty control; attempting to delete the
18 # contents of the control will result in a (selected) value of zero,
19 # thus preserving a legitimate integer value, or an empty control
20 # (if a value of None is allowed for the control.) Similarly, replacing the
21 # contents of the control with '-' will result in a selected (absolute)
24 # wxIntCtrl also supports range limits, with the option of either
25 # enforcing them or simply coloring the text of the control if the limits
27 #----------------------------------------------------------------------------
28 # 12/08/2003 - Jeff Grimmett (grimmtooth@softhome.net)
30 # o 2.5 Compatability changes
38 #----------------------------------------------------------------------------
40 from sys
import maxint
41 MAXINT
= maxint
# (constants should be in upper case)
44 #----------------------------------------------------------------------------
46 # Used to trap events indicating that the current
47 # integer value of the control has been changed.
48 wxEVT_COMMAND_INT_UPDATED
= wx
.NewEventType()
49 EVT_INT
= wx
.PyEventBinder(wxEVT_COMMAND_INT_UPDATED
, 1)
51 #----------------------------------------------------------------------------
53 # wxWindows' wxTextCtrl translates Composite "control key"
54 # events into single events before returning them to its OnChar
55 # routine. The doc says that this results in 1 for Ctrl-A, 2 for
56 # Ctrl-B, etc. However, there are no wxPython or wxWindows
57 # symbols for them, so I'm defining codes for Ctrl-X (cut) and
58 # Ctrl-V (paste) here for readability:
59 WXK_CTRL_X
= (ord('X')+1) - ord('A')
60 WXK_CTRL_V
= (ord('V')+1) - ord('A')
62 class wxIntUpdatedEvent(wx
.PyCommandEvent
):
63 def __init__(self
, id, value
= 0, object=None):
64 wx
.PyCommandEvent
.__init
__(self
, wxEVT_COMMAND_INT_UPDATED
, id)
67 self
.SetEventObject(object)
70 """Retrieve the value of the control at the time
71 this event was generated."""
75 #----------------------------------------------------------------------------
77 class wxIntValidator( wx
.PyValidator
):
79 Validator class used with wxIntCtrl; handles all validation of input
80 prior to changing the value of the underlying wx.TextCtrl.
83 wx
.PyValidator
.__init
__(self
)
84 self
.Bind(wx
.EVT_CHAR
, self
.OnChar
)
87 return self
.__class
__()
89 def Validate(self
, window
): # window here is the *parent* of the ctrl
91 Because each operation on the control is vetted as it's made,
92 the value of the control is always valid.
97 def OnChar(self
, event
):
99 Validates keystrokes to make sure the resulting value will a legal
100 value. Erasing the value causes it to be set to 0, with the value
101 selected, so it can be replaced. Similarly, replacing the value
102 with a '-' sign causes the value to become -1, with the value
103 selected. Leading zeros are removed if introduced by selection,
104 and are prevented from being inserted.
106 key
= event
.KeyCode()
107 ctrl
= event
.GetEventObject()
110 value
= ctrl
.GetValue()
111 textval
= wx
.TextCtrl
.GetValue(ctrl
)
112 allow_none
= ctrl
.IsNoneAllowed()
114 pos
= ctrl
.GetInsertionPoint()
115 sel_start
, sel_to
= ctrl
.GetSelection()
116 select_len
= sel_to
- sel_start
118 # (Uncomment for debugging:)
119 ## print 'keycode:', key
121 ## print 'sel_start, sel_to:', sel_start, sel_to
122 ## print 'select_len:', select_len
123 ## print 'textval:', textval
125 # set defaults for processing:
137 # Validate action, and predict resulting value, so we can
138 # range check the result and validate that too.
140 if key
in (wx
.WXK_DELETE
, wx
.WXK_BACK
, WXK_CTRL_X
):
142 new_text
= textval
[:sel_start
] + textval
[sel_to
:]
143 elif key
== wx
.WXK_DELETE
and pos
< len(textval
):
144 new_text
= textval
[:pos
] + textval
[pos
+1:]
145 elif key
== wx
.WXK_BACK
and pos
> 0:
146 new_text
= textval
[:pos
-1] + textval
[pos
:]
147 # (else value shouldn't change)
149 if new_text
in ('', '-'):
150 # Deletion of last significant digit:
151 if allow_none
and new_text
== '':
159 new_value
= ctrl
._fromGUI
(new_text
)
164 elif key
== WXK_CTRL_V
: # (see comments at top of file)
165 # Only allow paste if number:
166 paste_text
= ctrl
._getClipboardContents
()
167 new_text
= textval
[:sel_start
] + paste_text
+ textval
[sel_to
:]
168 if new_text
== '' and allow_none
:
173 # Convert the resulting strings, verifying they
174 # are legal integers and will fit in proper
175 # size if ctrl limited to int. (if not,
177 new_value
= ctrl
._fromGUI
(new_text
)
180 paste_value
= ctrl
._fromGUI
(paste_text
)
184 new_pos
= sel_start
+ len(str(paste_value
))
186 # if resulting value is 0, truncate and highlight value:
187 if new_value
== 0 and len(new_text
) > 1:
190 elif paste_value
== 0:
191 # Disallow pasting a leading zero with nothing selected:
193 and value
is not None
194 and ( (value
>= 0 and pos
== 0)
195 or (value
< 0 and pos
in [0,1]) ) ):
204 elif key
< wx
.WXK_SPACE
or key
> 255:
208 elif chr(key
) == '-':
209 # Allow '-' to result in -1 if replacing entire contents:
211 or (value
== 0 and pos
== 0)
212 or (select_len
>= len(str(abs(value
)))) ):
216 # else allow negative sign only at start, and only if
217 # number isn't already zero or negative:
218 elif pos
!= 0 or (value
is not None and value
< 0):
221 new_text
= '-' + textval
224 new_value
= ctrl
._fromGUI
(new_text
)
229 elif chr(key
) in string
.digits
:
230 # disallow inserting a leading zero with nothing selected
233 and value
is not None
234 and ( (value
>= 0 and pos
== 0)
235 or (value
< 0 and pos
in [0,1]) ) ):
237 # disallow inserting digits before the minus sign:
238 elif value
is not None and value
< 0 and pos
== 0:
241 new_text
= textval
[:sel_start
] + chr(key
) + textval
[sel_to
:]
243 new_value
= ctrl
._fromGUI
(new_text
)
253 # Do range checking for new candidate value:
254 if ctrl
.IsLimited() and not ctrl
.IsInBounds(new_value
):
256 elif new_value
is not None:
257 # ensure resulting text doesn't result in a leading 0:
258 if not set_to_zero
and not set_to_minus_one
:
259 if( (new_value
> 0 and new_text
[0] == '0')
260 or (new_value
< 0 and new_text
[1] == '0')
261 or (new_value
== 0 and select_len
> 1 ) ):
263 # Allow replacement of leading chars with
264 # zero, but remove the leading zero, effectively
265 # making this like "remove leading digits"
267 # Account for leading zero when positioning cursor:
268 if( key
== wx
.WXK_BACK
269 or (paste
and paste_value
== 0 and new_pos
> 0) ):
270 new_pos
= new_pos
- 1
272 wx
.CallAfter(ctrl
.SetValue
, new_value
)
273 wx
.CallAfter(ctrl
.SetInsertionPoint
, new_pos
)
277 # Always do paste numerically, to remove
278 # leading/trailing spaces
279 wx
.CallAfter(ctrl
.SetValue
, new_value
)
280 wx
.CallAfter(ctrl
.SetInsertionPoint
, new_pos
)
283 elif (new_value
== 0 and len(new_text
) > 1 ):
287 ctrl
._colorValue
(new_value
) # (one way or t'other)
289 # (Uncomment for debugging:)
291 ## print 'new value:', new_value
292 ## if paste: print 'paste'
293 ## if set_to_none: print 'set_to_none'
294 ## if set_to_zero: print 'set_to_zero'
295 ## if set_to_minus_one: print 'set_to_minus_one'
296 ## if internally_set: print 'internally_set'
298 ## print 'new text:', new_text
299 ## print 'disallowed'
304 wx
.CallAfter(ctrl
.SetValue
, new_value
)
307 # select to "empty" numeric value
308 wx
.CallAfter(ctrl
.SetValue
, new_value
)
309 wx
.CallAfter(ctrl
.SetInsertionPoint
, 0)
310 wx
.CallAfter(ctrl
.SetSelection
, 0, 1)
312 elif set_to_minus_one
:
313 wx
.CallAfter(ctrl
.SetValue
, new_value
)
314 wx
.CallAfter(ctrl
.SetInsertionPoint
, 1)
315 wx
.CallAfter(ctrl
.SetSelection
, 1, 2)
317 elif not internally_set
:
318 event
.Skip() # allow base wxTextCtrl to finish processing
320 elif not wx
.Validator_IsSilent():
324 def TransferToWindow(self
):
325 """ Transfer data from validator to window.
327 The default implementation returns False, indicating that an error
328 occurred. We simply return True, as we don't do any data transfer.
330 return True # Prevent wx.Dialog from complaining.
333 def TransferFromWindow(self
):
334 """ Transfer data from window to validator.
336 The default implementation returns False, indicating that an error
337 occurred. We simply return True, as we don't do any data transfer.
339 return True # Prevent wx.Dialog from complaining.
342 #----------------------------------------------------------------------------
344 class wxIntCtrl(wx
.TextCtrl
):
346 This class provides a control that takes and returns integers as
347 value, and provides bounds support and optional value limiting.
352 pos = wxDefaultPosition,
353 size = wxDefaultSize,
355 validator = wxDefaultValidator,
362 default_color = wxBLACK,
366 If no initial value is set, the default will be zero, or
367 the minimum value, if specified. If an illegal string is specified,
368 a ValueError will result. (You can always later set the initial
369 value with SetValue() after instantiation of the control.)
371 The minimum value that the control should allow. This can be
372 adjusted with SetMin(). If the control is not limited, any value
373 below this bound will be colored with the current out-of-bounds color.
374 If min < -sys.maxint-1 and the control is configured to not allow long
375 values, the minimum bound will still be set to the long value, but
376 the implicit bound will be -sys.maxint-1.
378 The maximum value that the control should allow. This can be
379 adjusted with SetMax(). If the control is not limited, any value
380 above this bound will be colored with the current out-of-bounds color.
381 if max > sys.maxint and the control is configured to not allow long
382 values, the maximum bound will still be set to the long value, but
383 the implicit bound will be sys.maxint.
386 Boolean indicating whether the control prevents values from
387 exceeding the currently set minimum and maximum values (bounds).
388 If False and bounds are set, out-of-bounds values will
389 be colored with the current out-of-bounds color.
392 Boolean indicating whether or not the control is allowed to be
393 empty, representing a value of None for the control.
396 Boolean indicating whether or not the control is allowed to hold
397 and return a long as well as an int.
400 Color value used for in-bounds values of the control.
403 Color value used for out-of-bounds values of the control
404 when the bounds are set but the control is not limited.
407 Normally None, wxIntCtrl uses its own validator to do value
408 validation and input control. However, a validator derived
409 from wxIntValidator can be supplied to override the data
410 transfer methods for the wxIntValidator class.
414 self
, parent
, id=-1, value
= 0,
415 pos
= wx
.DefaultPosition
, size
= wx
.DefaultSize
,
416 style
= 0, validator
= wx
.DefaultValidator
,
419 limited
= 0, allow_none
= 0, allow_long
= 0,
420 default_color
= wx
.BLACK
, oob_color
= wx
.RED
,
423 # Establish attrs required for any operation on value:
427 self
.__default
_color
= wx
.BLACK
428 self
.__oob
_color
= wx
.RED
429 self
.__allow
_none
= 0
430 self
.__allow
_long
= 0
431 self
.__oldvalue
= None
433 if validator
== wx
.DefaultValidator
:
434 validator
= wxIntValidator()
436 wx
.TextCtrl
.__init
__(
437 self
, parent
, id, self
._toGUI
(0),
438 pos
, size
, style
, validator
, name
)
440 # The following lets us set out our "integer update" events:
441 self
.Bind(wx
.EVT_TEXT
, self
.OnText
)
443 # Establish parameters, with appropriate error checking
445 self
.SetBounds(min, max)
446 self
.SetLimited(limited
)
447 self
.SetColors(default_color
, oob_color
)
448 self
.SetNoneAllowed(allow_none
)
449 self
.SetLongAllowed(allow_long
)
454 def OnText( self
, event
):
456 Handles an event indicating that the text control's value
457 has changed, and issue EVT_INT event.
458 NOTE: using wx.TextCtrl.SetValue() to change the control's
459 contents from within a wx.EVT_CHAR handler can cause double
460 text events. So we check for actual changes to the text
461 before passing the events on.
463 value
= self
.GetValue()
464 if value
!= self
.__oldvalue
:
466 self
.GetEventHandler().ProcessEvent(
467 wxIntUpdatedEvent( self
.GetId(), self
.GetValue(), self
) )
470 # let normal processing of the text continue
472 self
.__oldvalue
= value
# record for next event
477 Returns the current integer (long) value of the control.
479 return self
._fromGUI
( wx
.TextCtrl
.GetValue(self
) )
481 def SetValue(self
, value
):
483 Sets the value of the control to the integer value specified.
484 The resulting actual value of the control may be altered to
485 conform with the bounds set on the control if limited,
486 or colored if not limited but the value is out-of-bounds.
487 A ValueError exception will be raised if an invalid value
490 wx
.TextCtrl
.SetValue( self
, self
._toGUI
(value
) )
494 def SetMin(self
, min=None):
496 Sets the minimum value of the control. If a value of None
497 is provided, then the control will have no explicit minimum value.
498 If the value specified is greater than the current maximum value,
499 then the function returns 0 and the minimum will not change from
500 its current setting. On success, the function returns 1.
502 If successful and the current value is lower than the new lower
503 bound, if the control is limited, the value will be automatically
504 adjusted to the new minimum value; if not limited, the value in the
505 control will be colored with the current out-of-bounds color.
507 If min > -sys.maxint-1 and the control is configured to not allow longs,
508 the function will return 0, and the min will not be set.
510 if( self
.__max
is None
512 or (self
.__max
is not None and self
.__max
>= min) ):
515 if self
.IsLimited() and min is not None and self
.GetValue() < min:
526 Gets the minimum value of the control. It will return the current
527 minimum integer, or None if not specified.
532 def SetMax(self
, max=None):
534 Sets the maximum value of the control. If a value of None
535 is provided, then the control will have no explicit maximum value.
536 If the value specified is less than the current minimum value, then
537 the function returns 0 and the maximum will not change from its
538 current setting. On success, the function returns 1.
540 If successful and the current value is greater than the new upper
541 bound, if the control is limited the value will be automatically
542 adjusted to this maximum value; if not limited, the value in the
543 control will be colored with the current out-of-bounds color.
545 If max > sys.maxint and the control is configured to not allow longs,
546 the function will return 0, and the max will not be set.
548 if( self
.__min
is None
550 or (self
.__min
is not None and self
.__min
<= max) ):
553 if self
.IsLimited() and max is not None and self
.GetValue() > max:
564 Gets the maximum value of the control. It will return the current
565 maximum integer, or None if not specified.
570 def SetBounds(self
, min=None, max=None):
572 This function is a convenience function for setting the min and max
573 values at the same time. The function only applies the maximum bound
574 if setting the minimum bound is successful, and returns True
575 only if both operations succeed.
576 NOTE: leaving out an argument will remove the corresponding bound.
578 ret
= self
.SetMin(min)
579 return ret
and self
.SetMax(max)
584 This function returns a two-tuple (min,max), indicating the
585 current bounds of the control. Each value can be None if
586 that bound is not set.
588 return (self
.__min
, self
.__max
)
591 def SetLimited(self
, limited
):
593 If called with a value of True, this function will cause the control
594 to limit the value to fall within the bounds currently specified.
595 If the control's value currently exceeds the bounds, it will then
596 be limited accordingly.
598 If called with a value of 0, this function will disable value
599 limiting, but coloring of out-of-bounds values will still take
600 place if bounds have been set for the control.
602 self
.__limited
= limited
606 if not min is None and self
.GetValue() < min:
608 elif not max is None and self
.GetValue() > max:
616 Returns True if the control is currently limiting the
617 value to fall within the current bounds.
619 return self
.__limited
622 def IsInBounds(self
, value
=None):
624 Returns True if no value is specified and the current value
625 of the control falls within the current bounds. This function can
626 also be called with a value to see if that value would fall within
627 the current bounds of the given control.
630 value
= self
.GetValue()
632 if( not (value
is None and self
.IsNoneAllowed())
633 and type(value
) not in (types
.IntType
, types
.LongType
) ):
635 'wxIntCtrl requires integer values, passed %s'% repr(value
) )
639 if min is None: min = value
640 if max is None: max = value
642 # if bounds set, and value is None, return False
643 if value
== None and (min is not None or max is not None):
646 return min <= value
<= max
649 def SetNoneAllowed(self
, allow_none
):
651 Change the behavior of the validation code, allowing control
652 to have a value of None or not, as appropriate. If the value
653 of the control is currently None, and allow_none is 0, the
654 value of the control will be set to the minimum value of the
655 control, or 0 if no lower bound is set.
657 self
.__allow
_none
= allow_none
658 if not allow_none
and self
.GetValue() is None:
660 if min is not None: self
.SetValue(min)
661 else: self
.SetValue(0)
664 def IsNoneAllowed(self
):
665 return self
.__allow
_none
668 def SetLongAllowed(self
, allow_long
):
670 Change the behavior of the validation code, allowing control
671 to have a long value or not, as appropriate. If the value
672 of the control is currently long, and allow_long is 0, the
673 value of the control will be adjusted to fall within the
674 size of an integer type, at either the sys.maxint or -sys.maxint-1,
675 for positive and negative values, respectively.
677 current_value
= self
.GetValue()
678 if not allow_long
and type(current_value
) is types
.LongType
:
679 if current_value
> 0:
680 self
.SetValue(MAXINT
)
682 self
.SetValue(MININT
)
683 self
.__allow
_long
= allow_long
686 def IsLongAllowed(self
):
687 return self
.__allow
_long
691 def SetColors(self
, default_color
=wx
.BLACK
, oob_color
=wx
.RED
):
693 Tells the control what colors to use for normal and out-of-bounds
694 values. If the value currently exceeds the bounds, it will be
695 recolored accordingly.
697 self
.__default
_color
= default_color
698 self
.__oob
_color
= oob_color
704 Returns a tuple of (default_color, oob_color), indicating
705 the current color settings for the control.
707 return self
.__default
_color
, self
.__oob
_color
710 def _colorValue(self
, value
=None):
712 Colors text with oob_color if current value exceeds bounds
715 if not self
.IsInBounds(value
):
716 self
.SetForegroundColour(self
.__oob
_color
)
718 self
.SetForegroundColour(self
.__default
_color
)
722 def _toGUI( self
, value
):
724 Conversion function used to set the value of the control; does
725 type and bounds checking and raises ValueError if argument is
728 if value
is None and self
.IsNoneAllowed():
730 elif type(value
) == types
.LongType
and not self
.IsLongAllowed():
732 'wxIntCtrl requires integer value, passed long' )
733 elif type(value
) not in (types
.IntType
, types
.LongType
):
735 'wxIntCtrl requires integer value, passed %s'% repr(value
) )
737 elif self
.IsLimited():
740 if not min is None and value
< min:
742 'value is below minimum value of control %d'% value
)
743 if not max is None and value
> max:
745 'value exceeds value of control %d'% value
)
750 def _fromGUI( self
, value
):
752 Conversion function used in getting the value of the control.
755 # One or more of the underlying text control implementations
756 # issue an intermediate EVT_TEXT when replacing the control's
757 # value, where the intermediate value is an empty string.
758 # So, to ensure consistency and to prevent spurious ValueErrors,
759 # we make the following test, and react accordingly:
762 if not self
.IsNoneAllowed():
770 if self
.IsLongAllowed():
778 Override the wxTextCtrl's .Cut function, with our own
779 that does validation. Will result in a value of 0
780 if entire contents of control are removed.
782 sel_start
, sel_to
= self
.GetSelection()
783 select_len
= sel_to
- sel_start
784 textval
= wx
.TextCtrl
.GetValue(self
)
786 do
= wx
.TextDataObject()
787 do
.SetText(textval
[sel_start
:sel_to
])
788 wx
.TheClipboard
.Open()
789 wx
.TheClipboard
.SetData(do
)
790 wx
.TheClipboard
.Close()
791 if select_len
== len(wxTextCtrl
.GetValue(self
)):
792 if not self
.IsNoneAllowed():
794 self
.SetInsertionPoint(0)
795 self
.SetSelection(0,1)
799 new_value
= self
._fromGUI
(textval
[:sel_start
] + textval
[sel_to
:])
800 self
.SetValue(new_value
)
803 def _getClipboardContents( self
):
805 Subroutine for getting the current contents of the clipboard.
807 do
= wx
.TextDataObject()
808 wx
.TheClipboard
.Open()
809 success
= wx
.TheClipboard
.GetData(do
)
810 wx
.TheClipboard
.Close()
815 # Remove leading and trailing spaces before evaluating contents
816 return do
.GetText().strip()
821 Override the wxTextCtrl's .Paste function, with our own
822 that does validation. Will raise ValueError if not a
823 valid integerizable value.
825 paste_text
= self
._getClipboardContents
()
827 # (conversion will raise ValueError if paste isn't legal)
828 sel_start
, sel_to
= self
.GetSelection()
829 text
= wx
.TextCtrl
.GetValue( self
)
830 new_text
= text
[:sel_start
] + paste_text
+ text
[sel_to
:]
831 if new_text
== '' and self
.IsNoneAllowed():
834 value
= self
._fromGUI
(new_text
)
836 new_pos
= sel_start
+ len(paste_text
)
837 wx
.CallAfter(self
.SetInsertionPoint
, new_pos
)
841 #===========================================================================
843 if __name__
== '__main__':
847 class myDialog(wx
.Dialog
):
848 def __init__(self
, parent
, id, title
,
849 pos
= wx
.DefaultPosition
, size
= wx
.DefaultSize
,
850 style
= wx
.DEFAULT_DIALOG_STYLE
):
851 wx
.Dialog
.__init
__(self
, parent
, id, title
, pos
, size
, style
)
853 self
.int_ctrl
= wxIntCtrl(self
, wx
.NewId(), size
=(55,20))
854 self
.OK
= wx
.Button( self
, wx
.ID_OK
, "OK")
855 self
.Cancel
= wx
.Button( self
, wx
.ID_CANCEL
, "Cancel")
857 vs
= wx
.BoxSizer( wx
.VERTICAL
)
858 vs
.Add( self
.int_ctrl
, 0, wx
.ALIGN_CENTRE|wx
.ALL
, 5 )
859 hs
= wx
.BoxSizer( wx
.HORIZONTAL
)
860 hs
.Add( self
.OK
, 0, wx
.ALIGN_CENTRE|wx
.ALL
, 5 )
861 hs
.Add( self
.Cancel
, 0, wx
.ALIGN_CENTRE|wx
.ALL
, 5 )
862 vs
.Add(hs
, 0, wx
.ALIGN_CENTRE|wx
.ALL
, 5 )
864 self
.SetAutoLayout( True )
867 vs
.SetSizeHints( self
)
868 self
.Bind(EVT_INT
, self
.OnInt
, self
.int_ctrl
)
870 def OnInt(self
, event
):
871 print 'int now', event
.GetValue()
873 class TestApp(wx
.App
):
876 self
.frame
= wx
.Frame(None, -1, "Test", (20,20), (120,100) )
877 self
.panel
= wx
.Panel(self
.frame
, -1)
878 button
= wx
.Button(self
.panel
, 10, "Push Me", (20, 20))
879 self
.Bind(wx
.EVT_BUTTON
, self
.OnClick
, button
)
881 traceback
.print_exc()
885 def OnClick(self
, event
):
886 dlg
= myDialog(self
.panel
, -1, "test wxIntCtrl")
887 dlg
.int_ctrl
.SetValue(501)
888 dlg
.int_ctrl
.SetInsertionPoint(1)
889 dlg
.int_ctrl
.SetSelection(1,2)
891 print 'final value', dlg
.int_ctrl
.GetValue()
896 self
.frame
.Show(True)
903 traceback
.print_exc()