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