diff options
Diffstat (limited to 'rapid/ValidatedEntry.py')
-rw-r--r-- | rapid/ValidatedEntry.py | 383 |
1 files changed, 383 insertions, 0 deletions
diff --git a/rapid/ValidatedEntry.py b/rapid/ValidatedEntry.py new file mode 100644 index 0000000..cb453f4 --- /dev/null +++ b/rapid/ValidatedEntry.py @@ -0,0 +1,383 @@ +# Copyright (c) 2006, Daniel J. Popowich +# +# Permission is hereby granted, free of charge, to any person +# obtaining a copy of this software and associated documentation files +# (the "Software"), to deal in the Software without restriction, +# including without limitation the rights to use, copy, modify, merge, +# publish, distribute, sublicense, and/or sell copies of the Software, +# and to permit persons to whom the Software is furnished to do so, +# subject to the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS +# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN +# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +# +# Send bug reports and contributions to: +# +# dpopowich AT astro dot umass dot edu +# + +''' +ValidatedEntry.py + +Provides ValidatedEntry, a subclass of gtk.Entry which validates +input. + +Usage: create an instance of ValidatedEntry, specifying the function +to validate input. E.g.: + + : def money(text): + : "validate input to be monetary value" + : ... + : + : money_entry = ValidatedEntry(money) + +Validation functions must accept one argument, the text to be +validated, and must return one of: + + 1: the input is valid. + 0: the input is invalid and should not be displayed. + -1: the input is partially valid and will be displayed (and by + default with a different background color). + +Three module-level variables are defined for the convenience of +validation function writers: VALID (1), INVALID (0), PARTIAL (-1). + +There is one public method, isvalid(), which will return True if the +current text is valid. + +Note: care should be taken when implementing validation functions to +allow empty strings to be VALID or at least PARTIAL. An empty string +should never be INVALID. + +Note: the hooks for calling the validation function are implemented by +connecting the object to handlers for the gtk.Editable "insert-text" +and "delete-text" signals. These handlers are connected to instances +in the constructor, so will, by default, be called before other +handlers connected to the widgets for "*-text" signals. When input is +INVALID, stop_emission() is called, so later handlers for "*-text" +signals will not be called. + +See the doc string for ValidatedEntry.__init__ for more details. + +''' + +import pygtk +pygtk.require('2.0') + +import gtk +import gtk.gdk + +if gtk.gtk_version < (2, 8): + import warnings + + msg ='''This module was developed and tested with version 2.8.9 of gtk. +You are using version %d.%d.%d. Your milage may vary''' % gtk.gtk_version + warnings.warn(msg) + +# major, minor, patch +version = 1, 0, 4 + +PARTIAL, INVALID, VALID = range(-1,2) + +class ValidatedEntry(gtk.Entry): + + white = gtk.gdk.color_parse('white') + yellow = gtk.gdk.color_parse('yellow') + + def __init__(self, valid_func, + max=0, + use_bg=True, valid_bg=white, partial_bg=yellow, + error_func=None): + ''' + Create instance of validating gtk.Entry. + + valid_func: the function to validate input. See module doc + string for details. + + max: passed to gtk.Entry constructor. (default: 0) + + use_bg: if True (the default) set the base color of the + widget to indicate validity; see valid_bg and partial_bg. + + valid_bg: a gtk.gdk.Color; the base color of the widget when + the input is valid. (default: white) + + partial_bg: a gtk.gdk.Color; the base color of the widget when + the input is partially valid. (default: yellow) + + error_func: a function to call (with no arguments) when + valid_func returns INVALID. If None (the default) + the default action will be to emit a short beep. + ''' + + assert valid_func('') != INVALID, 'valid_func cannot return INVALID for an empty string' + + gtk.Entry.__init__(self, max) + + self.__valid_func = valid_func + self.__use_bg = use_bg + self.__valid_bg = valid_bg + self.__partial_bg = partial_bg + self.__error_func = (error_func or + gtk.gdk.display_get_default().beep) + + self.connect('insert-text', self.__insert_text_cb) + self.connect('delete-text', self.__delete_text_cb) + + # bootstrap with an empty string (so the box will appear with + # the partial_bg if an empty string is PARTIAL) + self.insert_text('') + + def isvalid(self): + return self.__isvalid + + def __insert_text_cb(self, entry, text, length, position): + 'callback for "insert-text" signal' + + # generate what the new text will be + text = text[:length] + pos = self.get_position() + old = self.get_text() + new = old[:pos] + text + old[pos:] + + # validate the new text + self.__validate(new, 'insert-text') + + def __delete_text_cb(self, entry, start, end): + 'callback for "delete-text" signal' + + # generate what the new text will be + old = self.get_text() + new = old[:start] + old[end:] + + # validate the new text + self.__validate(new, 'delete-text') + + def __validate(self, text, signal): + 'calls the user-provided validation function' + + # validate + r = self.__valid_func(text) + if r == VALID: + self.__isvalid = True + if self.__use_bg: + self.modify_base(gtk.STATE_NORMAL, self.__valid_bg) + elif r == PARTIAL: + self.__isvalid = False + if self.__use_bg: + self.modify_base(gtk.STATE_NORMAL, self.__partial_bg) + else: + # don't set self.__isvalid: since we're not displaying the + # new value, the validity should be whatever it was before + self.stop_emission(signal) + self.__error_func() + + +###################################################################### +# +# Sample validation functions to use with ValidatedEntry +# +###################################################################### + +import re + + +# STRING (non-empty after stripping) +def v_nonemptystring(value): + ''' + VALID: non-empty string after stripping whitespace + PARTAL: empty or all whitespace + INVALID: N/A + ''' + if value.strip(): + return VALID + return PARTIAL + +# INT +def v_int(value): + ''' + VALID: any postive or negative integer + PARTAL: empty or leading "-" + INVALID: non-numeral + ''' + v = value.strip() + if not v or v == '-': + return PARTIAL + try: + int(value) + return VALID + except: + return INVALID + +# FLOAT +def v_float(value): + ''' + VALID: any postive or negative floating point + PARTAL: empty or leading "-", "." + INVALID: non-numeral + ''' + v = value.strip() + if not v or v in ('-', '.', '-.'): + return PARTIAL + try: + float(value) + return VALID + except: + return INVALID + + +# ISBN +_isbnpartial = re.compile('[0-9]{0,9}[0-9xX]?$') +def v_isbn(v): + + '''Validate ISBN input. + + From the isbn manual, section 4.4: + + The check digit is the last digit of an ISBN. It is calculated on + a modulus 11 with weights 10-2, using X in lieu of 10 where ten + would occur as a check digit. This means that each of the first + nine digits of the ISBN -- excluding the check digit itself -- is + multiplied by a number ranging from 10 to 2 and that the resulting + sum of the products, plus the check digit, must be divisible by 11 + without a remainder.''' + + + if _isbnpartial.match(v): + # isbn is ten characters in length + if len(v) < 10: + return PARTIAL + + s = 0 + + for i, c in enumerate(v): + s += (c in 'xX' and 10 or int(c)) * (10 - i) + + if s % 11 == 0: + return VALID + + return INVALID + +# MONEY +# re for (possibly negative) money +_money_re = re.compile('-?\d*(\.\d{1,2})?$') +# validation function for money +def v_money(value): + ''' + VALID: any postive or negative floating point with at most two + digits after the decimal point. + PARTAL: empty or leading "-", "." + INVALID: non-numeral or more than two digits after the decimal + point. + ''' + if not value or value == '-' or value[-1] == '.': + return PARTIAL + + if _money_re.match(value): + return VALID + + return INVALID + +# PHONE +# the characters in a phone number +_phonechars = re.compile('[- 0-9]*$') +# valid phone number: [AC +]EXT-LINE +_phone = re.compile('([2-9][0-8][0-9]\s+)?[2-9][0-9]{2}-[0-9]{4}$') +def v_phone(value): + ''' + VALID: any phone number of the form: EXT-LINE -or- AC EXT-LINE. + PARTAL: any characters that make up a valid #. + INVALID: characters that are not used in a phone #. + ''' + if _phone.match(value): + return VALID + if _phonechars.match(value): + return PARTIAL + return INVALID + +def empty_valid(vfunc): + + ''' + empty_valid is a factory function returning a validation function. + All of the validation functions in this module return PARTIAL for + empty strings which, in effect, forces non-empty input. There may + be a case where, e.g., you want money input to be optional, but + v_money will not consider empty input VALID. Instead of writing + another validation function you can instead use empty_valid(). By + wrapping a validation function with empty_valid(), input (after + stripping), if empty, will be considered VALID. E.g.: + + ventry = ValidatedEntry(empty_valid(v_money)) + + It is recommended that all your validation functions treat empty + input as PARTIAL, for consistency across all validation functions + and for use with empty_valid(). + ''' + + def validate(value): + if not value.strip(): + return VALID + return vfunc(value) + + return validate + + +def bounded(vfunc, conv, minv=None, maxv=None): + + ''' + bounded is a factory function returning a validation function + providing bounded input. E.g., you may want an entry that accepts + integers, but within a range, say, a score on a test graded in + whole numbers from 0 to 100: + + score_entry = ValidatedEntry(bounded(v_int, int, 0, 100)) + + Arguments: + + vfunc: A validation function. + conv: A callable that accepts a string argument (the text in + the entry) and returns a value to be compared to minv + and maxv. + minv: None or a value of the same type returned by conv. If + None, there is no minimum value enforced. If a value, + it will be the minimum value considered VALID. + maxv: None or a value of the same type returned by conv. If + None, there is no maximum value enforced. If a value, + it will be the maximum value considered VALID. + + One or both of minv/maxv must be specified. + + The function returned will call vfunc on entry input and if vfunc + returns VALID, the input will be converted by conv and compared to + minv/maxv. If the converted value is within the bounds of + minv/maxv then VALID will be returned, else PARTIAL will be + returned. + + ''' + + assert minv is not None or maxv is not None, \ + 'One of minv/maxv must be specified' + + def F(value): + + r = vfunc(value) + if r == VALID: + v = conv(value) + if minv is not None and v < minv: + return PARTIAL + if maxv is not None and v > maxv: + return PARTIAL + return r + + return F + + |