| 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 | # |
| 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. |
| 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 | # |
| 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 |
| 26 | # are exceeded. |
| 27 | #---------------------------------------------------------------------------- |
| 28 | # 12/08/2003 - Jeff Grimmett (grimmtooth@softhome.net) |
| 29 | # |
| 30 | # o 2.5 Compatability changes |
| 31 | # |
| 32 | # 12/20/2003 - Jeff Grimmett (grimmtooth@softhome.net) |
| 33 | # |
| 34 | # o wxIntUpdateEvent -> IntUpdateEvent |
| 35 | # o wxIntValidator -> IntValidator |
| 36 | # o wxIntCtrl -> IntCtrl |
| 37 | # |
| 38 | |
| 39 | import string |
| 40 | import types |
| 41 | |
| 42 | import wx |
| 43 | |
| 44 | #---------------------------------------------------------------------------- |
| 45 | |
| 46 | from sys import maxint |
| 47 | MAXINT = maxint # (constants should be in upper case) |
| 48 | MININT = -maxint-1 |
| 49 | |
| 50 | #---------------------------------------------------------------------------- |
| 51 | |
| 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 | #---------------------------------------------------------------------------- |
| 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 | |
| 68 | class IntUpdatedEvent(wx.PyCommandEvent): |
| 69 | def __init__(self, id, value = 0, object=None): |
| 70 | wx.PyCommandEvent.__init__(self, wxEVT_COMMAND_INT_UPDATED, id) |
| 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 | |
| 83 | class IntValidator( wx.PyValidator ): |
| 84 | """ |
| 85 | Validator class used with IntCtrl; handles all validation of input |
| 86 | prior to changing the value of the underlying wx.TextCtrl. |
| 87 | """ |
| 88 | def __init__(self): |
| 89 | wx.PyValidator.__init__(self) |
| 90 | self.Bind(wx.EVT_CHAR, self.OnChar) |
| 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() |
| 117 | textval = wx.TextCtrl.GetValue(ctrl) |
| 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 | |
| 146 | if key in (wx.WXK_DELETE, wx.WXK_BACK, WXK_CTRL_X): |
| 147 | if select_len: |
| 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) |
| 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) |
| 184 | |
| 185 | if paste_text: |
| 186 | paste_value = ctrl._fromGUI(paste_text) |
| 187 | else: |
| 188 | paste_value = 0 |
| 189 | |
| 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 |
| 203 | |
| 204 | paste = 1 |
| 205 | |
| 206 | except ValueError: |
| 207 | allow_event = 0 |
| 208 | |
| 209 | |
| 210 | elif key < wx.WXK_SPACE or key > 255: |
| 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: |
| 274 | if( key == wx.WXK_BACK |
| 275 | or (paste and paste_value == 0 and new_pos > 0) ): |
| 276 | new_pos = new_pos - 1 |
| 277 | |
| 278 | wx.CallAfter(ctrl.SetValue, new_value) |
| 279 | wx.CallAfter(ctrl.SetInsertionPoint, new_pos) |
| 280 | internally_set = 1 |
| 281 | |
| 282 | elif paste: |
| 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) |
| 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 | ## print |
| 307 | |
| 308 | if allow_event: |
| 309 | if set_to_none: |
| 310 | wx.CallAfter(ctrl.SetValue, new_value) |
| 311 | |
| 312 | elif set_to_zero: |
| 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) |
| 317 | |
| 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) |
| 322 | |
| 323 | elif not internally_set: |
| 324 | event.Skip() # allow base wxTextCtrl to finish processing |
| 325 | |
| 326 | elif not wx.Validator_IsSilent(): |
| 327 | wx.Bell() |
| 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 | """ |
| 336 | return True # Prevent wx.Dialog from complaining. |
| 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 | """ |
| 345 | return True # Prevent wx.Dialog from complaining. |
| 346 | |
| 347 | |
| 348 | #---------------------------------------------------------------------------- |
| 349 | |
| 350 | class IntCtrl(wx.TextCtrl): |
| 351 | """ |
| 352 | This class provides a control that takes and returns integers as |
| 353 | value, and provides bounds support and optional value limiting. |
| 354 | |
| 355 | IntCtrl( |
| 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 |
| 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. |
| 417 | """ |
| 418 | |
| 419 | def __init__ ( |
| 420 | self, parent, id=-1, value = 0, |
| 421 | pos = wx.DefaultPosition, size = wx.DefaultSize, |
| 422 | style = 0, validator = wx.DefaultValidator, |
| 423 | name = "integer", |
| 424 | min=None, max=None, |
| 425 | limited = 0, allow_none = 0, allow_long = 0, |
| 426 | default_color = wx.BLACK, oob_color = wx.RED, |
| 427 | ): |
| 428 | |
| 429 | # Establish attrs required for any operation on value: |
| 430 | self.__min = None |
| 431 | self.__max = None |
| 432 | self.__limited = 0 |
| 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 |
| 438 | |
| 439 | if validator == wx.DefaultValidator: |
| 440 | validator = IntValidator() |
| 441 | |
| 442 | wx.TextCtrl.__init__( |
| 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: |
| 447 | self.Bind(wx.EVT_TEXT, self.OnText ) |
| 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. |
| 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. |
| 468 | """ |
| 469 | value = self.GetValue() |
| 470 | if value != self.__oldvalue: |
| 471 | try: |
| 472 | self.GetEventHandler().ProcessEvent( |
| 473 | IntUpdatedEvent( self.GetId(), self.GetValue(), self ) ) |
| 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 | """ |
| 485 | return self._fromGUI( wx.TextCtrl.GetValue(self) ) |
| 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 | """ |
| 496 | wx.TextCtrl.SetValue( self, self._toGUI(value) ) |
| 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 ( |
| 641 | 'IntCtrl requires integer values, passed %s'% repr(value) ) |
| 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 | |
| 697 | def SetColors(self, default_color=wx.BLACK, oob_color=wx.RED): |
| 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 ( |
| 738 | 'IntCtrl requires integer value, passed long' ) |
| 739 | elif type(value) not in (types.IntType, types.LongType): |
| 740 | raise ValueError ( |
| 741 | 'IntCtrl requires integer value, passed %s'% repr(value) ) |
| 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 |
| 790 | textval = wx.TextCtrl.GetValue(self) |
| 791 | |
| 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(): |
| 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 | """ |
| 813 | do = wx.TextDataObject() |
| 814 | wx.TheClipboard.Open() |
| 815 | success = wx.TheClipboard.GetData(do) |
| 816 | wx.TheClipboard.Close() |
| 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() |
| 835 | text = wx.TextCtrl.GetValue( self ) |
| 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) |
| 843 | wx.CallAfter(self.SetInsertionPoint, new_pos) |
| 844 | |
| 845 | |
| 846 | |
| 847 | #=========================================================================== |
| 848 | |
| 849 | if __name__ == '__main__': |
| 850 | |
| 851 | import traceback |
| 852 | |
| 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) |
| 858 | |
| 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") |
| 862 | |
| 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 ) |
| 869 | |
| 870 | self.SetAutoLayout( True ) |
| 871 | self.SetSizer( vs ) |
| 872 | vs.Fit( self ) |
| 873 | vs.SetSizeHints( self ) |
| 874 | self.Bind(EVT_INT, self.OnInt, self.int_ctrl) |
| 875 | |
| 876 | def OnInt(self, event): |
| 877 | print 'int now', event.GetValue() |
| 878 | |
| 879 | class TestApp(wx.App): |
| 880 | def OnInit(self): |
| 881 | try: |
| 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) |
| 886 | except: |
| 887 | traceback.print_exc() |
| 888 | return False |
| 889 | return True |
| 890 | |
| 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) |
| 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() |