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