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.
346 default_color = wxBLACK,
348 pos = wxDefaultPosition,
349 size = wxDefaultSize,
354 If no initial value is set, the default will be zero, or
355 the minimum value, if specified. If an illegal string is specified,
356 a ValueError will result. (You can always later set the initial
357 value with SetValue() after instantiation of the control.)
359 The minimum value that the control should allow. This can be
360 adjusted with SetMin(). If the control is not limited, any value
361 below this bound will be colored with the current out-of-bounds color.
362 If min < -sys.maxint-1 and the control is configured to not allow long
363 values, the minimum bound will still be set to the long value, but
364 the implicit bound will be -sys.maxint-1.
366 The maximum value that the control should allow. This can be
367 adjusted with SetMax(). If the control is not limited, any value
368 above this bound will be colored with the current out-of-bounds color.
369 if max > sys.maxint and the control is configured to not allow long
370 values, the maximum bound will still be set to the long value, but
371 the implicit bound will be sys.maxint.
374 Boolean indicating whether the control prevents values from
375 exceeding the currently set minimum and maximum values (bounds).
376 If False and bounds are set, out-of-bounds values will
377 be colored with the current out-of-bounds color.
380 Boolean indicating whether or not the control is allowed to be
381 empty, representing a value of None for the control.
384 Boolean indicating whether or not the control is allowed to hold
385 and return a long as well as an int.
388 Color value used for in-bounds values of the control.
391 Color value used for out-of-bounds values of the control
392 when the bounds are set but the control is not limited.
396 value
= 0, min=None, max=None,
397 limited
= 0, allow_none
= 0, allow_long
= 0,
398 default_color
= wxBLACK
, oob_color
= wxRED
,
399 pos
= wxDefaultPosition
, size
= wxDefaultSize
,
400 style
= 0, name
= "integer",
403 # Establish attrs required for any operation on value:
407 self
.__default
_color
= wxBLACK
408 self
.__oob
_color
= wxRED
409 self
.__allow
_none
= 0
410 self
.__allow
_long
= 0
413 self
, parent
, id, self
._toGUI
(0),
414 pos
, size
, style
, wxIntValidator(), name
)
416 # The following lets us set out our "integer update" events:
417 EVT_TEXT( self
, self
.GetId(), self
.OnText
)
419 # Establish parameters, with appropriate error checking
421 self
.SetBounds(min, max)
422 self
.SetLimited(limited
)
423 self
.SetColors(default_color
, oob_color
)
424 self
.SetNoneAllowed(allow_none
)
425 self
.SetLongAllowed(allow_long
)
429 def OnText( self
, event
):
431 Handles an event indicating that the text control's value
432 has changed, and issue EVT_INT event.
435 self
.GetEventHandler().ProcessEvent(
436 wxIntUpdatedEvent( self
.GetId(), self
.GetValue(), self
) )
439 # let normal processing of the text continue
445 Returns the current integer (long) value of the control.
447 return self
._fromGUI
( wxTextCtrl
.GetValue(self
) )
449 def SetValue(self
, value
):
451 Sets the value of the control to the integer value specified.
452 The resulting actual value of the control may be altered to
453 conform with the bounds set on the control if limited,
454 or colored if not limited but the value is out-of-bounds.
455 A ValueError exception will be raised if an invalid value
458 wxTextCtrl
.SetValue( self
, self
._toGUI
(value
) )
462 def SetMin(self
, min=None):
464 Sets the minimum value of the control. If a value of None
465 is provided, then the control will have no explicit minimum value.
466 If the value specified is greater than the current maximum value,
467 then the function returns 0 and the minimum will not change from
468 its current setting. On success, the function returns 1.
470 If successful and the current value is lower than the new lower
471 bound, if the control is limited, the value will be automatically
472 adjusted to the new minimum value; if not limited, the value in the
473 control will be colored with the current out-of-bounds color.
475 If min > -sys.maxint-1 and the control is configured to not allow longs,
476 the function will return 0, and the min will not be set.
478 if( self
.__max
is None
480 or (self
.__max
is not None and self
.__max
>= min) ):
483 if self
.IsLimited() and min is not None and self
.GetValue() < min:
494 Gets the minimum value of the control. It will return the current
495 minimum integer, or None if not specified.
500 def SetMax(self
, max=None):
502 Sets the maximum value of the control. If a value of None
503 is provided, then the control will have no explicit maximum value.
504 If the value specified is less than the current minimum value, then
505 the function returns 0 and the maximum will not change from its
506 current setting. On success, the function returns 1.
508 If successful and the current value is greater than the new upper
509 bound, if the control is limited the value will be automatically
510 adjusted to this maximum value; if not limited, the value in the
511 control will be colored with the current out-of-bounds color.
513 If max > sys.maxint and the control is configured to not allow longs,
514 the function will return 0, and the max will not be set.
516 if( self
.__min
is None
518 or (self
.__min
is not None and self
.__min
<= max) ):
521 if self
.IsLimited() and max is not None and self
.GetValue() > max:
532 Gets the maximum value of the control. It will return the current
533 maximum integer, or None if not specified.
538 def SetBounds(self
, min=None, max=None):
540 This function is a convenience function for setting the min and max
541 values at the same time. The function only applies the maximum bound
542 if setting the minimum bound is successful, and returns True
543 only if both operations succeed.
544 NOTE: leaving out an argument will remove the corresponding bound.
546 ret
= self
.SetMin(min)
547 return ret
and self
.SetMax(max)
552 This function returns a two-tuple (min,max), indicating the
553 current bounds of the control. Each value can be None if
554 that bound is not set.
556 return (self
.__min
, self
.__max
)
559 def SetLimited(self
, limited
):
561 If called with a value of True, this function will cause the control
562 to limit the value to fall within the bounds currently specified.
563 If the control's value currently exceeds the bounds, it will then
564 be limited accordingly.
566 If called with a value of 0, this function will disable value
567 limiting, but coloring of out-of-bounds values will still take
568 place if bounds have been set for the control.
570 self
.__limited
= limited
574 if not min is None and self
.GetValue() < min:
576 elif not max is None and self
.GetValue() > max:
584 Returns True if the control is currently limiting the
585 value to fall within the current bounds.
587 return self
.__limited
590 def IsInBounds(self
, value
=None):
592 Returns True if no value is specified and the current value
593 of the control falls within the current bounds. This function can
594 also be called with a value to see if that value would fall within
595 the current bounds of the given control.
598 value
= self
.GetValue()
600 if( not (value
is None and self
.IsNoneAllowed())
601 and type(value
) not in (types
.IntType
, types
.LongType
) ):
603 'wxIntCtrl requires integer values, passed %s'% repr(value
) )
607 if min is None: min = value
608 if max is None: max = value
610 # if bounds set, and value is None, return False
611 if value
== None and (min is not None or max is not None):
614 return min <= value
<= max
617 def SetNoneAllowed(self
, allow_none
):
619 Change the behavior of the validation code, allowing control
620 to have a value of None or not, as appropriate. If the value
621 of the control is currently None, and allow_none is 0, the
622 value of the control will be set to the minimum value of the
623 control, or 0 if no lower bound is set.
625 self
.__allow
_none
= allow_none
626 if not allow_none
and self
.GetValue() is None:
628 if min is not None: self
.SetValue(min)
629 else: self
.SetValue(0)
632 def IsNoneAllowed(self
):
633 return self
.__allow
_none
636 def SetLongAllowed(self
, allow_long
):
638 Change the behavior of the validation code, allowing control
639 to have a long value or not, as appropriate. If the value
640 of the control is currently long, and allow_long is 0, the
641 value of the control will be adjusted to fall within the
642 size of an integer type, at either the sys.maxint or -sys.maxint-1,
643 for positive and negative values, respectively.
645 current_value
= self
.GetValue()
646 if not allow_long
and type(current_value
) is types
.LongType
:
647 if current_value
> 0:
648 self
.SetValue(MAXINT
)
650 self
.SetValue(MININT
)
651 self
.__allow
_long
= allow_long
654 def IsLongAllowed(self
):
655 return self
.__allow
_long
659 def SetColors(self
, default_color
=wxBLACK
, oob_color
=wxRED
):
661 Tells the control what colors to use for normal and out-of-bounds
662 values. If the value currently exceeds the bounds, it will be
663 recolored accordingly.
665 self
.__default
_color
= default_color
666 self
.__oob
_color
= oob_color
672 Returns a tuple of (default_color, oob_color), indicating
673 the current color settings for the control.
675 return self
.__default
_color
, self
.__oob
_color
678 def _colorValue(self
, value
=None):
680 Colors text with oob_color if current value exceeds bounds
683 if not self
.IsInBounds(value
):
684 self
.SetForegroundColour(self
.__oob
_color
)
686 self
.SetForegroundColour(self
.__default
_color
)
690 def _toGUI( self
, value
):
692 Conversion function used to set the value of the control; does
693 type and bounds checking and raises ValueError if argument is
696 if value
is None and self
.IsNoneAllowed():
698 elif type(value
) == types
.LongType
and not self
.IsLongAllowed():
700 'wxIntCtrl requires integer value, passed long' )
701 elif type(value
) not in (types
.IntType
, types
.LongType
):
703 'wxIntCtrl requires integer value, passed %s'% repr(value
) )
705 elif self
.IsLimited():
708 if not min is None and value
< min:
710 'value is below minimum value of control %d'% value
)
711 if not max is None and value
> max:
713 'value exceeds value of control %d'% value
)
718 def _fromGUI( self
, value
):
720 Conversion function used in getting the value of the control.
723 if not self
.IsNoneAllowed():
731 if self
.IsLongAllowed():
739 Override the wxTextCtrl's .Cut function, with our own
740 that does validation. Will result in a value of 0
741 if entire contents of control are removed.
743 sel_start
, sel_to
= self
.GetSelection()
744 select_len
= sel_to
- sel_start
745 textval
= wxTextCtrl
.GetValue(self
)
747 do
= wxTextDataObject()
748 do
.SetText(textval
[sel_start
:sel_to
])
749 wxTheClipboard
.Open()
750 wxTheClipboard
.SetData(do
)
751 wxTheClipboard
.Close()
752 if select_len
== len(wxTextCtrl
.GetValue(self
)):
753 if not self
.IsNoneAllowed():
755 self
.SetInsertionPoint(0)
756 self
.SetSelection(0,1)
760 new_value
= self
._fromGUI
(textval
[:sel_start
] + textval
[sel_to
:])
761 self
.SetValue(new_value
)
764 def _getClipboardContents( self
):
766 Subroutine for getting the current contents of the clipboard.
768 do
= wxTextDataObject()
769 wxTheClipboard
.Open()
770 success
= wxTheClipboard
.GetData(do
)
771 wxTheClipboard
.Close()
776 # Remove leading and trailing spaces before evaluating contents
777 return do
.GetText().strip()
782 Override the wxTextCtrl's .Paste function, with our own
783 that does validation. Will raise ValueError if not a
784 valid integerizable value.
786 paste_text
= self
._getClipboardContents
()
788 # (conversion will raise ValueError if paste isn't legal)
789 sel_start
, sel_to
= self
.GetSelection()
790 text
= wxTextCtrl
.GetValue( self
)
791 new_text
= text
[:sel_start
] + paste_text
+ text
[sel_to
:]
792 if new_text
== '' and self
.IsNoneAllowed():
795 value
= self
._fromGUI
(new_text
)
797 new_pos
= sel_start
+ len(paste_text
)
798 wxCallAfter(self
.SetInsertionPoint
, new_pos
)
802 #===========================================================================
804 if __name__
== '__main__':
808 class myDialog(wxDialog
):
809 def __init__(self
, parent
, id, title
,
810 pos
= wxPyDefaultPosition
, size
= wxPyDefaultSize
,
811 style
= wxDEFAULT_DIALOG_STYLE
):
812 wxDialog
.__init
__(self
, parent
, id, title
, pos
, size
, style
)
814 self
.int_ctrl
= wxIntCtrl(self
, NewId(), size
=(55,20))
815 self
.OK
= wxButton( self
, wxID_OK
, "OK")
816 self
.Cancel
= wxButton( self
, wxID_CANCEL
, "Cancel")
818 vs
= wxBoxSizer( wxVERTICAL
)
819 vs
.AddWindow( self
.int_ctrl
, 0, wxALIGN_CENTRE|wxALL
, 5 )
820 hs
= wxBoxSizer( wxHORIZONTAL
)
821 hs
.AddWindow( self
.OK
, 0, wxALIGN_CENTRE|wxALL
, 5 )
822 hs
.AddWindow( self
.Cancel
, 0, wxALIGN_CENTRE|wxALL
, 5 )
823 vs
.AddSizer(hs
, 0, wxALIGN_CENTRE|wxALL
, 5 )
825 self
.SetAutoLayout( True )
828 vs
.SetSizeHints( self
)
829 EVT_INT(self
, self
.int_ctrl
.GetId(), self
.OnInt
)
831 def OnInt(self
, event
):
832 print 'int now', event
.GetValue()
834 class TestApp(wxApp
):
837 self
.frame
= wxFrame(NULL
, -1, "Test",
838 wxPoint(20,20), wxSize(120,100) )
839 self
.panel
= wxPanel(self
.frame
, -1)
840 button
= wxButton(self
.panel
, 10, "Push Me",
842 EVT_BUTTON(self
, 10, self
.OnClick
)
844 traceback
.print_exc()
848 def OnClick(self
, event
):
849 dlg
= myDialog(self
.panel
, -1, "test wxIntCtrl")
850 dlg
.int_ctrl
.SetValue(501)
851 dlg
.int_ctrl
.SetInsertionPoint(1)
852 dlg
.int_ctrl
.SetSelection(1,2)
854 print 'final value', dlg
.int_ctrl
.GetValue()
859 self
.frame
.Show(True)
866 traceback
.print_exc()