1# Based on iwidgets2.2.0/entryfield.itk code. 2 3import re 4import string 5import types 6import Tkinter 7import Pmw 8 9# Possible return values of validation functions. 10OK = 1 11ERROR = 0 12PARTIAL = -1 13 14class 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 332Pmw.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 343def 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 352def 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 361def alphabeticvalidator(text): 362 if _alphabeticregex.match(text) is None: 363 return ERROR 364 else: 365 return OK 366 367def alphanumericvalidator(text): 368 if _alphanumericregex.match(text) is None: 369 return ERROR 370 else: 371 return OK 372 373def 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 382def 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 405def 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 416def 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 438def _registerEntryField(entry, entryField): 439 # Register an EntryField widget for an Entry widget 440 441 _entryCache[entry] = entryField 442 443def _deregisterEntryField(entry): 444 # Deregister an Entry widget 445 del _entryCache[entry] 446 447def _preProcess(event): 448 # Forward preprocess events for an Entry to it's EntryField 449 450 _entryCache[event.widget]._preProcess() 451 452def _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() 459