Commit | Line | Data |
---|---|---|
86530b38 AT |
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() |