459 lines
13 KiB
Python
459 lines
13 KiB
Python
|
# Based on iwidgets2.2.0/entryfield.itk code.
|
||
|
|
||
|
import re
|
||
|
import string
|
||
|
import types
|
||
|
import Tkinter
|
||
|
import Pmw
|
||
|
|
||
|
# Possible return values of validation functions.
|
||
|
OK = 1
|
||
|
ERROR = 0
|
||
|
PARTIAL = -1
|
||
|
|
||
|
class EntryField(Pmw.MegaWidget):
|
||
|
_classBindingsDefinedFor = 0
|
||
|
|
||
|
def __init__(self, parent = None, **kw):
|
||
|
|
||
|
# Define the megawidget options.
|
||
|
INITOPT = Pmw.INITOPT
|
||
|
optiondefs = (
|
||
|
('command', None, None),
|
||
|
('errorbackground', 'pink', None),
|
||
|
('invalidcommand', self.bell, None),
|
||
|
('labelmargin', 0, INITOPT),
|
||
|
('labelpos', None, INITOPT),
|
||
|
('modifiedcommand', None, None),
|
||
|
('sticky', 'ew', INITOPT),
|
||
|
('validate', None, self._validate),
|
||
|
('extravalidators', {}, None),
|
||
|
('value', '', INITOPT),
|
||
|
)
|
||
|
self.defineoptions(kw, optiondefs)
|
||
|
|
||
|
# Initialise the base class (after defining the options).
|
||
|
Pmw.MegaWidget.__init__(self, parent)
|
||
|
|
||
|
# Create the components.
|
||
|
interior = self.interior()
|
||
|
self._entryFieldEntry = self.createcomponent('entry',
|
||
|
(), None,
|
||
|
Tkinter.Entry, (interior,))
|
||
|
self._entryFieldEntry.grid(column=2, row=2, sticky=self['sticky'])
|
||
|
if self['value'] != '':
|
||
|
self.__setEntry(self['value'])
|
||
|
interior.grid_columnconfigure(2, weight=1)
|
||
|
interior.grid_rowconfigure(2, weight=1)
|
||
|
|
||
|
self.createlabel(interior)
|
||
|
|
||
|
# Initialise instance variables.
|
||
|
|
||
|
self.normalBackground = None
|
||
|
self._previousText = None
|
||
|
|
||
|
# Initialise instance.
|
||
|
|
||
|
_registerEntryField(self._entryFieldEntry, self)
|
||
|
|
||
|
# Establish the special class bindings if not already done.
|
||
|
# Also create bindings if the Tkinter default interpreter has
|
||
|
# changed. Use Tkinter._default_root to create class
|
||
|
# bindings, so that a reference to root is created by
|
||
|
# bind_class rather than a reference to self, which would
|
||
|
# prevent object cleanup.
|
||
|
if EntryField._classBindingsDefinedFor != Tkinter._default_root:
|
||
|
tagList = self._entryFieldEntry.bindtags()
|
||
|
root = Tkinter._default_root
|
||
|
|
||
|
allSequences = {}
|
||
|
for tag in tagList:
|
||
|
|
||
|
sequences = root.bind_class(tag)
|
||
|
if type(sequences) is types.StringType:
|
||
|
# In old versions of Tkinter, bind_class returns a string
|
||
|
sequences = root.tk.splitlist(sequences)
|
||
|
|
||
|
for sequence in sequences:
|
||
|
allSequences[sequence] = None
|
||
|
for sequence in allSequences.keys():
|
||
|
root.bind_class('EntryFieldPre', sequence, _preProcess)
|
||
|
root.bind_class('EntryFieldPost', sequence, _postProcess)
|
||
|
|
||
|
EntryField._classBindingsDefinedFor = root
|
||
|
|
||
|
self._entryFieldEntry.bindtags(('EntryFieldPre',) +
|
||
|
self._entryFieldEntry.bindtags() + ('EntryFieldPost',))
|
||
|
self._entryFieldEntry.bind('<Return>', self._executeCommand)
|
||
|
|
||
|
# Check keywords and initialise options.
|
||
|
self.initialiseoptions()
|
||
|
|
||
|
def destroy(self):
|
||
|
_deregisterEntryField(self._entryFieldEntry)
|
||
|
Pmw.MegaWidget.destroy(self)
|
||
|
|
||
|
def _getValidatorFunc(self, validator, index):
|
||
|
# Search the extra and standard validator lists for the
|
||
|
# given 'validator'. If 'validator' is an alias, then
|
||
|
# continue the search using the alias. Make sure that
|
||
|
# self-referencial aliases do not cause infinite loops.
|
||
|
|
||
|
extraValidators = self['extravalidators']
|
||
|
traversedValidators = []
|
||
|
|
||
|
while 1:
|
||
|
traversedValidators.append(validator)
|
||
|
if extraValidators.has_key(validator):
|
||
|
validator = extraValidators[validator][index]
|
||
|
elif _standardValidators.has_key(validator):
|
||
|
validator = _standardValidators[validator][index]
|
||
|
else:
|
||
|
return validator
|
||
|
if validator in traversedValidators:
|
||
|
return validator
|
||
|
|
||
|
def _validate(self):
|
||
|
dict = {
|
||
|
'validator' : None,
|
||
|
'min' : None,
|
||
|
'max' : None,
|
||
|
'minstrict' : 1,
|
||
|
'maxstrict' : 1,
|
||
|
}
|
||
|
opt = self['validate']
|
||
|
if type(opt) is types.DictionaryType:
|
||
|
dict.update(opt)
|
||
|
else:
|
||
|
dict['validator'] = opt
|
||
|
|
||
|
# Look up validator maps and replace 'validator' field with
|
||
|
# the corresponding function.
|
||
|
validator = dict['validator']
|
||
|
valFunction = self._getValidatorFunc(validator, 0)
|
||
|
self._checkValidateFunction(valFunction, 'validate', validator)
|
||
|
dict['validator'] = valFunction
|
||
|
|
||
|
# Look up validator maps and replace 'stringtovalue' field
|
||
|
# with the corresponding function.
|
||
|
if dict.has_key('stringtovalue'):
|
||
|
stringtovalue = dict['stringtovalue']
|
||
|
strFunction = self._getValidatorFunc(stringtovalue, 1)
|
||
|
self._checkValidateFunction(
|
||
|
strFunction, 'stringtovalue', stringtovalue)
|
||
|
else:
|
||
|
strFunction = self._getValidatorFunc(validator, 1)
|
||
|
if strFunction == validator:
|
||
|
strFunction = len
|
||
|
dict['stringtovalue'] = strFunction
|
||
|
|
||
|
self._validationInfo = dict
|
||
|
args = dict.copy()
|
||
|
del args['validator']
|
||
|
del args['min']
|
||
|
del args['max']
|
||
|
del args['minstrict']
|
||
|
del args['maxstrict']
|
||
|
del args['stringtovalue']
|
||
|
self._validationArgs = args
|
||
|
self._previousText = None
|
||
|
|
||
|
if type(dict['min']) == types.StringType and strFunction is not None:
|
||
|
dict['min'] = apply(strFunction, (dict['min'],), args)
|
||
|
if type(dict['max']) == types.StringType and strFunction is not None:
|
||
|
dict['max'] = apply(strFunction, (dict['max'],), args)
|
||
|
|
||
|
self._checkValidity()
|
||
|
|
||
|
def _checkValidateFunction(self, function, option, validator):
|
||
|
# Raise an error if 'function' is not a function or None.
|
||
|
|
||
|
if function is not None and not callable(function):
|
||
|
extraValidators = self['extravalidators']
|
||
|
extra = extraValidators.keys()
|
||
|
extra.sort()
|
||
|
extra = tuple(extra)
|
||
|
standard = _standardValidators.keys()
|
||
|
standard.sort()
|
||
|
standard = tuple(standard)
|
||
|
msg = 'bad %s value "%s": must be a function or one of ' \
|
||
|
'the standard validators %s or extra validators %s'
|
||
|
raise ValueError, msg % (option, validator, standard, extra)
|
||
|
|
||
|
def _executeCommand(self, event = None):
|
||
|
cmd = self['command']
|
||
|
if callable(cmd):
|
||
|
if event is None:
|
||
|
# Return result of command for invoke() method.
|
||
|
return cmd()
|
||
|
else:
|
||
|
cmd()
|
||
|
|
||
|
def _preProcess(self):
|
||
|
|
||
|
self._previousText = self._entryFieldEntry.get()
|
||
|
self._previousICursor = self._entryFieldEntry.index('insert')
|
||
|
self._previousXview = self._entryFieldEntry.index('@0')
|
||
|
if self._entryFieldEntry.selection_present():
|
||
|
self._previousSel= (self._entryFieldEntry.index('sel.first'),
|
||
|
self._entryFieldEntry.index('sel.last'))
|
||
|
else:
|
||
|
self._previousSel = None
|
||
|
|
||
|
def _postProcess(self):
|
||
|
|
||
|
# No need to check if text has not changed.
|
||
|
previousText = self._previousText
|
||
|
if previousText == self._entryFieldEntry.get():
|
||
|
return self.valid()
|
||
|
|
||
|
valid = self._checkValidity()
|
||
|
if self.hulldestroyed():
|
||
|
# The invalidcommand called by _checkValidity() destroyed us.
|
||
|
return valid
|
||
|
|
||
|
cmd = self['modifiedcommand']
|
||
|
if callable(cmd) and previousText != self._entryFieldEntry.get():
|
||
|
cmd()
|
||
|
return valid
|
||
|
|
||
|
def checkentry(self):
|
||
|
# If there is a variable specified by the entry_textvariable
|
||
|
# option, checkentry() should be called after the set() method
|
||
|
# of the variable is called.
|
||
|
|
||
|
self._previousText = None
|
||
|
return self._postProcess()
|
||
|
|
||
|
def _getValidity(self):
|
||
|
text = self._entryFieldEntry.get()
|
||
|
dict = self._validationInfo
|
||
|
args = self._validationArgs
|
||
|
|
||
|
if dict['validator'] is not None:
|
||
|
status = apply(dict['validator'], (text,), args)
|
||
|
if status != OK:
|
||
|
return status
|
||
|
|
||
|
# Check for out of (min, max) range.
|
||
|
if dict['stringtovalue'] is not None:
|
||
|
min = dict['min']
|
||
|
max = dict['max']
|
||
|
if min is None and max is None:
|
||
|
return OK
|
||
|
val = apply(dict['stringtovalue'], (text,), args)
|
||
|
if min is not None and val < min:
|
||
|
if dict['minstrict']:
|
||
|
return ERROR
|
||
|
else:
|
||
|
return PARTIAL
|
||
|
if max is not None and val > max:
|
||
|
if dict['maxstrict']:
|
||
|
return ERROR
|
||
|
else:
|
||
|
return PARTIAL
|
||
|
return OK
|
||
|
|
||
|
def _checkValidity(self):
|
||
|
valid = self._getValidity()
|
||
|
oldValidity = valid
|
||
|
|
||
|
if valid == ERROR:
|
||
|
# The entry is invalid.
|
||
|
cmd = self['invalidcommand']
|
||
|
if callable(cmd):
|
||
|
cmd()
|
||
|
if self.hulldestroyed():
|
||
|
# The invalidcommand destroyed us.
|
||
|
return oldValidity
|
||
|
|
||
|
# Restore the entry to its previous value.
|
||
|
if self._previousText is not None:
|
||
|
self.__setEntry(self._previousText)
|
||
|
self._entryFieldEntry.icursor(self._previousICursor)
|
||
|
self._entryFieldEntry.xview(self._previousXview)
|
||
|
if self._previousSel is not None:
|
||
|
self._entryFieldEntry.selection_range(self._previousSel[0],
|
||
|
self._previousSel[1])
|
||
|
|
||
|
# Check if the saved text is valid as well.
|
||
|
valid = self._getValidity()
|
||
|
|
||
|
self._valid = valid
|
||
|
|
||
|
if self.hulldestroyed():
|
||
|
# The validator or stringtovalue commands called by
|
||
|
# _checkValidity() destroyed us.
|
||
|
return oldValidity
|
||
|
|
||
|
if valid == OK:
|
||
|
if self.normalBackground is not None:
|
||
|
self._entryFieldEntry.configure(
|
||
|
background = self.normalBackground)
|
||
|
self.normalBackground = None
|
||
|
else:
|
||
|
if self.normalBackground is None:
|
||
|
self.normalBackground = self._entryFieldEntry.cget('background')
|
||
|
self._entryFieldEntry.configure(
|
||
|
background = self['errorbackground'])
|
||
|
|
||
|
return oldValidity
|
||
|
|
||
|
def invoke(self):
|
||
|
return self._executeCommand()
|
||
|
|
||
|
def valid(self):
|
||
|
return self._valid == OK
|
||
|
|
||
|
def clear(self):
|
||
|
self.setentry('')
|
||
|
|
||
|
def __setEntry(self, text):
|
||
|
oldState = str(self._entryFieldEntry.cget('state'))
|
||
|
if oldState != 'normal':
|
||
|
self._entryFieldEntry.configure(state='normal')
|
||
|
self._entryFieldEntry.delete(0, 'end')
|
||
|
self._entryFieldEntry.insert(0, text)
|
||
|
if oldState != 'normal':
|
||
|
self._entryFieldEntry.configure(state=oldState)
|
||
|
|
||
|
def setentry(self, text):
|
||
|
self._preProcess()
|
||
|
self.__setEntry(text)
|
||
|
return self._postProcess()
|
||
|
|
||
|
def getvalue(self):
|
||
|
return self._entryFieldEntry.get()
|
||
|
|
||
|
def setvalue(self, text):
|
||
|
return self.setentry(text)
|
||
|
|
||
|
Pmw.forwardmethods(EntryField, Tkinter.Entry, '_entryFieldEntry')
|
||
|
|
||
|
# ======================================================================
|
||
|
|
||
|
|
||
|
# Entry field validation functions
|
||
|
|
||
|
_numericregex = re.compile('^[0-9]*$')
|
||
|
_alphabeticregex = re.compile('^[a-z]*$', re.IGNORECASE)
|
||
|
_alphanumericregex = re.compile('^[0-9a-z]*$', re.IGNORECASE)
|
||
|
|
||
|
def numericvalidator(text):
|
||
|
if text == '':
|
||
|
return PARTIAL
|
||
|
else:
|
||
|
if _numericregex.match(text) is None:
|
||
|
return ERROR
|
||
|
else:
|
||
|
return OK
|
||
|
|
||
|
def integervalidator(text):
|
||
|
if text in ('', '-', '+'):
|
||
|
return PARTIAL
|
||
|
try:
|
||
|
string.atol(text)
|
||
|
return OK
|
||
|
except ValueError:
|
||
|
return ERROR
|
||
|
|
||
|
def alphabeticvalidator(text):
|
||
|
if _alphabeticregex.match(text) is None:
|
||
|
return ERROR
|
||
|
else:
|
||
|
return OK
|
||
|
|
||
|
def alphanumericvalidator(text):
|
||
|
if _alphanumericregex.match(text) is None:
|
||
|
return ERROR
|
||
|
else:
|
||
|
return OK
|
||
|
|
||
|
def hexadecimalvalidator(text):
|
||
|
if text in ('', '0x', '0X', '+', '+0x', '+0X', '-', '-0x', '-0X'):
|
||
|
return PARTIAL
|
||
|
try:
|
||
|
string.atol(text, 16)
|
||
|
return OK
|
||
|
except ValueError:
|
||
|
return ERROR
|
||
|
|
||
|
def realvalidator(text, separator = '.'):
|
||
|
if separator != '.':
|
||
|
if string.find(text, '.') >= 0:
|
||
|
return ERROR
|
||
|
index = string.find(text, separator)
|
||
|
if index >= 0:
|
||
|
text = text[:index] + '.' + text[index + 1:]
|
||
|
try:
|
||
|
string.atof(text)
|
||
|
return OK
|
||
|
except ValueError:
|
||
|
# Check if the string could be made valid by appending a digit
|
||
|
# eg ('-', '+', '.', '-.', '+.', '1.23e', '1E-').
|
||
|
if len(text) == 0:
|
||
|
return PARTIAL
|
||
|
if text[-1] in string.digits:
|
||
|
return ERROR
|
||
|
try:
|
||
|
string.atof(text + '0')
|
||
|
return PARTIAL
|
||
|
except ValueError:
|
||
|
return ERROR
|
||
|
|
||
|
def timevalidator(text, separator = ':'):
|
||
|
try:
|
||
|
Pmw.timestringtoseconds(text, separator)
|
||
|
return OK
|
||
|
except ValueError:
|
||
|
if len(text) > 0 and text[0] in ('+', '-'):
|
||
|
text = text[1:]
|
||
|
if re.search('[^0-9' + separator + ']', text) is not None:
|
||
|
return ERROR
|
||
|
return PARTIAL
|
||
|
|
||
|
def datevalidator(text, format = 'ymd', separator = '/'):
|
||
|
try:
|
||
|
Pmw.datestringtojdn(text, format, separator)
|
||
|
return OK
|
||
|
except ValueError:
|
||
|
if re.search('[^0-9' + separator + ']', text) is not None:
|
||
|
return ERROR
|
||
|
return PARTIAL
|
||
|
|
||
|
_standardValidators = {
|
||
|
'numeric' : (numericvalidator, string.atol),
|
||
|
'integer' : (integervalidator, string.atol),
|
||
|
'hexadecimal' : (hexadecimalvalidator, lambda s: string.atol(s, 16)),
|
||
|
'real' : (realvalidator, Pmw.stringtoreal),
|
||
|
'alphabetic' : (alphabeticvalidator, len),
|
||
|
'alphanumeric' : (alphanumericvalidator, len),
|
||
|
'time' : (timevalidator, Pmw.timestringtoseconds),
|
||
|
'date' : (datevalidator, Pmw.datestringtojdn),
|
||
|
}
|
||
|
|
||
|
_entryCache = {}
|
||
|
|
||
|
def _registerEntryField(entry, entryField):
|
||
|
# Register an EntryField widget for an Entry widget
|
||
|
|
||
|
_entryCache[entry] = entryField
|
||
|
|
||
|
def _deregisterEntryField(entry):
|
||
|
# Deregister an Entry widget
|
||
|
del _entryCache[entry]
|
||
|
|
||
|
def _preProcess(event):
|
||
|
# Forward preprocess events for an Entry to it's EntryField
|
||
|
|
||
|
_entryCache[event.widget]._preProcess()
|
||
|
|
||
|
def _postProcess(event):
|
||
|
# Forward postprocess events for an Entry to it's EntryField
|
||
|
|
||
|
# The function specified by the 'command' option may have destroyed
|
||
|
# the megawidget in a binding earlier in bindtags, so need to check.
|
||
|
if _entryCache.has_key(event.widget):
|
||
|
_entryCache[event.widget]._postProcess()
|