| 1 | # Based on iwidgets2.2.0/entryfield.itk code. |
| 2 | |
| 3 | import re |
| 4 | import string |
| 5 | import types |
| 6 | import Tkinter |
| 7 | import Pmw |
| 8 | |
| 9 | # Possible return values of validation functions. |
| 10 | OK = 1 |
| 11 | ERROR = 0 |
| 12 | PARTIAL = -1 |
| 13 | |
| 14 | class EntryField(Pmw.MegaWidget): |
| 15 | _classBindingsDefinedFor = 0 |
| 16 | |
| 17 | def __init__(self, parent = None, **kw): |
| 18 | |
| 19 | # Define the megawidget options. |
| 20 | INITOPT = Pmw.INITOPT |
| 21 | optiondefs = ( |
| 22 | ('command', None, None), |
| 23 | ('errorbackground', 'pink', None), |
| 24 | ('invalidcommand', self.bell, None), |
| 25 | ('labelmargin', 0, INITOPT), |
| 26 | ('labelpos', None, INITOPT), |
| 27 | ('modifiedcommand', None, None), |
| 28 | ('sticky', 'ew', INITOPT), |
| 29 | ('validate', None, self._validate), |
| 30 | ('extravalidators', {}, None), |
| 31 | ('value', '', INITOPT), |
| 32 | ) |
| 33 | self.defineoptions(kw, optiondefs) |
| 34 | |
| 35 | # Initialise the base class (after defining the options). |
| 36 | Pmw.MegaWidget.__init__(self, parent) |
| 37 | |
| 38 | # Create the components. |
| 39 | interior = self.interior() |
| 40 | self._entryFieldEntry = self.createcomponent('entry', |
| 41 | (), None, |
| 42 | Tkinter.Entry, (interior,)) |
| 43 | self._entryFieldEntry.grid(column=2, row=2, sticky=self['sticky']) |
| 44 | if self['value'] != '': |
| 45 | self.__setEntry(self['value']) |
| 46 | interior.grid_columnconfigure(2, weight=1) |
| 47 | interior.grid_rowconfigure(2, weight=1) |
| 48 | |
| 49 | self.createlabel(interior) |
| 50 | |
| 51 | # Initialise instance variables. |
| 52 | |
| 53 | self.normalBackground = None |
| 54 | self._previousText = None |
| 55 | |
| 56 | # Initialise instance. |
| 57 | |
| 58 | _registerEntryField(self._entryFieldEntry, self) |
| 59 | |
| 60 | # Establish the special class bindings if not already done. |
| 61 | # Also create bindings if the Tkinter default interpreter has |
| 62 | # changed. Use Tkinter._default_root to create class |
| 63 | # bindings, so that a reference to root is created by |
| 64 | # bind_class rather than a reference to self, which would |
| 65 | # prevent object cleanup. |
| 66 | if EntryField._classBindingsDefinedFor != Tkinter._default_root: |
| 67 | tagList = self._entryFieldEntry.bindtags() |
| 68 | root = Tkinter._default_root |
| 69 | |
| 70 | allSequences = {} |
| 71 | for tag in tagList: |
| 72 | |
| 73 | sequences = root.bind_class(tag) |
| 74 | if type(sequences) is types.StringType: |
| 75 | # In old versions of Tkinter, bind_class returns a string |
| 76 | sequences = root.tk.splitlist(sequences) |
| 77 | |
| 78 | for sequence in sequences: |
| 79 | allSequences[sequence] = None |
| 80 | for sequence in allSequences.keys(): |
| 81 | root.bind_class('EntryFieldPre', sequence, _preProcess) |
| 82 | root.bind_class('EntryFieldPost', sequence, _postProcess) |
| 83 | |
| 84 | EntryField._classBindingsDefinedFor = root |
| 85 | |
| 86 | self._entryFieldEntry.bindtags(('EntryFieldPre',) + |
| 87 | self._entryFieldEntry.bindtags() + ('EntryFieldPost',)) |
| 88 | self._entryFieldEntry.bind('<Return>', self._executeCommand) |
| 89 | |
| 90 | # Check keywords and initialise options. |
| 91 | self.initialiseoptions() |
| 92 | |
| 93 | def destroy(self): |
| 94 | _deregisterEntryField(self._entryFieldEntry) |
| 95 | Pmw.MegaWidget.destroy(self) |
| 96 | |
| 97 | def _getValidatorFunc(self, validator, index): |
| 98 | # Search the extra and standard validator lists for the |
| 99 | # given 'validator'. If 'validator' is an alias, then |
| 100 | # continue the search using the alias. Make sure that |
| 101 | # self-referencial aliases do not cause infinite loops. |
| 102 | |
| 103 | extraValidators = self['extravalidators'] |
| 104 | traversedValidators = [] |
| 105 | |
| 106 | while 1: |
| 107 | traversedValidators.append(validator) |
| 108 | if extraValidators.has_key(validator): |
| 109 | validator = extraValidators[validator][index] |
| 110 | elif _standardValidators.has_key(validator): |
| 111 | validator = _standardValidators[validator][index] |
| 112 | else: |
| 113 | return validator |
| 114 | if validator in traversedValidators: |
| 115 | return validator |
| 116 | |
| 117 | def _validate(self): |
| 118 | dict = { |
| 119 | 'validator' : None, |
| 120 | 'min' : None, |
| 121 | 'max' : None, |
| 122 | 'minstrict' : 1, |
| 123 | 'maxstrict' : 1, |
| 124 | } |
| 125 | opt = self['validate'] |
| 126 | if type(opt) is types.DictionaryType: |
| 127 | dict.update(opt) |
| 128 | else: |
| 129 | dict['validator'] = opt |
| 130 | |
| 131 | # Look up validator maps and replace 'validator' field with |
| 132 | # the corresponding function. |
| 133 | validator = dict['validator'] |
| 134 | valFunction = self._getValidatorFunc(validator, 0) |
| 135 | self._checkValidateFunction(valFunction, 'validate', validator) |
| 136 | dict['validator'] = valFunction |
| 137 | |
| 138 | # Look up validator maps and replace 'stringtovalue' field |
| 139 | # with the corresponding function. |
| 140 | if dict.has_key('stringtovalue'): |
| 141 | stringtovalue = dict['stringtovalue'] |
| 142 | strFunction = self._getValidatorFunc(stringtovalue, 1) |
| 143 | self._checkValidateFunction( |
| 144 | strFunction, 'stringtovalue', stringtovalue) |
| 145 | else: |
| 146 | strFunction = self._getValidatorFunc(validator, 1) |
| 147 | if strFunction == validator: |
| 148 | strFunction = len |
| 149 | dict['stringtovalue'] = strFunction |
| 150 | |
| 151 | self._validationInfo = dict |
| 152 | args = dict.copy() |
| 153 | del args['validator'] |
| 154 | del args['min'] |
| 155 | del args['max'] |
| 156 | del args['minstrict'] |
| 157 | del args['maxstrict'] |
| 158 | del args['stringtovalue'] |
| 159 | self._validationArgs = args |
| 160 | self._previousText = None |
| 161 | |
| 162 | if type(dict['min']) == types.StringType and strFunction is not None: |
| 163 | dict['min'] = apply(strFunction, (dict['min'],), args) |
| 164 | if type(dict['max']) == types.StringType and strFunction is not None: |
| 165 | dict['max'] = apply(strFunction, (dict['max'],), args) |
| 166 | |
| 167 | self._checkValidity() |
| 168 | |
| 169 | def _checkValidateFunction(self, function, option, validator): |
| 170 | # Raise an error if 'function' is not a function or None. |
| 171 | |
| 172 | if function is not None and not callable(function): |
| 173 | extraValidators = self['extravalidators'] |
| 174 | extra = extraValidators.keys() |
| 175 | extra.sort() |
| 176 | extra = tuple(extra) |
| 177 | standard = _standardValidators.keys() |
| 178 | standard.sort() |
| 179 | standard = tuple(standard) |
| 180 | msg = 'bad %s value "%s": must be a function or one of ' \ |
| 181 | 'the standard validators %s or extra validators %s' |
| 182 | raise ValueError, msg % (option, validator, standard, extra) |
| 183 | |
| 184 | def _executeCommand(self, event = None): |
| 185 | cmd = self['command'] |
| 186 | if callable(cmd): |
| 187 | if event is None: |
| 188 | # Return result of command for invoke() method. |
| 189 | return cmd() |
| 190 | else: |
| 191 | cmd() |
| 192 | |
| 193 | def _preProcess(self): |
| 194 | |
| 195 | self._previousText = self._entryFieldEntry.get() |
| 196 | self._previousICursor = self._entryFieldEntry.index('insert') |
| 197 | self._previousXview = self._entryFieldEntry.index('@0') |
| 198 | if self._entryFieldEntry.selection_present(): |
| 199 | self._previousSel= (self._entryFieldEntry.index('sel.first'), |
| 200 | self._entryFieldEntry.index('sel.last')) |
| 201 | else: |
| 202 | self._previousSel = None |
| 203 | |
| 204 | def _postProcess(self): |
| 205 | |
| 206 | # No need to check if text has not changed. |
| 207 | previousText = self._previousText |
| 208 | if previousText == self._entryFieldEntry.get(): |
| 209 | return self.valid() |
| 210 | |
| 211 | valid = self._checkValidity() |
| 212 | if self.hulldestroyed(): |
| 213 | # The invalidcommand called by _checkValidity() destroyed us. |
| 214 | return valid |
| 215 | |
| 216 | cmd = self['modifiedcommand'] |
| 217 | if callable(cmd) and previousText != self._entryFieldEntry.get(): |
| 218 | cmd() |
| 219 | return valid |
| 220 | |
| 221 | def checkentry(self): |
| 222 | # If there is a variable specified by the entry_textvariable |
| 223 | # option, checkentry() should be called after the set() method |
| 224 | # of the variable is called. |
| 225 | |
| 226 | self._previousText = None |
| 227 | return self._postProcess() |
| 228 | |
| 229 | def _getValidity(self): |
| 230 | text = self._entryFieldEntry.get() |
| 231 | dict = self._validationInfo |
| 232 | args = self._validationArgs |
| 233 | |
| 234 | if dict['validator'] is not None: |
| 235 | status = apply(dict['validator'], (text,), args) |
| 236 | if status != OK: |
| 237 | return status |
| 238 | |
| 239 | # Check for out of (min, max) range. |
| 240 | if dict['stringtovalue'] is not None: |
| 241 | min = dict['min'] |
| 242 | max = dict['max'] |
| 243 | if min is None and max is None: |
| 244 | return OK |
| 245 | val = apply(dict['stringtovalue'], (text,), args) |
| 246 | if min is not None and val < min: |
| 247 | if dict['minstrict']: |
| 248 | return ERROR |
| 249 | else: |
| 250 | return PARTIAL |
| 251 | if max is not None and val > max: |
| 252 | if dict['maxstrict']: |
| 253 | return ERROR |
| 254 | else: |
| 255 | return PARTIAL |
| 256 | return OK |
| 257 | |
| 258 | def _checkValidity(self): |
| 259 | valid = self._getValidity() |
| 260 | oldValidity = valid |
| 261 | |
| 262 | if valid == ERROR: |
| 263 | # The entry is invalid. |
| 264 | cmd = self['invalidcommand'] |
| 265 | if callable(cmd): |
| 266 | cmd() |
| 267 | if self.hulldestroyed(): |
| 268 | # The invalidcommand destroyed us. |
| 269 | return oldValidity |
| 270 | |
| 271 | # Restore the entry to its previous value. |
| 272 | if self._previousText is not None: |
| 273 | self.__setEntry(self._previousText) |
| 274 | self._entryFieldEntry.icursor(self._previousICursor) |
| 275 | self._entryFieldEntry.xview(self._previousXview) |
| 276 | if self._previousSel is not None: |
| 277 | self._entryFieldEntry.selection_range(self._previousSel[0], |
| 278 | self._previousSel[1]) |
| 279 | |
| 280 | # Check if the saved text is valid as well. |
| 281 | valid = self._getValidity() |
| 282 | |
| 283 | self._valid = valid |
| 284 | |
| 285 | if self.hulldestroyed(): |
| 286 | # The validator or stringtovalue commands called by |
| 287 | # _checkValidity() destroyed us. |
| 288 | return oldValidity |
| 289 | |
| 290 | if valid == OK: |
| 291 | if self.normalBackground is not None: |
| 292 | self._entryFieldEntry.configure( |
| 293 | background = self.normalBackground) |
| 294 | self.normalBackground = None |
| 295 | else: |
| 296 | if self.normalBackground is None: |
| 297 | self.normalBackground = self._entryFieldEntry.cget('background') |
| 298 | self._entryFieldEntry.configure( |
| 299 | background = self['errorbackground']) |
| 300 | |
| 301 | return oldValidity |
| 302 | |
| 303 | def invoke(self): |
| 304 | return self._executeCommand() |
| 305 | |
| 306 | def valid(self): |
| 307 | return self._valid == OK |
| 308 | |
| 309 | def clear(self): |
| 310 | self.setentry('') |
| 311 | |
| 312 | def __setEntry(self, text): |
| 313 | oldState = str(self._entryFieldEntry.cget('state')) |
| 314 | if oldState != 'normal': |
| 315 | self._entryFieldEntry.configure(state='normal') |
| 316 | self._entryFieldEntry.delete(0, 'end') |
| 317 | self._entryFieldEntry.insert(0, text) |
| 318 | if oldState != 'normal': |
| 319 | self._entryFieldEntry.configure(state=oldState) |
| 320 | |
| 321 | def setentry(self, text): |
| 322 | self._preProcess() |
| 323 | self.__setEntry(text) |
| 324 | return self._postProcess() |
| 325 | |
| 326 | def getvalue(self): |
| 327 | return self._entryFieldEntry.get() |
| 328 | |
| 329 | def setvalue(self, text): |
| 330 | return self.setentry(text) |
| 331 | |
| 332 | Pmw.forwardmethods(EntryField, Tkinter.Entry, '_entryFieldEntry') |
| 333 | |
| 334 | # ====================================================================== |
| 335 | |
| 336 | |
| 337 | # Entry field validation functions |
| 338 | |
| 339 | _numericregex = re.compile('^[0-9]*$') |
| 340 | _alphabeticregex = re.compile('^[a-z]*$', re.IGNORECASE) |
| 341 | _alphanumericregex = re.compile('^[0-9a-z]*$', re.IGNORECASE) |
| 342 | |
| 343 | def numericvalidator(text): |
| 344 | if text == '': |
| 345 | return PARTIAL |
| 346 | else: |
| 347 | if _numericregex.match(text) is None: |
| 348 | return ERROR |
| 349 | else: |
| 350 | return OK |
| 351 | |
| 352 | def integervalidator(text): |
| 353 | if text in ('', '-', '+'): |
| 354 | return PARTIAL |
| 355 | try: |
| 356 | string.atol(text) |
| 357 | return OK |
| 358 | except ValueError: |
| 359 | return ERROR |
| 360 | |
| 361 | def alphabeticvalidator(text): |
| 362 | if _alphabeticregex.match(text) is None: |
| 363 | return ERROR |
| 364 | else: |
| 365 | return OK |
| 366 | |
| 367 | def alphanumericvalidator(text): |
| 368 | if _alphanumericregex.match(text) is None: |
| 369 | return ERROR |
| 370 | else: |
| 371 | return OK |
| 372 | |
| 373 | def hexadecimalvalidator(text): |
| 374 | if text in ('', '0x', '0X', '+', '+0x', '+0X', '-', '-0x', '-0X'): |
| 375 | return PARTIAL |
| 376 | try: |
| 377 | string.atol(text, 16) |
| 378 | return OK |
| 379 | except ValueError: |
| 380 | return ERROR |
| 381 | |
| 382 | def realvalidator(text, separator = '.'): |
| 383 | if separator != '.': |
| 384 | if string.find(text, '.') >= 0: |
| 385 | return ERROR |
| 386 | index = string.find(text, separator) |
| 387 | if index >= 0: |
| 388 | text = text[:index] + '.' + text[index + 1:] |
| 389 | try: |
| 390 | string.atof(text) |
| 391 | return OK |
| 392 | except ValueError: |
| 393 | # Check if the string could be made valid by appending a digit |
| 394 | # eg ('-', '+', '.', '-.', '+.', '1.23e', '1E-'). |
| 395 | if len(text) == 0: |
| 396 | return PARTIAL |
| 397 | if text[-1] in string.digits: |
| 398 | return ERROR |
| 399 | try: |
| 400 | string.atof(text + '0') |
| 401 | return PARTIAL |
| 402 | except ValueError: |
| 403 | return ERROR |
| 404 | |
| 405 | def timevalidator(text, separator = ':'): |
| 406 | try: |
| 407 | Pmw.timestringtoseconds(text, separator) |
| 408 | return OK |
| 409 | except ValueError: |
| 410 | if len(text) > 0 and text[0] in ('+', '-'): |
| 411 | text = text[1:] |
| 412 | if re.search('[^0-9' + separator + ']', text) is not None: |
| 413 | return ERROR |
| 414 | return PARTIAL |
| 415 | |
| 416 | def datevalidator(text, format = 'ymd', separator = '/'): |
| 417 | try: |
| 418 | Pmw.datestringtojdn(text, format, separator) |
| 419 | return OK |
| 420 | except ValueError: |
| 421 | if re.search('[^0-9' + separator + ']', text) is not None: |
| 422 | return ERROR |
| 423 | return PARTIAL |
| 424 | |
| 425 | _standardValidators = { |
| 426 | 'numeric' : (numericvalidator, string.atol), |
| 427 | 'integer' : (integervalidator, string.atol), |
| 428 | 'hexadecimal' : (hexadecimalvalidator, lambda s: string.atol(s, 16)), |
| 429 | 'real' : (realvalidator, Pmw.stringtoreal), |
| 430 | 'alphabetic' : (alphabeticvalidator, len), |
| 431 | 'alphanumeric' : (alphanumericvalidator, len), |
| 432 | 'time' : (timevalidator, Pmw.timestringtoseconds), |
| 433 | 'date' : (datevalidator, Pmw.datestringtojdn), |
| 434 | } |
| 435 | |
| 436 | _entryCache = {} |
| 437 | |
| 438 | def _registerEntryField(entry, entryField): |
| 439 | # Register an EntryField widget for an Entry widget |
| 440 | |
| 441 | _entryCache[entry] = entryField |
| 442 | |
| 443 | def _deregisterEntryField(entry): |
| 444 | # Deregister an Entry widget |
| 445 | del _entryCache[entry] |
| 446 | |
| 447 | def _preProcess(event): |
| 448 | # Forward preprocess events for an Entry to it's EntryField |
| 449 | |
| 450 | _entryCache[event.widget]._preProcess() |
| 451 | |
| 452 | def _postProcess(event): |
| 453 | # Forward postprocess events for an Entry to it's EntryField |
| 454 | |
| 455 | # The function specified by the 'command' option may have destroyed |
| 456 | # the megawidget in a binding earlier in bindtags, so need to check. |
| 457 | if _entryCache.has_key(event.widget): |
| 458 | _entryCache[event.widget]._postProcess() |