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
28 from wxPython
.wx
import *
30 from sys
import maxint
31 MAXINT
= maxint
# (constants should be in upper case)
34 #----------------------------------------------------------------------------
36 wxEVT_COMMAND_INT_UPDATED
= wxNewEventType()
38 # wxWindows' wxTextCtrl translates Composite "control key"
39 # events into single events before returning them to its OnChar
40 # routine. The doc says that this results in 1 for Ctrl-A, 2 for
41 # Ctrl-B, etc. However, there are no wxPython or wxWindows
42 # symbols for them, so I'm defining codes for Ctrl-X (cut) and
43 # Ctrl-V (paste) here for readability:
44 WXK_CTRL_X
= (ord('X')+1) - ord('A')
45 WXK_CTRL_V
= (ord('V')+1) - ord('A')
48 def EVT_INT(win
, id, func
):
49 """Used to trap events indicating that the current
50 integer value of the control has been changed."""
51 win
.Connect(id, -1, wxEVT_COMMAND_INT_UPDATED
, func
)
54 class wxIntUpdatedEvent(wxPyCommandEvent
):
55 def __init__(self
, id, value
= 0, object=None):
56 wxPyCommandEvent
.__init
__(self
, wxEVT_COMMAND_INT_UPDATED
, id)
59 self
.SetEventObject(object)
62 """Retrieve the value of the control at the time
63 this event was generated."""
67 #----------------------------------------------------------------------------
69 class wxIntValidator( wxPyValidator
):
71 Validator class used with wxIntCtrl; handles all validation of input
72 prior to changing the value of the underlying wxTextCtrl.
75 wxPyValidator
.__init
__(self
)
76 EVT_CHAR(self
, self
.OnChar
)
79 return self
.__class
__()
81 def Validate(self
, window
): # window here is the *parent* of the ctrl
83 Because each operation on the control is vetted as it's made,
84 the value of the control is always valid.
89 def OnChar(self
, event
):
91 Validates keystrokes to make sure the resulting value will a legal
92 value. Erasing the value causes it to be set to 0, with the value
93 selected, so it can be replaced. Similarly, replacing the value
94 with a '-' sign causes the value to become -1, with the value
95 selected. Leading zeros are removed if introduced by selection,
96 and are prevented from being inserted.
99 ctrl
= event
.GetEventObject()
102 value
= ctrl
.GetValue()
103 textval
= wxTextCtrl
.GetValue(ctrl
)
104 allow_none
= ctrl
.IsNoneAllowed()
106 pos
= ctrl
.GetInsertionPoint()
107 sel_start
, sel_to
= ctrl
.GetSelection()
108 select_len
= sel_to
- sel_start
110 # (Uncomment for debugging:)
111 ## print 'keycode:', key
113 ## print 'sel_start, sel_to:', sel_start, sel_to
114 ## print 'select_len:', select_len
115 ## print 'textval:', textval
117 # set defaults for processing:
129 # Validate action, and predict resulting value, so we can
130 # range check the result and validate that too.
132 if key
in (WXK_DELETE
, WXK_BACK
, WXK_CTRL_X
):
134 new_text
= textval
[:sel_start
] + textval
[sel_to
:]
135 elif key
== WXK_DELETE
and pos
< len(textval
):
136 new_text
= textval
[:pos
] + textval
[pos
+1:]
137 elif key
== WXK_BACK
and pos
> 0:
138 new_text
= textval
[:pos
-1] + textval
[pos
:]
139 # (else value shouldn't change)
141 if new_text
in ('', '-'):
142 # Deletion of last significant digit:
143 if allow_none
and new_text
== '':
151 new_value
= ctrl
._fromGUI
(new_text
)
156 elif key
== WXK_CTRL_V
: # (see comments at top of file)
157 # Only allow paste if number:
158 paste_text
= ctrl
._getClipboardContents
()
159 new_text
= textval
[:sel_start
] + paste_text
+ textval
[sel_to
:]
160 if new_text
== '' and allow_none
:
165 # Convert the resulting strings, verifying they
166 # are legal integers and will fit in proper
167 # size if ctrl limited to int. (if not,
169 new_value
= ctrl
._fromGUI
(new_text
)
171 paste_value
= ctrl
._fromGUI
(paste_text
)
174 new_pos
= sel_start
+ len(str(paste_value
))
176 # if resulting value is 0, truncate and highlight value:
177 if new_value
== 0 and len(new_text
) > 1:
180 elif paste_value
== 0:
181 # Disallow pasting a leading zero with nothing selected:
183 and value
is not None
184 and ( (value
>= 0 and pos
== 0)
185 or (value
< 0 and pos
in [0,1]) ) ):
193 elif key
< WXK_SPACE
or key
> 255:
197 elif chr(key
) == '-':
198 # Allow '-' to result in -1 if replacing entire contents:
200 or (value
== 0 and pos
== 0)
201 or (select_len
>= len(str(abs(value
)))) ):
205 # else allow negative sign only at start, and only if
206 # number isn't already zero or negative:
207 elif pos
!= 0 or (value
is not None and value
< 0):
210 new_text
= '-' + textval
213 new_value
= ctrl
._fromGUI
(new_text
)
218 elif chr(key
) in string
.digits
:
219 # disallow inserting a leading zero with nothing selected
222 and value
is not None
223 and ( (value
>= 0 and pos
== 0)
224 or (value
< 0 and pos
in [0,1]) ) ):
226 # disallow inserting digits before the minus sign:
227 elif value
is not None and value
< 0 and pos
== 0:
230 new_text
= textval
[:sel_start
] + chr(key
) + textval
[sel_to
:]
232 new_value
= ctrl
._fromGUI
(new_text
)
242 # Do range checking for new candidate value:
243 if ctrl
.IsLimited() and not ctrl
.IsInBounds(new_value
):
245 elif new_value
is not None:
246 # ensure resulting text doesn't result in a leading 0:
247 if not set_to_zero
and not set_to_minus_one
:
248 if( (new_value
> 0 and new_text
[0] == '0')
249 or (new_value
< 0 and new_text
[1] == '0')
250 or (new_value
== 0 and select_len
> 1 ) ):
252 # Allow replacement of leading chars with
253 # zero, but remove the leading zero, effectively
254 # making this like "remove leading digits"
256 # Account for leading zero when positioning cursor:
258 or (paste
and paste_value
== 0 and new_pos
> 0) ):
259 new_pos
= new_pos
- 1
261 wxCallAfter(ctrl
.SetValue
, new_value
)
262 wxCallAfter(ctrl
.SetInsertionPoint
, new_pos
)
266 # Always do paste numerically, to remove
267 # leading/trailing spaces
268 wxCallAfter(ctrl
.SetValue
, new_value
)
269 wxCallAfter(ctrl
.SetInsertionPoint
, new_pos
)
272 elif (new_value
== 0 and len(new_text
) > 1 ):
276 ctrl
._colorValue
(new_value
) # (one way or t'other)
278 # (Uncomment for debugging:)
280 ## print 'new value:', new_value
281 ## if paste: print 'paste'
282 ## if set_to_none: print 'set_to_none'
283 ## if set_to_zero: print 'set_to_zero'
284 ## if set_to_minus_one: print 'set_to_minus_one'
285 ## if internally_set: print 'internally_set'
287 ## print 'new text:', new_text
288 ## print 'disallowed'
293 wxCallAfter(ctrl
.SetValue
, new_value
)
296 # select to "empty" numeric value
297 wxCallAfter(ctrl
.SetValue
, new_value
)
298 wxCallAfter(ctrl
.SetInsertionPoint
, 0)
299 wxCallAfter(ctrl
.SetSelection
, 0, 1)
301 elif set_to_minus_one
:
302 wxCallAfter(ctrl
.SetValue
, new_value
)
303 wxCallAfter(ctrl
.SetInsertionPoint
, 1)
304 wxCallAfter(ctrl
.SetSelection
, 1, 2)
306 elif not internally_set
:
307 event
.Skip() # allow base wxTextCtrl to finish processing
309 elif not wxValidator_IsSilent():
313 def TransferToWindow(self
):
314 """ Transfer data from validator to window.
316 The default implementation returns False, indicating that an error
317 occurred. We simply return True, as we don't do any data transfer.
319 return True # Prevent wxDialog from complaining.
322 def TransferFromWindow(self
):
323 """ Transfer data from window to validator.
325 The default implementation returns False, indicating that an error
326 occurred. We simply return True, as we don't do any data transfer.
328 return True # Prevent wxDialog from complaining.
331 #----------------------------------------------------------------------------
333 class wxIntCtrl(wxTextCtrl
):
335 This class provides a control that takes and returns integers as
336 value, and provides bounds support and optional value limiting.
341 pos = wxDefaultPosition,
342 size = wxDefaultSize,
344 validator = wxDefaultValidator,
351 default_color = wxBLACK,
355 If no initial value is set, the default will be zero, or
356 the minimum value, if specified. If an illegal string is specified,
357 a ValueError will result. (You can always later set the initial
358 value with SetValue() after instantiation of the control.)
360 The minimum value that the control should allow. This can be
361 adjusted with SetMin(). If the control is not limited, any value
362 below this bound will be colored with the current out-of-bounds color.
363 If min < -sys.maxint-1 and the control is configured to not allow long
364 values, the minimum bound will still be set to the long value, but
365 the implicit bound will be -sys.maxint-1.
367 The maximum value that the control should allow. This can be
368 adjusted with SetMax(). If the control is not limited, any value
369 above this bound will be colored with the current out-of-bounds color.
370 if max > sys.maxint and the control is configured to not allow long
371 values, the maximum bound will still be set to the long value, but
372 the implicit bound will be sys.maxint.
375 Boolean indicating whether the control prevents values from
376 exceeding the currently set minimum and maximum values (bounds).
377 If False and bounds are set, out-of-bounds values will
378 be colored with the current out-of-bounds color.
381 Boolean indicating whether or not the control is allowed to be
382 empty, representing a value of None for the control.
385 Boolean indicating whether or not the control is allowed to hold
386 and return a long as well as an int.
389 Color value used for in-bounds values of the control.
392 Color value used for out-of-bounds values of the control
393 when the bounds are set but the control is not limited.
396 Normally None, wxIntCtrl uses its own validator to do value
397 validation and input control. However, a validator derived
398 from wxIntValidator can be supplied to override the data
399 transfer methods for the wxIntValidator class.
403 self
, parent
, id=-1, value
= 0,
404 pos
= wxDefaultPosition
, size
= wxDefaultSize
,
405 style
= 0, validator
= wxDefaultValidator
,
408 limited
= 0, allow_none
= 0, allow_long
= 0,
409 default_color
= wxBLACK
, oob_color
= wxRED
,
412 # Establish attrs required for any operation on value:
416 self
.__default
_color
= wxBLACK
417 self
.__oob
_color
= wxRED
418 self
.__allow
_none
= 0
419 self
.__allow
_long
= 0
420 self
.__oldvalue
= None
422 if validator
== wxDefaultValidator
:
423 validator
= wxIntValidator()
426 self
, parent
, id, self
._toGUI
(0),
427 pos
, size
, style
, validator
, name
)
429 # The following lets us set out our "integer update" events:
430 EVT_TEXT( self
, self
.GetId(), self
.OnText
)
432 # Establish parameters, with appropriate error checking
434 self
.SetBounds(min, max)
435 self
.SetLimited(limited
)
436 self
.SetColors(default_color
, oob_color
)
437 self
.SetNoneAllowed(allow_none
)
438 self
.SetLongAllowed(allow_long
)
443 def OnText( self
, event
):
445 Handles an event indicating that the text control's value
446 has changed, and issue EVT_INT event.
447 NOTE: using wxTextCtrl.SetValue() to change the control's
448 contents from within a EVT_CHAR handler can cause double
449 text events. So we check for actual changes to the text
450 before passing the events on.
452 value
= self
.GetValue()
453 if value
!= self
.__oldvalue
:
455 self
.GetEventHandler().ProcessEvent(
456 wxIntUpdatedEvent( self
.GetId(), self
.GetValue(), self
) )
459 # let normal processing of the text continue
461 self
.__oldvalue
= value
# record for next event
466 Returns the current integer (long) value of the control.
468 return self
._fromGUI
( wxTextCtrl
.GetValue(self
) )
470 def SetValue(self
, value
):
472 Sets the value of the control to the integer value specified.
473 The resulting actual value of the control may be altered to
474 conform with the bounds set on the control if limited,
475 or colored if not limited but the value is out-of-bounds.
476 A ValueError exception will be raised if an invalid value
479 wxTextCtrl
.SetValue( self
, self
._toGUI
(value
) )
483 def SetMin(self
, min=None):
485 Sets the minimum value of the control. If a value of None
486 is provided, then the control will have no explicit minimum value.
487 If the value specified is greater than the current maximum value,
488 then the function returns 0 and the minimum will not change from
489 its current setting. On success, the function returns 1.
491 If successful and the current value is lower than the new lower
492 bound, if the control is limited, the value will be automatically
493 adjusted to the new minimum value; if not limited, the value in the
494 control will be colored with the current out-of-bounds color.
496 If min > -sys.maxint-1 and the control is configured to not allow longs,
497 the function will return 0, and the min will not be set.
499 if( self
.__max
is None
501 or (self
.__max
is not None and self
.__max
>= min) ):
504 if self
.IsLimited() and min is not None and self
.GetValue() < min:
515 Gets the minimum value of the control. It will return the current
516 minimum integer, or None if not specified.
521 def SetMax(self
, max=None):
523 Sets the maximum value of the control. If a value of None
524 is provided, then the control will have no explicit maximum value.
525 If the value specified is less than the current minimum value, then
526 the function returns 0 and the maximum will not change from its
527 current setting. On success, the function returns 1.
529 If successful and the current value is greater than the new upper
530 bound, if the control is limited the value will be automatically
531 adjusted to this maximum value; if not limited, the value in the
532 control will be colored with the current out-of-bounds color.
534 If max > sys.maxint and the control is configured to not allow longs,
535 the function will return 0, and the max will not be set.
537 if( self
.__min
is None
539 or (self
.__min
is not None and self
.__min
<= max) ):
542 if self
.IsLimited() and max is not None and self
.GetValue() > max:
553 Gets the maximum value of the control. It will return the current
554 maximum integer, or None if not specified.
559 def SetBounds(self
, min=None, max=None):
561 This function is a convenience function for setting the min and max
562 values at the same time. The function only applies the maximum bound
563 if setting the minimum bound is successful, and returns True
564 only if both operations succeed.
565 NOTE: leaving out an argument will remove the corresponding bound.
567 ret
= self
.SetMin(min)
568 return ret
and self
.SetMax(max)
573 This function returns a two-tuple (min,max), indicating the
574 current bounds of the control. Each value can be None if
575 that bound is not set.
577 return (self
.__min
, self
.__max
)
580 def SetLimited(self
, limited
):
582 If called with a value of True, this function will cause the control
583 to limit the value to fall within the bounds currently specified.
584 If the control's value currently exceeds the bounds, it will then
585 be limited accordingly.
587 If called with a value of 0, this function will disable value
588 limiting, but coloring of out-of-bounds values will still take
589 place if bounds have been set for the control.
591 self
.__limited
= limited
595 if not min is None and self
.GetValue() < min:
597 elif not max is None and self
.GetValue() > max:
605 Returns True if the control is currently limiting the
606 value to fall within the current bounds.
608 return self
.__limited
611 def IsInBounds(self
, value
=None):
613 Returns True if no value is specified and the current value
614 of the control falls within the current bounds. This function can
615 also be called with a value to see if that value would fall within
616 the current bounds of the given control.
619 value
= self
.GetValue()
621 if( not (value
is None and self
.IsNoneAllowed())
622 and type(value
) not in (types
.IntType
, types
.LongType
) ):
624 'wxIntCtrl requires integer values, passed %s'% repr(value
) )
628 if min is None: min = value
629 if max is None: max = value
631 # if bounds set, and value is None, return False
632 if value
== None and (min is not None or max is not None):
635 return min <= value
<= max
638 def SetNoneAllowed(self
, allow_none
):
640 Change the behavior of the validation code, allowing control
641 to have a value of None or not, as appropriate. If the value
642 of the control is currently None, and allow_none is 0, the
643 value of the control will be set to the minimum value of the
644 control, or 0 if no lower bound is set.
646 self
.__allow
_none
= allow_none
647 if not allow_none
and self
.GetValue() is None:
649 if min is not None: self
.SetValue(min)
650 else: self
.SetValue(0)
653 def IsNoneAllowed(self
):
654 return self
.__allow
_none
657 def SetLongAllowed(self
, allow_long
):
659 Change the behavior of the validation code, allowing control
660 to have a long value or not, as appropriate. If the value
661 of the control is currently long, and allow_long is 0, the
662 value of the control will be adjusted to fall within the
663 size of an integer type, at either the sys.maxint or -sys.maxint-1,
664 for positive and negative values, respectively.
666 current_value
= self
.GetValue()
667 if not allow_long
and type(current_value
) is types
.LongType
:
668 if current_value
> 0:
669 self
.SetValue(MAXINT
)
671 self
.SetValue(MININT
)
672 self
.__allow
_long
= allow_long
675 def IsLongAllowed(self
):
676 return self
.__allow
_long
680 def SetColors(self
, default_color
=wxBLACK
, oob_color
=wxRED
):
682 Tells the control what colors to use for normal and out-of-bounds
683 values. If the value currently exceeds the bounds, it will be
684 recolored accordingly.
686 self
.__default
_color
= default_color
687 self
.__oob
_color
= oob_color
693 Returns a tuple of (default_color, oob_color), indicating
694 the current color settings for the control.
696 return self
.__default
_color
, self
.__oob
_color
699 def _colorValue(self
, value
=None):
701 Colors text with oob_color if current value exceeds bounds
704 if not self
.IsInBounds(value
):
705 self
.SetForegroundColour(self
.__oob
_color
)
707 self
.SetForegroundColour(self
.__default
_color
)
711 def _toGUI( self
, value
):
713 Conversion function used to set the value of the control; does
714 type and bounds checking and raises ValueError if argument is
717 if value
is None and self
.IsNoneAllowed():
719 elif type(value
) == types
.LongType
and not self
.IsLongAllowed():
721 'wxIntCtrl requires integer value, passed long' )
722 elif type(value
) not in (types
.IntType
, types
.LongType
):
724 'wxIntCtrl requires integer value, passed %s'% repr(value
) )
726 elif self
.IsLimited():
729 if not min is None and value
< min:
731 'value is below minimum value of control %d'% value
)
732 if not max is None and value
> max:
734 'value exceeds value of control %d'% value
)
739 def _fromGUI( self
, value
):
741 Conversion function used in getting the value of the control.
744 # One or more of the underlying text control implementations
745 # issue an intermediate EVT_TEXT when replacing the control's
746 # value, where the intermediate value is an empty string.
747 # So, to ensure consistency and to prevent spurious ValueErrors,
748 # we make the following test, and react accordingly:
751 if not self
.IsNoneAllowed():
759 if self
.IsLongAllowed():
767 Override the wxTextCtrl's .Cut function, with our own
768 that does validation. Will result in a value of 0
769 if entire contents of control are removed.
771 sel_start
, sel_to
= self
.GetSelection()
772 select_len
= sel_to
- sel_start
773 textval
= wxTextCtrl
.GetValue(self
)
775 do
= wxTextDataObject()
776 do
.SetText(textval
[sel_start
:sel_to
])
777 wxTheClipboard
.Open()
778 wxTheClipboard
.SetData(do
)
779 wxTheClipboard
.Close()
780 if select_len
== len(wxTextCtrl
.GetValue(self
)):
781 if not self
.IsNoneAllowed():
783 self
.SetInsertionPoint(0)
784 self
.SetSelection(0,1)
788 new_value
= self
._fromGUI
(textval
[:sel_start
] + textval
[sel_to
:])
789 self
.SetValue(new_value
)
792 def _getClipboardContents( self
):
794 Subroutine for getting the current contents of the clipboard.
796 do
= wxTextDataObject()
797 wxTheClipboard
.Open()
798 success
= wxTheClipboard
.GetData(do
)
799 wxTheClipboard
.Close()
804 # Remove leading and trailing spaces before evaluating contents
805 return do
.GetText().strip()
810 Override the wxTextCtrl's .Paste function, with our own
811 that does validation. Will raise ValueError if not a
812 valid integerizable value.
814 paste_text
= self
._getClipboardContents
()
816 # (conversion will raise ValueError if paste isn't legal)
817 sel_start
, sel_to
= self
.GetSelection()
818 text
= wxTextCtrl
.GetValue( self
)
819 new_text
= text
[:sel_start
] + paste_text
+ text
[sel_to
:]
820 if new_text
== '' and self
.IsNoneAllowed():
823 value
= self
._fromGUI
(new_text
)
825 new_pos
= sel_start
+ len(paste_text
)
826 wxCallAfter(self
.SetInsertionPoint
, new_pos
)
830 #===========================================================================
832 if __name__
== '__main__':
836 class myDialog(wxDialog
):
837 def __init__(self
, parent
, id, title
,
838 pos
= wxPyDefaultPosition
, size
= wxPyDefaultSize
,
839 style
= wxDEFAULT_DIALOG_STYLE
):
840 wxDialog
.__init
__(self
, parent
, id, title
, pos
, size
, style
)
842 self
.int_ctrl
= wxIntCtrl(self
, wxNewId(), size
=(55,20))
843 self
.OK
= wxButton( self
, wxID_OK
, "OK")
844 self
.Cancel
= wxButton( self
, wxID_CANCEL
, "Cancel")
846 vs
= wxBoxSizer( wxVERTICAL
)
847 vs
.AddWindow( self
.int_ctrl
, 0, wxALIGN_CENTRE|wxALL
, 5 )
848 hs
= wxBoxSizer( wxHORIZONTAL
)
849 hs
.AddWindow( self
.OK
, 0, wxALIGN_CENTRE|wxALL
, 5 )
850 hs
.AddWindow( self
.Cancel
, 0, wxALIGN_CENTRE|wxALL
, 5 )
851 vs
.AddSizer(hs
, 0, wxALIGN_CENTRE|wxALL
, 5 )
853 self
.SetAutoLayout( True )
856 vs
.SetSizeHints( self
)
857 EVT_INT(self
, self
.int_ctrl
.GetId(), self
.OnInt
)
859 def OnInt(self
, event
):
860 print 'int now', event
.GetValue()
862 class TestApp(wxApp
):
865 self
.frame
= wxFrame(NULL
, -1, "Test",
866 wxPoint(20,20), wxSize(120,100) )
867 self
.panel
= wxPanel(self
.frame
, -1)
868 button
= wxButton(self
.panel
, 10, "Push Me",
870 EVT_BUTTON(self
, 10, self
.OnClick
)
872 traceback
.print_exc()
876 def OnClick(self
, event
):
877 dlg
= myDialog(self
.panel
, -1, "test wxIntCtrl")
878 dlg
.int_ctrl
.SetValue(501)
879 dlg
.int_ctrl
.SetInsertionPoint(1)
880 dlg
.int_ctrl
.SetSelection(1,2)
882 print 'final value', dlg
.int_ctrl
.GetValue()
887 self
.frame
.Show(True)
894 traceback
.print_exc()