1
#----------------------------------------------------------------------------
2
# Name: wxPython.lib.intctrl.py
5
# Copyright: (c) 2003 by Will Sadkin
6
# RCS-ID: $Id: intctrl.py,v 1.6 2003/12/22 19:09:52 RD Exp $
7
# License: wxWindows license
8
#----------------------------------------------------------------------------
10
# This was written to provide a standard integer edit control for wxPython.
12
# IntCtrl 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
# IntCtrl 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
32
# 12/20/2003 - Jeff Grimmett (grimmtooth@softhome.net)
34
# o wxIntUpdateEvent -> IntUpdateEvent
35
# o wxIntValidator -> IntValidator
36
# o wxIntCtrl -> IntCtrl
44
#----------------------------------------------------------------------------
46
from sys import maxint
47
MAXINT = maxint # (constants should be in upper case)
50
#----------------------------------------------------------------------------
52
# Used to trap events indicating that the current
53
# integer value of the control has been changed.
54
wxEVT_COMMAND_INT_UPDATED = wx.NewEventType()
55
EVT_INT = wx.PyEventBinder(wxEVT_COMMAND_INT_UPDATED, 1)
57
#----------------------------------------------------------------------------
59
# wxWindows' wxTextCtrl translates Composite "control key"
60
# events into single events before returning them to its OnChar
61
# routine. The doc says that this results in 1 for Ctrl-A, 2 for
62
# Ctrl-B, etc. However, there are no wxPython or wxWindows
63
# symbols for them, so I'm defining codes for Ctrl-X (cut) and
64
# Ctrl-V (paste) here for readability:
65
WXK_CTRL_X = (ord('X')+1) - ord('A')
66
WXK_CTRL_V = (ord('V')+1) - ord('A')
68
class IntUpdatedEvent(wx.PyCommandEvent):
69
def __init__(self, id, value = 0, object=None):
70
wx.PyCommandEvent.__init__(self, wxEVT_COMMAND_INT_UPDATED, id)
73
self.SetEventObject(object)
76
"""Retrieve the value of the control at the time
77
this event was generated."""
81
#----------------------------------------------------------------------------
83
class IntValidator( wx.PyValidator ):
85
Validator class used with IntCtrl; handles all validation of input
86
prior to changing the value of the underlying wx.TextCtrl.
89
wx.PyValidator.__init__(self)
90
self.Bind(wx.EVT_CHAR, self.OnChar)
93
return self.__class__()
95
def Validate(self, window): # window here is the *parent* of the ctrl
97
Because each operation on the control is vetted as it's made,
98
the value of the control is always valid.
103
def OnChar(self, event):
105
Validates keystrokes to make sure the resulting value will a legal
106
value. Erasing the value causes it to be set to 0, with the value
107
selected, so it can be replaced. Similarly, replacing the value
108
with a '-' sign causes the value to become -1, with the value
109
selected. Leading zeros are removed if introduced by selection,
110
and are prevented from being inserted.
112
key = event.KeyCode()
113
ctrl = event.GetEventObject()
116
value = ctrl.GetValue()
117
textval = wx.TextCtrl.GetValue(ctrl)
118
allow_none = ctrl.IsNoneAllowed()
120
pos = ctrl.GetInsertionPoint()
121
sel_start, sel_to = ctrl.GetSelection()
122
select_len = sel_to - sel_start
124
# (Uncomment for debugging:)
125
## print 'keycode:', key
127
## print 'sel_start, sel_to:', sel_start, sel_to
128
## print 'select_len:', select_len
129
## print 'textval:', textval
131
# set defaults for processing:
143
# Validate action, and predict resulting value, so we can
144
# range check the result and validate that too.
146
if key in (wx.WXK_DELETE, wx.WXK_BACK, WXK_CTRL_X):
148
new_text = textval[:sel_start] + textval[sel_to:]
149
elif key == wx.WXK_DELETE and pos < len(textval):
150
new_text = textval[:pos] + textval[pos+1:]
151
elif key == wx.WXK_BACK and pos > 0:
152
new_text = textval[:pos-1] + textval[pos:]
153
# (else value shouldn't change)
155
if new_text in ('', '-'):
156
# Deletion of last significant digit:
157
if allow_none and new_text == '':
165
new_value = ctrl._fromGUI(new_text)
170
elif key == WXK_CTRL_V: # (see comments at top of file)
171
# Only allow paste if number:
172
paste_text = ctrl._getClipboardContents()
173
new_text = textval[:sel_start] + paste_text + textval[sel_to:]
174
if new_text == '' and allow_none:
179
# Convert the resulting strings, verifying they
180
# are legal integers and will fit in proper
181
# size if ctrl limited to int. (if not,
183
new_value = ctrl._fromGUI(new_text)
186
paste_value = ctrl._fromGUI(paste_text)
190
new_pos = sel_start + len(str(paste_value))
192
# if resulting value is 0, truncate and highlight value:
193
if new_value == 0 and len(new_text) > 1:
196
elif paste_value == 0:
197
# Disallow pasting a leading zero with nothing selected:
199
and value is not None
200
and ( (value >= 0 and pos == 0)
201
or (value < 0 and pos in [0,1]) ) ):
210
elif key < wx.WXK_SPACE or key > 255:
214
elif chr(key) == '-':
215
# Allow '-' to result in -1 if replacing entire contents:
217
or (value == 0 and pos == 0)
218
or (select_len >= len(str(abs(value)))) ):
222
# else allow negative sign only at start, and only if
223
# number isn't already zero or negative:
224
elif pos != 0 or (value is not None and value < 0):
227
new_text = '-' + textval
230
new_value = ctrl._fromGUI(new_text)
235
elif chr(key) in string.digits:
236
# disallow inserting a leading zero with nothing selected
239
and value is not None
240
and ( (value >= 0 and pos == 0)
241
or (value < 0 and pos in [0,1]) ) ):
243
# disallow inserting digits before the minus sign:
244
elif value is not None and value < 0 and pos == 0:
247
new_text = textval[:sel_start] + chr(key) + textval[sel_to:]
249
new_value = ctrl._fromGUI(new_text)
259
# Do range checking for new candidate value:
260
if ctrl.IsLimited() and not ctrl.IsInBounds(new_value):
262
elif new_value is not None:
263
# ensure resulting text doesn't result in a leading 0:
264
if not set_to_zero and not set_to_minus_one:
265
if( (new_value > 0 and new_text[0] == '0')
266
or (new_value < 0 and new_text[1] == '0')
267
or (new_value == 0 and select_len > 1 ) ):
269
# Allow replacement of leading chars with
270
# zero, but remove the leading zero, effectively
271
# making this like "remove leading digits"
273
# Account for leading zero when positioning cursor:
274
if( key == wx.WXK_BACK
275
or (paste and paste_value == 0 and new_pos > 0) ):
276
new_pos = new_pos - 1
278
wx.CallAfter(ctrl.SetValue, new_value)
279
wx.CallAfter(ctrl.SetInsertionPoint, new_pos)
283
# Always do paste numerically, to remove
284
# leading/trailing spaces
285
wx.CallAfter(ctrl.SetValue, new_value)
286
wx.CallAfter(ctrl.SetInsertionPoint, new_pos)
289
elif (new_value == 0 and len(new_text) > 1 ):
293
ctrl._colorValue(new_value) # (one way or t'other)
295
# (Uncomment for debugging:)
297
## print 'new value:', new_value
298
## if paste: print 'paste'
299
## if set_to_none: print 'set_to_none'
300
## if set_to_zero: print 'set_to_zero'
301
## if set_to_minus_one: print 'set_to_minus_one'
302
## if internally_set: print 'internally_set'
304
## print 'new text:', new_text
305
## print 'disallowed'
310
wx.CallAfter(ctrl.SetValue, new_value)
313
# select to "empty" numeric value
314
wx.CallAfter(ctrl.SetValue, new_value)
315
wx.CallAfter(ctrl.SetInsertionPoint, 0)
316
wx.CallAfter(ctrl.SetSelection, 0, 1)
318
elif set_to_minus_one:
319
wx.CallAfter(ctrl.SetValue, new_value)
320
wx.CallAfter(ctrl.SetInsertionPoint, 1)
321
wx.CallAfter(ctrl.SetSelection, 1, 2)
323
elif not internally_set:
324
event.Skip() # allow base wxTextCtrl to finish processing
326
elif not wx.Validator_IsSilent():
330
def TransferToWindow(self):
331
""" Transfer data from validator to window.
333
The default implementation returns False, indicating that an error
334
occurred. We simply return True, as we don't do any data transfer.
336
return True # Prevent wx.Dialog from complaining.
339
def TransferFromWindow(self):
340
""" Transfer data from window to validator.
342
The default implementation returns False, indicating that an error
343
occurred. We simply return True, as we don't do any data transfer.
345
return True # Prevent wx.Dialog from complaining.
348
#----------------------------------------------------------------------------
350
class IntCtrl(wx.TextCtrl):
352
This class provides a control that takes and returns integers as
353
value, and provides bounds support and optional value limiting.
358
pos = wxDefaultPosition,
359
size = wxDefaultSize,
361
validator = wxDefaultValidator,
368
default_color = wxBLACK,
372
If no initial value is set, the default will be zero, or
373
the minimum value, if specified. If an illegal string is specified,
374
a ValueError will result. (You can always later set the initial
375
value with SetValue() after instantiation of the control.)
377
The minimum value that the control should allow. This can be
378
adjusted with SetMin(). If the control is not limited, any value
379
below this bound will be colored with the current out-of-bounds color.
380
If min < -sys.maxint-1 and the control is configured to not allow long
381
values, the minimum bound will still be set to the long value, but
382
the implicit bound will be -sys.maxint-1.
384
The maximum value that the control should allow. This can be
385
adjusted with SetMax(). If the control is not limited, any value
386
above this bound will be colored with the current out-of-bounds color.
387
if max > sys.maxint and the control is configured to not allow long
388
values, the maximum bound will still be set to the long value, but
389
the implicit bound will be sys.maxint.
392
Boolean indicating whether the control prevents values from
393
exceeding the currently set minimum and maximum values (bounds).
394
If False and bounds are set, out-of-bounds values will
395
be colored with the current out-of-bounds color.
398
Boolean indicating whether or not the control is allowed to be
399
empty, representing a value of None for the control.
402
Boolean indicating whether or not the control is allowed to hold
403
and return a long as well as an int.
406
Color value used for in-bounds values of the control.
409
Color value used for out-of-bounds values of the control
410
when the bounds are set but the control is not limited.
413
Normally None, IntCtrl uses its own validator to do value
414
validation and input control. However, a validator derived
415
from IntValidator can be supplied to override the data
416
transfer methods for the IntValidator class.
420
self, parent, id=-1, value = 0,
421
pos = wx.DefaultPosition, size = wx.DefaultSize,
422
style = 0, validator = wx.DefaultValidator,
425
limited = 0, allow_none = 0, allow_long = 0,
426
default_color = wx.BLACK, oob_color = wx.RED,
429
# Establish attrs required for any operation on value:
433
self.__default_color = wx.BLACK
434
self.__oob_color = wx.RED
435
self.__allow_none = 0
436
self.__allow_long = 0
437
self.__oldvalue = None
439
if validator == wx.DefaultValidator:
440
validator = IntValidator()
442
wx.TextCtrl.__init__(
443
self, parent, id, self._toGUI(0),
444
pos, size, style, validator, name )
446
# The following lets us set out our "integer update" events:
447
self.Bind(wx.EVT_TEXT, self.OnText )
449
# Establish parameters, with appropriate error checking
451
self.SetBounds(min, max)
452
self.SetLimited(limited)
453
self.SetColors(default_color, oob_color)
454
self.SetNoneAllowed(allow_none)
455
self.SetLongAllowed(allow_long)
460
def OnText( self, event ):
462
Handles an event indicating that the text control's value
463
has changed, and issue EVT_INT event.
464
NOTE: using wx.TextCtrl.SetValue() to change the control's
465
contents from within a wx.EVT_CHAR handler can cause double
466
text events. So we check for actual changes to the text
467
before passing the events on.
469
value = self.GetValue()
470
if value != self.__oldvalue:
472
self.GetEventHandler().ProcessEvent(
473
IntUpdatedEvent( self.GetId(), self.GetValue(), self ) )
476
# let normal processing of the text continue
478
self.__oldvalue = value # record for next event
483
Returns the current integer (long) value of the control.
485
return self._fromGUI( wx.TextCtrl.GetValue(self) )
487
def SetValue(self, value):
489
Sets the value of the control to the integer value specified.
490
The resulting actual value of the control may be altered to
491
conform with the bounds set on the control if limited,
492
or colored if not limited but the value is out-of-bounds.
493
A ValueError exception will be raised if an invalid value
496
wx.TextCtrl.SetValue( self, self._toGUI(value) )
500
def SetMin(self, min=None):
502
Sets the minimum value of the control. If a value of None
503
is provided, then the control will have no explicit minimum value.
504
If the value specified is greater than the current maximum value,
505
then the function returns 0 and the minimum will not change from
506
its current setting. On success, the function returns 1.
508
If successful and the current value is lower than the new lower
509
bound, if the control is limited, the value will be automatically
510
adjusted to the new minimum value; if not limited, the value in the
511
control will be colored with the current out-of-bounds color.
513
If min > -sys.maxint-1 and the control is configured to not allow longs,
514
the function will return 0, and the min will not be set.
516
if( self.__max is None
518
or (self.__max is not None and self.__max >= min) ):
521
if self.IsLimited() and min is not None and self.GetValue() < min:
532
Gets the minimum value of the control. It will return the current
533
minimum integer, or None if not specified.
538
def SetMax(self, max=None):
540
Sets the maximum value of the control. If a value of None
541
is provided, then the control will have no explicit maximum value.
542
If the value specified is less than the current minimum value, then
543
the function returns 0 and the maximum will not change from its
544
current setting. On success, the function returns 1.
546
If successful and the current value is greater than the new upper
547
bound, if the control is limited the value will be automatically
548
adjusted to this maximum value; if not limited, the value in the
549
control will be colored with the current out-of-bounds color.
551
If max > sys.maxint and the control is configured to not allow longs,
552
the function will return 0, and the max will not be set.
554
if( self.__min is None
556
or (self.__min is not None and self.__min <= max) ):
559
if self.IsLimited() and max is not None and self.GetValue() > max:
570
Gets the maximum value of the control. It will return the current
571
maximum integer, or None if not specified.
576
def SetBounds(self, min=None, max=None):
578
This function is a convenience function for setting the min and max
579
values at the same time. The function only applies the maximum bound
580
if setting the minimum bound is successful, and returns True
581
only if both operations succeed.
582
NOTE: leaving out an argument will remove the corresponding bound.
584
ret = self.SetMin(min)
585
return ret and self.SetMax(max)
590
This function returns a two-tuple (min,max), indicating the
591
current bounds of the control. Each value can be None if
592
that bound is not set.
594
return (self.__min, self.__max)
597
def SetLimited(self, limited):
599
If called with a value of True, this function will cause the control
600
to limit the value to fall within the bounds currently specified.
601
If the control's value currently exceeds the bounds, it will then
602
be limited accordingly.
604
If called with a value of 0, this function will disable value
605
limiting, but coloring of out-of-bounds values will still take
606
place if bounds have been set for the control.
608
self.__limited = limited
612
if not min is None and self.GetValue() < min:
614
elif not max is None and self.GetValue() > max:
622
Returns True if the control is currently limiting the
623
value to fall within the current bounds.
625
return self.__limited
628
def IsInBounds(self, value=None):
630
Returns True if no value is specified and the current value
631
of the control falls within the current bounds. This function can
632
also be called with a value to see if that value would fall within
633
the current bounds of the given control.
636
value = self.GetValue()
638
if( not (value is None and self.IsNoneAllowed())
639
and type(value) not in (types.IntType, types.LongType) ):
641
'IntCtrl requires integer values, passed %s'% repr(value) )
645
if min is None: min = value
646
if max is None: max = value
648
# if bounds set, and value is None, return False
649
if value == None and (min is not None or max is not None):
652
return min <= value <= max
655
def SetNoneAllowed(self, allow_none):
657
Change the behavior of the validation code, allowing control
658
to have a value of None or not, as appropriate. If the value
659
of the control is currently None, and allow_none is 0, the
660
value of the control will be set to the minimum value of the
661
control, or 0 if no lower bound is set.
663
self.__allow_none = allow_none
664
if not allow_none and self.GetValue() is None:
666
if min is not None: self.SetValue(min)
667
else: self.SetValue(0)
670
def IsNoneAllowed(self):
671
return self.__allow_none
674
def SetLongAllowed(self, allow_long):
676
Change the behavior of the validation code, allowing control
677
to have a long value or not, as appropriate. If the value
678
of the control is currently long, and allow_long is 0, the
679
value of the control will be adjusted to fall within the
680
size of an integer type, at either the sys.maxint or -sys.maxint-1,
681
for positive and negative values, respectively.
683
current_value = self.GetValue()
684
if not allow_long and type(current_value) is types.LongType:
685
if current_value > 0:
686
self.SetValue(MAXINT)
688
self.SetValue(MININT)
689
self.__allow_long = allow_long
692
def IsLongAllowed(self):
693
return self.__allow_long
697
def SetColors(self, default_color=wx.BLACK, oob_color=wx.RED):
699
Tells the control what colors to use for normal and out-of-bounds
700
values. If the value currently exceeds the bounds, it will be
701
recolored accordingly.
703
self.__default_color = default_color
704
self.__oob_color = oob_color
710
Returns a tuple of (default_color, oob_color), indicating
711
the current color settings for the control.
713
return self.__default_color, self.__oob_color
716
def _colorValue(self, value=None):
718
Colors text with oob_color if current value exceeds bounds
721
if not self.IsInBounds(value):
722
self.SetForegroundColour(self.__oob_color)
724
self.SetForegroundColour(self.__default_color)
728
def _toGUI( self, value ):
730
Conversion function used to set the value of the control; does
731
type and bounds checking and raises ValueError if argument is
734
if value is None and self.IsNoneAllowed():
736
elif type(value) == types.LongType and not self.IsLongAllowed():
738
'IntCtrl requires integer value, passed long' )
739
elif type(value) not in (types.IntType, types.LongType):
741
'IntCtrl requires integer value, passed %s'% repr(value) )
743
elif self.IsLimited():
746
if not min is None and value < min:
748
'value is below minimum value of control %d'% value )
749
if not max is None and value > max:
751
'value exceeds value of control %d'% value )
756
def _fromGUI( self, value ):
758
Conversion function used in getting the value of the control.
761
# One or more of the underlying text control implementations
762
# issue an intermediate EVT_TEXT when replacing the control's
763
# value, where the intermediate value is an empty string.
764
# So, to ensure consistency and to prevent spurious ValueErrors,
765
# we make the following test, and react accordingly:
768
if not self.IsNoneAllowed():
776
if self.IsLongAllowed():
784
Override the wxTextCtrl's .Cut function, with our own
785
that does validation. Will result in a value of 0
786
if entire contents of control are removed.
788
sel_start, sel_to = self.GetSelection()
789
select_len = sel_to - sel_start
790
textval = wx.TextCtrl.GetValue(self)
792
do = wx.TextDataObject()
793
do.SetText(textval[sel_start:sel_to])
794
wx.TheClipboard.Open()
795
wx.TheClipboard.SetData(do)
796
wx.TheClipboard.Close()
797
if select_len == len(wxTextCtrl.GetValue(self)):
798
if not self.IsNoneAllowed():
800
self.SetInsertionPoint(0)
801
self.SetSelection(0,1)
805
new_value = self._fromGUI(textval[:sel_start] + textval[sel_to:])
806
self.SetValue(new_value)
809
def _getClipboardContents( self ):
811
Subroutine for getting the current contents of the clipboard.
813
do = wx.TextDataObject()
814
wx.TheClipboard.Open()
815
success = wx.TheClipboard.GetData(do)
816
wx.TheClipboard.Close()
821
# Remove leading and trailing spaces before evaluating contents
822
return do.GetText().strip()
827
Override the wxTextCtrl's .Paste function, with our own
828
that does validation. Will raise ValueError if not a
829
valid integerizable value.
831
paste_text = self._getClipboardContents()
833
# (conversion will raise ValueError if paste isn't legal)
834
sel_start, sel_to = self.GetSelection()
835
text = wx.TextCtrl.GetValue( self )
836
new_text = text[:sel_start] + paste_text + text[sel_to:]
837
if new_text == '' and self.IsNoneAllowed():
840
value = self._fromGUI(new_text)
842
new_pos = sel_start + len(paste_text)
843
wx.CallAfter(self.SetInsertionPoint, new_pos)
847
#===========================================================================
849
if __name__ == '__main__':
853
class myDialog(wx.Dialog):
854
def __init__(self, parent, id, title,
855
pos = wx.DefaultPosition, size = wx.DefaultSize,
856
style = wx.DEFAULT_DIALOG_STYLE ):
857
wx.Dialog.__init__(self, parent, id, title, pos, size, style)
859
self.int_ctrl = IntCtrl(self, wx.NewId(), size=(55,20))
860
self.OK = wx.Button( self, wx.ID_OK, "OK")
861
self.Cancel = wx.Button( self, wx.ID_CANCEL, "Cancel")
863
vs = wx.BoxSizer( wx.VERTICAL )
864
vs.Add( self.int_ctrl, 0, wx.ALIGN_CENTRE|wx.ALL, 5 )
865
hs = wx.BoxSizer( wx.HORIZONTAL )
866
hs.Add( self.OK, 0, wx.ALIGN_CENTRE|wx.ALL, 5 )
867
hs.Add( self.Cancel, 0, wx.ALIGN_CENTRE|wx.ALL, 5 )
868
vs.Add(hs, 0, wx.ALIGN_CENTRE|wx.ALL, 5 )
870
self.SetAutoLayout( True )
873
vs.SetSizeHints( self )
874
self.Bind(EVT_INT, self.OnInt, self.int_ctrl)
876
def OnInt(self, event):
877
print 'int now', event.GetValue()
879
class TestApp(wx.App):
882
self.frame = wx.Frame(None, -1, "Test", (20,20), (120,100) )
883
self.panel = wx.Panel(self.frame, -1)
884
button = wx.Button(self.panel, 10, "Push Me", (20, 20))
885
self.Bind(wx.EVT_BUTTON, self.OnClick, button)
887
traceback.print_exc()
891
def OnClick(self, event):
892
dlg = myDialog(self.panel, -1, "test IntCtrl")
893
dlg.int_ctrl.SetValue(501)
894
dlg.int_ctrl.SetInsertionPoint(1)
895
dlg.int_ctrl.SetSelection(1,2)
897
print 'final value', dlg.int_ctrl.GetValue()
902
self.frame.Show(True)
909
traceback.print_exc()