summaryrefslogtreecommitdiff
path: root/rapid/ValidatedEntry.py
diff options
context:
space:
mode:
Diffstat (limited to 'rapid/ValidatedEntry.py')
-rw-r--r--rapid/ValidatedEntry.py383
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
+
+