Commit | Line | Data |
---|---|---|
d14a1e28 RD |
1 | #---------------------------------------------------------------------------- |
2 | # Name: wxPython.lib.intctrl.py | |
3 | # Author: Will Sadkin | |
4 | # Created: 01/16/2003 | |
5 | # Copyright: (c) 2003 by Will Sadkin | |
6 | # RCS-ID: $Id$ | |
7 | # License: wxWindows license | |
8 | #---------------------------------------------------------------------------- | |
9 | # NOTE: | |
10 | # This was written to provide a standard integer edit control for wxPython. | |
11 | # | |
d4b73b1b | 12 | # IntCtrl permits integer (long) values to be retrieved or set via |
d14a1e28 RD |
13 | # .GetValue() and .SetValue(), and provides an EVT_INT() event function |
14 | # for trapping changes to the control. | |
15 | # | |
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) | |
22 | # value of -1. | |
23 | # | |
d4b73b1b | 24 | # IntCtrl also supports range limits, with the option of either |
d14a1e28 RD |
25 | # enforcing them or simply coloring the text of the control if the limits |
26 | # are exceeded. | |
b881fc78 RD |
27 | #---------------------------------------------------------------------------- |
28 | # 12/08/2003 - Jeff Grimmett (grimmtooth@softhome.net) | |
29 | # | |
30 | # o 2.5 Compatability changes | |
31 | # | |
d4b73b1b RD |
32 | # 12/20/2003 - Jeff Grimmett (grimmtooth@softhome.net) |
33 | # | |
34 | # o wxIntUpdateEvent -> IntUpdateEvent | |
35 | # o wxIntValidator -> IntValidator | |
36 | # o wxIntCtrl -> IntCtrl | |
37 | # | |
b881fc78 RD |
38 | |
39 | import string | |
40 | import types | |
41 | ||
42 | import wx | |
43 | ||
44 | #---------------------------------------------------------------------------- | |
1fded56b | 45 | |
d14a1e28 RD |
46 | from sys import maxint |
47 | MAXINT = maxint # (constants should be in upper case) | |
48 | MININT = -maxint-1 | |
1fded56b | 49 | |
d14a1e28 RD |
50 | #---------------------------------------------------------------------------- |
51 | ||
b881fc78 RD |
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) | |
56 | ||
57 | #---------------------------------------------------------------------------- | |
d14a1e28 RD |
58 | |
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') | |
67 | ||
d4b73b1b | 68 | class IntUpdatedEvent(wx.PyCommandEvent): |
d14a1e28 | 69 | def __init__(self, id, value = 0, object=None): |
b881fc78 | 70 | wx.PyCommandEvent.__init__(self, wxEVT_COMMAND_INT_UPDATED, id) |
d14a1e28 RD |
71 | |
72 | self.__value = value | |
73 | self.SetEventObject(object) | |
74 | ||
75 | def GetValue(self): | |
76 | """Retrieve the value of the control at the time | |
77 | this event was generated.""" | |
78 | return self.__value | |
79 | ||
80 | ||
81 | #---------------------------------------------------------------------------- | |
82 | ||
d4b73b1b | 83 | class IntValidator( wx.PyValidator ): |
d14a1e28 | 84 | """ |
d4b73b1b | 85 | Validator class used with IntCtrl; handles all validation of input |
b881fc78 | 86 | prior to changing the value of the underlying wx.TextCtrl. |
d14a1e28 RD |
87 | """ |
88 | def __init__(self): | |
b881fc78 RD |
89 | wx.PyValidator.__init__(self) |
90 | self.Bind(wx.EVT_CHAR, self.OnChar) | |
d14a1e28 RD |
91 | |
92 | def Clone (self): | |
93 | return self.__class__() | |
94 | ||
95 | def Validate(self, window): # window here is the *parent* of the ctrl | |
96 | """ | |
97 | Because each operation on the control is vetted as it's made, | |
98 | the value of the control is always valid. | |
99 | """ | |
100 | return 1 | |
101 | ||
102 | ||
103 | def OnChar(self, event): | |
104 | """ | |
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. | |
111 | """ | |
112 | key = event.KeyCode() | |
113 | ctrl = event.GetEventObject() | |
114 | ||
115 | ||
116 | value = ctrl.GetValue() | |
b881fc78 | 117 | textval = wx.TextCtrl.GetValue(ctrl) |
d14a1e28 RD |
118 | allow_none = ctrl.IsNoneAllowed() |
119 | ||
120 | pos = ctrl.GetInsertionPoint() | |
121 | sel_start, sel_to = ctrl.GetSelection() | |
122 | select_len = sel_to - sel_start | |
123 | ||
124 | # (Uncomment for debugging:) | |
125 | ## print 'keycode:', key | |
126 | ## print 'pos:', pos | |
127 | ## print 'sel_start, sel_to:', sel_start, sel_to | |
128 | ## print 'select_len:', select_len | |
129 | ## print 'textval:', textval | |
130 | ||
131 | # set defaults for processing: | |
132 | allow_event = 1 | |
133 | set_to_none = 0 | |
134 | set_to_zero = 0 | |
135 | set_to_minus_one = 0 | |
136 | paste = 0 | |
137 | internally_set = 0 | |
138 | ||
139 | new_value = value | |
140 | new_text = textval | |
141 | new_pos = pos | |
142 | ||
143 | # Validate action, and predict resulting value, so we can | |
144 | # range check the result and validate that too. | |
145 | ||
b881fc78 | 146 | if key in (wx.WXK_DELETE, wx.WXK_BACK, WXK_CTRL_X): |
d14a1e28 RD |
147 | if select_len: |
148 | new_text = textval[:sel_start] + textval[sel_to:] | |
b881fc78 | 149 | elif key == wx.WXK_DELETE and pos < len(textval): |
d14a1e28 | 150 | new_text = textval[:pos] + textval[pos+1:] |
b881fc78 | 151 | elif key == wx.WXK_BACK and pos > 0: |
d14a1e28 RD |
152 | new_text = textval[:pos-1] + textval[pos:] |
153 | # (else value shouldn't change) | |
154 | ||
155 | if new_text in ('', '-'): | |
156 | # Deletion of last significant digit: | |
157 | if allow_none and new_text == '': | |
158 | new_value = None | |
159 | set_to_none = 1 | |
160 | else: | |
161 | new_value = 0 | |
162 | set_to_zero = 1 | |
163 | else: | |
164 | try: | |
165 | new_value = ctrl._fromGUI(new_text) | |
166 | except ValueError: | |
167 | allow_event = 0 | |
168 | ||
169 | ||
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: | |
175 | new_value = None | |
176 | set_to_none = 1 | |
177 | else: | |
178 | try: | |
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, | |
182 | # disallow event.) | |
183 | new_value = ctrl._fromGUI(new_text) | |
b881fc78 | 184 | |
d14a1e28 RD |
185 | if paste_text: |
186 | paste_value = ctrl._fromGUI(paste_text) | |
187 | else: | |
188 | paste_value = 0 | |
b881fc78 | 189 | |
d14a1e28 RD |
190 | new_pos = sel_start + len(str(paste_value)) |
191 | ||
192 | # if resulting value is 0, truncate and highlight value: | |
193 | if new_value == 0 and len(new_text) > 1: | |
194 | set_to_zero = 1 | |
195 | ||
196 | elif paste_value == 0: | |
197 | # Disallow pasting a leading zero with nothing selected: | |
198 | if( select_len == 0 | |
199 | and value is not None | |
200 | and ( (value >= 0 and pos == 0) | |
201 | or (value < 0 and pos in [0,1]) ) ): | |
202 | allow_event = 0 | |
b881fc78 | 203 | |
d14a1e28 RD |
204 | paste = 1 |
205 | ||
206 | except ValueError: | |
207 | allow_event = 0 | |
208 | ||
209 | ||
b881fc78 | 210 | elif key < wx.WXK_SPACE or key > 255: |
d14a1e28 RD |
211 | pass # event ok |
212 | ||
213 | ||
214 | elif chr(key) == '-': | |
215 | # Allow '-' to result in -1 if replacing entire contents: | |
216 | if( value is None | |
217 | or (value == 0 and pos == 0) | |
218 | or (select_len >= len(str(abs(value)))) ): | |
219 | new_value = -1 | |
220 | set_to_minus_one = 1 | |
221 | ||
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): | |
225 | allow_event = 0 | |
226 | else: | |
227 | new_text = '-' + textval | |
228 | new_pos = 1 | |
229 | try: | |
230 | new_value = ctrl._fromGUI(new_text) | |
231 | except ValueError: | |
232 | allow_event = 0 | |
233 | ||
234 | ||
235 | elif chr(key) in string.digits: | |
236 | # disallow inserting a leading zero with nothing selected | |
237 | if( chr(key) == '0' | |
238 | and select_len == 0 | |
239 | and value is not None | |
240 | and ( (value >= 0 and pos == 0) | |
241 | or (value < 0 and pos in [0,1]) ) ): | |
242 | allow_event = 0 | |
243 | # disallow inserting digits before the minus sign: | |
244 | elif value is not None and value < 0 and pos == 0: | |
245 | allow_event = 0 | |
246 | else: | |
247 | new_text = textval[:sel_start] + chr(key) + textval[sel_to:] | |
248 | try: | |
249 | new_value = ctrl._fromGUI(new_text) | |
250 | except ValueError: | |
251 | allow_event = 0 | |
252 | ||
253 | else: | |
254 | # not a legal char | |
255 | allow_event = 0 | |
256 | ||
257 | ||
258 | if allow_event: | |
259 | # Do range checking for new candidate value: | |
260 | if ctrl.IsLimited() and not ctrl.IsInBounds(new_value): | |
261 | allow_event = 0 | |
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 ) ): | |
268 | ||
269 | # Allow replacement of leading chars with | |
270 | # zero, but remove the leading zero, effectively | |
271 | # making this like "remove leading digits" | |
272 | ||
273 | # Account for leading zero when positioning cursor: | |
b881fc78 | 274 | if( key == wx.WXK_BACK |
d14a1e28 RD |
275 | or (paste and paste_value == 0 and new_pos > 0) ): |
276 | new_pos = new_pos - 1 | |
277 | ||
b881fc78 RD |
278 | wx.CallAfter(ctrl.SetValue, new_value) |
279 | wx.CallAfter(ctrl.SetInsertionPoint, new_pos) | |
d14a1e28 RD |
280 | internally_set = 1 |
281 | ||
282 | elif paste: | |
283 | # Always do paste numerically, to remove | |
284 | # leading/trailing spaces | |
b881fc78 RD |
285 | wx.CallAfter(ctrl.SetValue, new_value) |
286 | wx.CallAfter(ctrl.SetInsertionPoint, new_pos) | |
d14a1e28 RD |
287 | internally_set = 1 |
288 | ||
289 | elif (new_value == 0 and len(new_text) > 1 ): | |
290 | allow_event = 0 | |
291 | ||
292 | if allow_event: | |
293 | ctrl._colorValue(new_value) # (one way or t'other) | |
294 | ||
295 | # (Uncomment for debugging:) | |
296 | ## if allow_event: | |
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' | |
303 | ## else: | |
304 | ## print 'new text:', new_text | |
305 | ## print 'disallowed' | |
306 | ||
307 | ||
308 | if allow_event: | |
309 | if set_to_none: | |
b881fc78 | 310 | wx.CallAfter(ctrl.SetValue, new_value) |
d14a1e28 RD |
311 | |
312 | elif set_to_zero: | |
313 | # select to "empty" numeric value | |
b881fc78 RD |
314 | wx.CallAfter(ctrl.SetValue, new_value) |
315 | wx.CallAfter(ctrl.SetInsertionPoint, 0) | |
316 | wx.CallAfter(ctrl.SetSelection, 0, 1) | |
d14a1e28 RD |
317 | |
318 | elif set_to_minus_one: | |
b881fc78 RD |
319 | wx.CallAfter(ctrl.SetValue, new_value) |
320 | wx.CallAfter(ctrl.SetInsertionPoint, 1) | |
321 | wx.CallAfter(ctrl.SetSelection, 1, 2) | |
d14a1e28 RD |
322 | |
323 | elif not internally_set: | |
324 | event.Skip() # allow base wxTextCtrl to finish processing | |
325 | ||
b881fc78 RD |
326 | elif not wx.Validator_IsSilent(): |
327 | wx.Bell() | |
d14a1e28 RD |
328 | |
329 | ||
330 | def TransferToWindow(self): | |
331 | """ Transfer data from validator to window. | |
332 | ||
333 | The default implementation returns False, indicating that an error | |
334 | occurred. We simply return True, as we don't do any data transfer. | |
335 | """ | |
b881fc78 | 336 | return True # Prevent wx.Dialog from complaining. |
d14a1e28 RD |
337 | |
338 | ||
339 | def TransferFromWindow(self): | |
340 | """ Transfer data from window to validator. | |
341 | ||
342 | The default implementation returns False, indicating that an error | |
343 | occurred. We simply return True, as we don't do any data transfer. | |
344 | """ | |
b881fc78 | 345 | return True # Prevent wx.Dialog from complaining. |
d14a1e28 RD |
346 | |
347 | ||
348 | #---------------------------------------------------------------------------- | |
349 | ||
d4b73b1b | 350 | class IntCtrl(wx.TextCtrl): |
d14a1e28 RD |
351 | """ |
352 | This class provides a control that takes and returns integers as | |
353 | value, and provides bounds support and optional value limiting. | |
354 | ||
d4b73b1b | 355 | IntCtrl( |
d14a1e28 RD |
356 | parent, id = -1, |
357 | value = 0, | |
358 | pos = wxDefaultPosition, | |
359 | size = wxDefaultSize, | |
360 | style = 0, | |
361 | validator = wxDefaultValidator, | |
362 | name = "integer", | |
363 | min = None, | |
364 | max = None, | |
365 | limited = False, | |
366 | allow_none = False, | |
367 | allow_long = False, | |
368 | default_color = wxBLACK, | |
369 | oob_color = wxRED ) | |
370 | ||
371 | value | |
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.) | |
376 | min | |
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. | |
383 | max | |
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. | |
390 | ||
391 | limited | |
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. | |
396 | ||
397 | allow_none | |
398 | Boolean indicating whether or not the control is allowed to be | |
399 | empty, representing a value of None for the control. | |
400 | ||
401 | allow_long | |
402 | Boolean indicating whether or not the control is allowed to hold | |
403 | and return a long as well as an int. | |
404 | ||
405 | default_color | |
406 | Color value used for in-bounds values of the control. | |
407 | ||
408 | oob_color | |
409 | Color value used for out-of-bounds values of the control | |
410 | when the bounds are set but the control is not limited. | |
411 | ||
412 | validator | |
d4b73b1b | 413 | Normally None, IntCtrl uses its own validator to do value |
d14a1e28 | 414 | validation and input control. However, a validator derived |
d4b73b1b RD |
415 | from IntValidator can be supplied to override the data |
416 | transfer methods for the IntValidator class. | |
d14a1e28 RD |
417 | """ |
418 | ||
419 | def __init__ ( | |
420 | self, parent, id=-1, value = 0, | |
b881fc78 RD |
421 | pos = wx.DefaultPosition, size = wx.DefaultSize, |
422 | style = 0, validator = wx.DefaultValidator, | |
d14a1e28 RD |
423 | name = "integer", |
424 | min=None, max=None, | |
425 | limited = 0, allow_none = 0, allow_long = 0, | |
b881fc78 | 426 | default_color = wx.BLACK, oob_color = wx.RED, |
d14a1e28 RD |
427 | ): |
428 | ||
429 | # Establish attrs required for any operation on value: | |
430 | self.__min = None | |
431 | self.__max = None | |
432 | self.__limited = 0 | |
b881fc78 RD |
433 | self.__default_color = wx.BLACK |
434 | self.__oob_color = wx.RED | |
d14a1e28 RD |
435 | self.__allow_none = 0 |
436 | self.__allow_long = 0 | |
437 | self.__oldvalue = None | |
438 | ||
b881fc78 | 439 | if validator == wx.DefaultValidator: |
d4b73b1b | 440 | validator = IntValidator() |
d14a1e28 | 441 | |
b881fc78 | 442 | wx.TextCtrl.__init__( |
d14a1e28 RD |
443 | self, parent, id, self._toGUI(0), |
444 | pos, size, style, validator, name ) | |
445 | ||
446 | # The following lets us set out our "integer update" events: | |
b881fc78 | 447 | self.Bind(wx.EVT_TEXT, self.OnText ) |
d14a1e28 RD |
448 | |
449 | # Establish parameters, with appropriate error checking | |
450 | ||
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) | |
456 | self.SetValue(value) | |
457 | self.__oldvalue = 0 | |
458 | ||
459 | ||
460 | def OnText( self, event ): | |
461 | """ | |
462 | Handles an event indicating that the text control's value | |
463 | has changed, and issue EVT_INT event. | |
b881fc78 RD |
464 | NOTE: using wx.TextCtrl.SetValue() to change the control's |
465 | contents from within a wx.EVT_CHAR handler can cause double | |
d14a1e28 RD |
466 | text events. So we check for actual changes to the text |
467 | before passing the events on. | |
468 | """ | |
469 | value = self.GetValue() | |
470 | if value != self.__oldvalue: | |
471 | try: | |
472 | self.GetEventHandler().ProcessEvent( | |
d4b73b1b | 473 | IntUpdatedEvent( self.GetId(), self.GetValue(), self ) ) |
d14a1e28 RD |
474 | except ValueError: |
475 | return | |
476 | # let normal processing of the text continue | |
477 | event.Skip() | |
478 | self.__oldvalue = value # record for next event | |
479 | ||
480 | ||
481 | def GetValue(self): | |
482 | """ | |
483 | Returns the current integer (long) value of the control. | |
484 | """ | |
b881fc78 | 485 | return self._fromGUI( wx.TextCtrl.GetValue(self) ) |
d14a1e28 RD |
486 | |
487 | def SetValue(self, value): | |
488 | """ | |
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 | |
494 | is specified. | |
495 | """ | |
b881fc78 | 496 | wx.TextCtrl.SetValue( self, self._toGUI(value) ) |
d14a1e28 RD |
497 | self._colorValue() |
498 | ||
499 | ||
500 | def SetMin(self, min=None): | |
501 | """ | |
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. | |
507 | ||
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. | |
512 | ||
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. | |
515 | """ | |
516 | if( self.__max is None | |
517 | or min is None | |
518 | or (self.__max is not None and self.__max >= min) ): | |
519 | self.__min = min | |
520 | ||
521 | if self.IsLimited() and min is not None and self.GetValue() < min: | |
522 | self.SetValue(min) | |
523 | else: | |
524 | self._colorValue() | |
525 | return 1 | |
526 | else: | |
527 | return 0 | |
528 | ||
529 | ||
530 | def GetMin(self): | |
531 | """ | |
532 | Gets the minimum value of the control. It will return the current | |
533 | minimum integer, or None if not specified. | |
534 | """ | |
535 | return self.__min | |
536 | ||
537 | ||
538 | def SetMax(self, max=None): | |
539 | """ | |
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. | |
545 | ||
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. | |
550 | ||
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. | |
553 | """ | |
554 | if( self.__min is None | |
555 | or max is None | |
556 | or (self.__min is not None and self.__min <= max) ): | |
557 | self.__max = max | |
558 | ||
559 | if self.IsLimited() and max is not None and self.GetValue() > max: | |
560 | self.SetValue(max) | |
561 | else: | |
562 | self._colorValue() | |
563 | return 1 | |
564 | else: | |
565 | return 0 | |
566 | ||
567 | ||
568 | def GetMax(self): | |
569 | """ | |
570 | Gets the maximum value of the control. It will return the current | |
571 | maximum integer, or None if not specified. | |
572 | """ | |
573 | return self.__max | |
574 | ||
575 | ||
576 | def SetBounds(self, min=None, max=None): | |
577 | """ | |
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. | |
583 | """ | |
584 | ret = self.SetMin(min) | |
585 | return ret and self.SetMax(max) | |
586 | ||
587 | ||
588 | def GetBounds(self): | |
589 | """ | |
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. | |
593 | """ | |
594 | return (self.__min, self.__max) | |
595 | ||
596 | ||
597 | def SetLimited(self, limited): | |
598 | """ | |
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. | |
603 | ||
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. | |
607 | """ | |
608 | self.__limited = limited | |
609 | if limited: | |
610 | min = self.GetMin() | |
611 | max = self.GetMax() | |
612 | if not min is None and self.GetValue() < min: | |
613 | self.SetValue(min) | |
614 | elif not max is None and self.GetValue() > max: | |
615 | self.SetValue(max) | |
616 | else: | |
617 | self._colorValue() | |
618 | ||
619 | ||
620 | def IsLimited(self): | |
621 | """ | |
622 | Returns True if the control is currently limiting the | |
623 | value to fall within the current bounds. | |
624 | """ | |
625 | return self.__limited | |
626 | ||
627 | ||
628 | def IsInBounds(self, value=None): | |
629 | """ | |
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. | |
634 | """ | |
635 | if value is None: | |
636 | value = self.GetValue() | |
637 | ||
638 | if( not (value is None and self.IsNoneAllowed()) | |
639 | and type(value) not in (types.IntType, types.LongType) ): | |
640 | raise ValueError ( | |
d4b73b1b | 641 | 'IntCtrl requires integer values, passed %s'% repr(value) ) |
d14a1e28 RD |
642 | |
643 | min = self.GetMin() | |
644 | max = self.GetMax() | |
645 | if min is None: min = value | |
646 | if max is None: max = value | |
647 | ||
648 | # if bounds set, and value is None, return False | |
649 | if value == None and (min is not None or max is not None): | |
650 | return 0 | |
651 | else: | |
652 | return min <= value <= max | |
653 | ||
654 | ||
655 | def SetNoneAllowed(self, allow_none): | |
656 | """ | |
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. | |
662 | """ | |
663 | self.__allow_none = allow_none | |
664 | if not allow_none and self.GetValue() is None: | |
665 | min = self.GetMin() | |
666 | if min is not None: self.SetValue(min) | |
667 | else: self.SetValue(0) | |
668 | ||
669 | ||
670 | def IsNoneAllowed(self): | |
671 | return self.__allow_none | |
672 | ||
673 | ||
674 | def SetLongAllowed(self, allow_long): | |
675 | """ | |
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. | |
682 | """ | |
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) | |
687 | else: | |
688 | self.SetValue(MININT) | |
689 | self.__allow_long = allow_long | |
690 | ||
691 | ||
692 | def IsLongAllowed(self): | |
693 | return self.__allow_long | |
694 | ||
695 | ||
696 | ||
b881fc78 | 697 | def SetColors(self, default_color=wx.BLACK, oob_color=wx.RED): |
d14a1e28 RD |
698 | """ |
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. | |
702 | """ | |
703 | self.__default_color = default_color | |
704 | self.__oob_color = oob_color | |
705 | self._colorValue() | |
706 | ||
707 | ||
708 | def GetColors(self): | |
709 | """ | |
710 | Returns a tuple of (default_color, oob_color), indicating | |
711 | the current color settings for the control. | |
712 | """ | |
713 | return self.__default_color, self.__oob_color | |
714 | ||
715 | ||
716 | def _colorValue(self, value=None): | |
717 | """ | |
718 | Colors text with oob_color if current value exceeds bounds | |
719 | set for control. | |
720 | """ | |
721 | if not self.IsInBounds(value): | |
722 | self.SetForegroundColour(self.__oob_color) | |
723 | else: | |
724 | self.SetForegroundColour(self.__default_color) | |
725 | self.Refresh() | |
726 | ||
727 | ||
728 | def _toGUI( self, value ): | |
729 | """ | |
730 | Conversion function used to set the value of the control; does | |
731 | type and bounds checking and raises ValueError if argument is | |
732 | not a valid value. | |
733 | """ | |
734 | if value is None and self.IsNoneAllowed(): | |
735 | return '' | |
736 | elif type(value) == types.LongType and not self.IsLongAllowed(): | |
737 | raise ValueError ( | |
d4b73b1b | 738 | 'IntCtrl requires integer value, passed long' ) |
d14a1e28 RD |
739 | elif type(value) not in (types.IntType, types.LongType): |
740 | raise ValueError ( | |
d4b73b1b | 741 | 'IntCtrl requires integer value, passed %s'% repr(value) ) |
d14a1e28 RD |
742 | |
743 | elif self.IsLimited(): | |
744 | min = self.GetMin() | |
745 | max = self.GetMax() | |
746 | if not min is None and value < min: | |
747 | raise ValueError ( | |
748 | 'value is below minimum value of control %d'% value ) | |
749 | if not max is None and value > max: | |
750 | raise ValueError ( | |
751 | 'value exceeds value of control %d'% value ) | |
752 | ||
753 | return str(value) | |
754 | ||
755 | ||
756 | def _fromGUI( self, value ): | |
757 | """ | |
758 | Conversion function used in getting the value of the control. | |
759 | """ | |
760 | ||
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: | |
766 | # | |
767 | if value == '': | |
768 | if not self.IsNoneAllowed(): | |
769 | return 0 | |
770 | else: | |
771 | return None | |
772 | else: | |
773 | try: | |
774 | return int( value ) | |
775 | except ValueError: | |
776 | if self.IsLongAllowed(): | |
777 | return long( value ) | |
778 | else: | |
779 | raise | |
780 | ||
781 | ||
782 | def Cut( self ): | |
783 | """ | |
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. | |
787 | """ | |
788 | sel_start, sel_to = self.GetSelection() | |
789 | select_len = sel_to - sel_start | |
b881fc78 | 790 | textval = wx.TextCtrl.GetValue(self) |
d14a1e28 | 791 | |
b881fc78 | 792 | do = wx.TextDataObject() |
d14a1e28 | 793 | do.SetText(textval[sel_start:sel_to]) |
b881fc78 RD |
794 | wx.TheClipboard.Open() |
795 | wx.TheClipboard.SetData(do) | |
796 | wx.TheClipboard.Close() | |
d14a1e28 RD |
797 | if select_len == len(wxTextCtrl.GetValue(self)): |
798 | if not self.IsNoneAllowed(): | |
799 | self.SetValue(0) | |
800 | self.SetInsertionPoint(0) | |
801 | self.SetSelection(0,1) | |
802 | else: | |
803 | self.SetValue(None) | |
804 | else: | |
805 | new_value = self._fromGUI(textval[:sel_start] + textval[sel_to:]) | |
806 | self.SetValue(new_value) | |
807 | ||
808 | ||
809 | def _getClipboardContents( self ): | |
810 | """ | |
811 | Subroutine for getting the current contents of the clipboard. | |
812 | """ | |
b881fc78 RD |
813 | do = wx.TextDataObject() |
814 | wx.TheClipboard.Open() | |
815 | success = wx.TheClipboard.GetData(do) | |
816 | wx.TheClipboard.Close() | |
d14a1e28 RD |
817 | |
818 | if not success: | |
819 | return None | |
820 | else: | |
821 | # Remove leading and trailing spaces before evaluating contents | |
822 | return do.GetText().strip() | |
823 | ||
824 | ||
825 | def Paste( self ): | |
826 | """ | |
827 | Override the wxTextCtrl's .Paste function, with our own | |
828 | that does validation. Will raise ValueError if not a | |
829 | valid integerizable value. | |
830 | """ | |
831 | paste_text = self._getClipboardContents() | |
832 | if paste_text: | |
833 | # (conversion will raise ValueError if paste isn't legal) | |
834 | sel_start, sel_to = self.GetSelection() | |
b881fc78 | 835 | text = wx.TextCtrl.GetValue( self ) |
d14a1e28 RD |
836 | new_text = text[:sel_start] + paste_text + text[sel_to:] |
837 | if new_text == '' and self.IsNoneAllowed(): | |
838 | self.SetValue(None) | |
839 | else: | |
840 | value = self._fromGUI(new_text) | |
841 | self.SetValue(value) | |
842 | new_pos = sel_start + len(paste_text) | |
b881fc78 | 843 | wx.CallAfter(self.SetInsertionPoint, new_pos) |
d14a1e28 RD |
844 | |
845 | ||
846 | ||
847 | #=========================================================================== | |
848 | ||
849 | if __name__ == '__main__': | |
850 | ||
851 | import traceback | |
852 | ||
b881fc78 | 853 | class myDialog(wx.Dialog): |
d14a1e28 | 854 | def __init__(self, parent, id, title, |
b881fc78 RD |
855 | pos = wx.DefaultPosition, size = wx.DefaultSize, |
856 | style = wx.DEFAULT_DIALOG_STYLE ): | |
857 | wx.Dialog.__init__(self, parent, id, title, pos, size, style) | |
d14a1e28 | 858 | |
d4b73b1b | 859 | self.int_ctrl = IntCtrl(self, wx.NewId(), size=(55,20)) |
b881fc78 RD |
860 | self.OK = wx.Button( self, wx.ID_OK, "OK") |
861 | self.Cancel = wx.Button( self, wx.ID_CANCEL, "Cancel") | |
d14a1e28 | 862 | |
b881fc78 RD |
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 ) | |
d14a1e28 RD |
869 | |
870 | self.SetAutoLayout( True ) | |
871 | self.SetSizer( vs ) | |
872 | vs.Fit( self ) | |
873 | vs.SetSizeHints( self ) | |
b881fc78 | 874 | self.Bind(EVT_INT, self.OnInt, self.int_ctrl) |
d14a1e28 RD |
875 | |
876 | def OnInt(self, event): | |
877 | print 'int now', event.GetValue() | |
878 | ||
b881fc78 | 879 | class TestApp(wx.App): |
d14a1e28 RD |
880 | def OnInit(self): |
881 | try: | |
b881fc78 RD |
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) | |
d14a1e28 RD |
886 | except: |
887 | traceback.print_exc() | |
888 | return False | |
889 | return True | |
890 | ||
891 | def OnClick(self, event): | |
d4b73b1b | 892 | dlg = myDialog(self.panel, -1, "test IntCtrl") |
d14a1e28 RD |
893 | dlg.int_ctrl.SetValue(501) |
894 | dlg.int_ctrl.SetInsertionPoint(1) | |
895 | dlg.int_ctrl.SetSelection(1,2) | |
896 | rc = dlg.ShowModal() | |
897 | print 'final value', dlg.int_ctrl.GetValue() | |
898 | del dlg | |
899 | self.frame.Destroy() | |
900 | ||
901 | def Show(self): | |
902 | self.frame.Show(True) | |
903 | ||
904 | try: | |
905 | app = TestApp(0) | |
906 | app.Show() | |
907 | app.MainLoop() | |
908 | except: | |
909 | traceback.print_exc() |