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