wx.lib.masked: Patch from Will Sadkin. Includes Unicode fixes, plus

more helpful exceptions and ability to designate fields in mask
without intervening fixed characters.


git-svn-id: https://svn.wxwidgets.org/svn/wx/wxWidgets/trunk@43827 c3d73ce0-8a6f-49c7-b76d-6d57e0e08775
This commit is contained in:
Robin Dunn
2006-12-05 18:57:31 +00:00
parent 482976649a
commit 5fe636c6e2
4 changed files with 192 additions and 65 deletions

View File

@@ -633,9 +633,11 @@ def RunStandalone():
app.MainLoop() app.MainLoop()
#---------------------------------------------------------------------------- #----------------------------------------------------------------------------
import wx.lib.masked.maskededit as maskededit import wx.lib.masked.maskededit as maskededit
# strip out module header used for pydoc:
demodoc = '\n'.join(maskededit.__doc__.split('\n')[2:])
overview = """<html> overview = """<html>
<PRE><FONT SIZE=-1> <PRE><FONT SIZE=-1>
""" + maskededit.__doc__ + """ """ + demodoc + """
</FONT></PRE> </FONT></PRE>
""" """

View File

@@ -46,6 +46,9 @@ Added some modules from Riaan Booysen:
* An I18N sample for the demo. * An I18N sample for the demo.
wx.lib.masked: Patch from Will Sadkin. Includes Unicode fixes, plus
more helpful exceptions and ability to designate fields in mask
without intervening fixed characters.

View File

@@ -9,6 +9,12 @@
# Copyright: (c) 2004 # Copyright: (c) 2004
# License: wxWidgets license # License: wxWidgets license
#---------------------------------------------------------------------- #----------------------------------------------------------------------
__author__ = "Will Sadkin <wsadkin |at| parlancecorp.com>"
__date__ = "02 Dec 2006, 19:00 GMT-05:00"
__version__ = "1.11"
__doc__ = """\
package providing "masked edit" controls, allowing characters within a data entry control to remain fixed, and providing fine-grain control over allowed user input.
"""
# import relevant external symbols into package namespace: # import relevant external symbols into package namespace:
from maskededit import * from maskededit import *

View File

@@ -1,10 +1,10 @@
#---------------------------------------------------------------------------- #----------------------------------------------------------------------------
# Name: maskededit.py # Name: maskededit.py
# Authors: Jeff Childers, Will Sadkin # Authors: Jeff Childers, Will Sadkin
# Email: jchilders_98@yahoo.com, wsadkin@nameconnector.com # Email: jchilders_98@yahoo.com, wsadkin@parlancecorp.com
# Created: 02/11/2003 # Created: 02/11/2003
# Copyright: (c) 2003 by Jeff Childers, Will Sadkin, 2003 # Copyright: (c) 2003 by Jeff Childers, Will Sadkin, 2003
# Portions: (c) 2002 by Will Sadkin, 2002-2003 # Portions: (c) 2002 by Will Sadkin, 2002-2006
# RCS-ID: $Id$ # RCS-ID: $Id$
# License: wxWindows license # License: wxWindows license
#---------------------------------------------------------------------------- #----------------------------------------------------------------------------
@@ -50,7 +50,9 @@
# o wxTimeCtrl -> TimeCtrl # o wxTimeCtrl -> TimeCtrl
# #
"""\ __doc__ = """\
contains MaskedEditMixin class that drives all the other masked controls.
==================== ====================
Masked Edit Overview Masked Edit Overview
==================== ====================
@@ -88,11 +90,11 @@ masked.Ctrl:
will be masked.TextCtrl. will be masked.TextCtrl.
Each of the above classes has its own set of arguments, but masked.Ctrl Each of the above classes has its own set of arguments, but masked.Ctrl
provides a single "unified" interface for masked controls. Those for provides a single "unified" interface for masked controls.
masked.TextCtrl, masked.ComboBox and masked.IpAddrCtrl are all documented
below; the others have their own demo pages and interface descriptions. What follows is a description of how to configure the generic masked.TextCtrl
(See end of following discussion for how to configure the wx.MaskedCtrl() and masked.ComboBox; masked.NumCtrl and masked.TimeCtrl have their own demo
to select the above control types.) pages and interface descriptions.
========================= =========================
@@ -110,8 +112,12 @@ mask
a Allow lowercase letters only a Allow lowercase letters only
C Allow any letter, upper or lower C Allow any letter, upper or lower
X Allow string.letters, string.punctuation, string.digits X Allow string.letters, string.punctuation, string.digits
& Allow string.punctuation only & Allow string.punctuation only (doesn't include all unicode symbols)
\* Allow any ansi character \* Allow any visible character
| explicit field boundary (takes no space in the control; allows mix
of adjacent mask characters to be treated as separate fields,
eg: '&|###' means "field 0 = '&', field 1 = '###'", but there's
no fixed characters in between.
========= ========================================================== ========= ==========================================================
@@ -127,8 +133,13 @@ mask
import locale import locale
locale.setlocale(locale.LC_ALL, '') locale.setlocale(locale.LC_ALL, '')
The controls now also support (by popular demand) all "ansi" chars, The controls now also support (by popular demand) all "visible" characters,
that is, all ascii codes between 32 and 255, by use of the * mask character. by use of the * mask character, including unicode characters above
the standard ANSI keycode range.
Note: As string.punctuation doesn't typically include all unicode
symbols, you will have to use includechars to get some of these into
otherwise restricted positions in your control, such as those specified
with &.
Using these mask characters, a variety of template masks can be built. See Using these mask characters, a variety of template masks can be built. See
the demo for some other common examples include date+time, social security the demo for some other common examples include date+time, social security
@@ -323,6 +334,9 @@ to individual fields:
of the nature of the validation and control of input. However, of the nature of the validation and control of input. However,
you can supply one to provide data transfer routines for the you can supply one to provide data transfer routines for the
controls. controls.
raiseOnInvalidPaste False by default; normally a bad paste simply is ignored with a bell;
if True, this will cause a ValueError exception to be thrown,
with the .value attribute of the exception containing the bad value.
===================== ================================================================== ===================== ==================================================================
@@ -835,17 +849,53 @@ control = (
WXK_CTRL_X, WXK_CTRL_Z WXK_CTRL_X, WXK_CTRL_Z
) )
# Because unicode can go over the ansi character range, we need to explicitly test
# for all non-visible keystrokes, rather than just assuming a particular range for
# visible characters:
wx_control_keycodes = range(32) + list(nav) + list(control) + [
wx.WXK_START, wx.WXK_LBUTTON, wx.WXK_RBUTTON, wx.WXK_CANCEL, wx.WXK_MBUTTON,
wx.WXK_CLEAR, wx.WXK_SHIFT, wx.WXK_CONTROL, wx.WXK_MENU, wx.WXK_PAUSE,
wx.WXK_CAPITAL, wx.WXK_SELECT, wx.WXK_PRINT, wx.WXK_EXECUTE, wx.WXK_SNAPSHOT,
wx.WXK_HELP, wx.WXK_NUMPAD0, wx.WXK_NUMPAD1, wx.WXK_NUMPAD2, wx.WXK_NUMPAD3,
wx.WXK_NUMPAD4, wx.WXK_NUMPAD5, wx.WXK_NUMPAD6, wx.WXK_NUMPAD7, wx.WXK_NUMPAD8,
wx.WXK_NUMPAD9, wx.WXK_MULTIPLY, wx.WXK_ADD, wx.WXK_SEPARATOR, wx.WXK_SUBTRACT,
wx.WXK_DECIMAL, wx.WXK_DIVIDE, wx.WXK_F1, wx.WXK_F2, wx.WXK_F3, wx.WXK_F4,
wx.WXK_F5, wx.WXK_F6, wx.WXK_F7, wx.WXK_F8, wx.WXK_F9, wx.WXK_F10, wx.WXK_F11,
wx.WXK_F12, wx.WXK_F13, wx.WXK_F14, wx.WXK_F15, wx.WXK_F16, wx.WXK_F17,
wx.WXK_F18, wx.WXK_F19, wx.WXK_F20, wx.WXK_F21, wx.WXK_F22, wx.WXK_F23,
wx.WXK_F24, wx.WXK_NUMLOCK, wx.WXK_SCROLL, wx.WXK_PAGEUP, wx.WXK_PAGEDOWN,
wx.WXK_NUMPAD_SPACE, wx.WXK_NUMPAD_TAB, wx.WXK_NUMPAD_ENTER, wx.WXK_NUMPAD_F1,
wx.WXK_NUMPAD_F2, wx.WXK_NUMPAD_F3, wx.WXK_NUMPAD_F4, wx.WXK_NUMPAD_HOME,
wx.WXK_NUMPAD_LEFT, wx.WXK_NUMPAD_UP, wx.WXK_NUMPAD_RIGHT, wx.WXK_NUMPAD_DOWN,
wx.WXK_NUMPAD_PRIOR, wx.WXK_NUMPAD_PAGEUP, wx.WXK_NUMPAD_NEXT, wx.WXK_NUMPAD_PAGEDOWN,
wx.WXK_NUMPAD_END, wx.WXK_NUMPAD_BEGIN, wx.WXK_NUMPAD_INSERT, wx.WXK_NUMPAD_DELETE,
wx.WXK_NUMPAD_EQUAL, wx.WXK_NUMPAD_MULTIPLY, wx.WXK_NUMPAD_ADD, wx.WXK_NUMPAD_SEPARATOR,
wx.WXK_NUMPAD_SUBTRACT, wx.WXK_NUMPAD_DECIMAL, wx.WXK_NUMPAD_DIVIDE, wx.WXK_WINDOWS_LEFT,
wx.WXK_WINDOWS_RIGHT, wx.WXK_WINDOWS_MENU, wx.WXK_COMMAND,
# Hardware-specific buttons
wx.WXK_SPECIAL1, wx.WXK_SPECIAL2, wx.WXK_SPECIAL3, wx.WXK_SPECIAL4, wx.WXK_SPECIAL5,
wx.WXK_SPECIAL6, wx.WXK_SPECIAL7, wx.WXK_SPECIAL8, wx.WXK_SPECIAL9, wx.WXK_SPECIAL10,
wx.WXK_SPECIAL11, wx.WXK_SPECIAL12, wx.WXK_SPECIAL13, wx.WXK_SPECIAL14, wx.WXK_SPECIAL15,
wx.WXK_SPECIAL16, wx.WXK_SPECIAL17, wx.WXK_SPECIAL18, wx.WXK_SPECIAL19, wx.WXK_SPECIAL20
]
## ---------- ---------- ---------- ---------- ---------- ---------- ---------- ## ---------- ---------- ---------- ---------- ---------- ---------- ----------
## Constants for masking. This is where mask characters ## Constants for masking. This is where mask characters
## are defined. ## are defined.
## maskchars used to identify valid mask characters from all others ## maskchars used to identify valid mask characters from all others
## #- allow numeric 0-9 only ## # - allow numeric 0-9 only
## A- allow uppercase only. Combine with forceupper to force lowercase to upper ## A - allow uppercase only. Combine with forceupper to force lowercase to upper
## a- allow lowercase only. Combine with forcelower to force upper to lowercase ## a - allow lowercase only. Combine with forcelower to force upper to lowercase
## X- allow any character (string.letters, string.punctuation, string.digits) ## C - allow any letter, upper or lower
## X - allow string.letters, string.punctuation, string.digits
## & - allow string.punctuation only (doesn't include all unicode symbols)
## * - allow any visible character
## Note: locale settings affect what "uppercase", lowercase, etc comprise. ## Note: locale settings affect what "uppercase", lowercase, etc comprise.
## Note: '|' is not a maskchar, in that it is a mask processing directive, and so
## does not appear here.
## ##
maskchars = ("#","A","a","X","C","N",'*','&') maskchars = ("#","A","a","X","C","N",'*','&')
ansichars = "" ansichars = ""
@@ -1272,12 +1322,13 @@ class Field:
'validRequired': False, ## Set to True to disallow input that results in an invalid value 'validRequired': False, ## Set to True to disallow input that results in an invalid value
'emptyInvalid': False, ## Set to True to make EMPTY = INVALID 'emptyInvalid': False, ## Set to True to make EMPTY = INVALID
'description': "", ## primarily for autoformats, but could be useful elsewhere 'description': "", ## primarily for autoformats, but could be useful elsewhere
'raiseOnInvalidPaste': False, ## if True, paste into field will cause ValueError
} }
# This list contains all parameters that when set at the control level should # This list contains all parameters that when set at the control level should
# propagate down to each field: # propagate down to each field:
propagating_params = ('fillChar', 'groupChar', 'decimalChar','useParensForNegatives', propagating_params = ('fillChar', 'groupChar', 'decimalChar','useParensForNegatives',
'compareNoCase', 'emptyInvalid', 'validRequired') 'compareNoCase', 'emptyInvalid', 'validRequired', 'raiseOnInvalidPaste')
def __init__(self, **kwargs): def __init__(self, **kwargs):
""" """
@@ -1289,7 +1340,9 @@ class Field:
for key in kwargs.keys(): for key in kwargs.keys():
if key not in Field.valid_params.keys(): if key not in Field.valid_params.keys():
#### dbg(indent=0) #### dbg(indent=0)
raise TypeError('invalid parameter "%s"' % (key)) ae = AttributeError('invalid parameter "%s"' % (key))
ae.attribute = key
raise ae
# Set defaults for each parameter for this instance, and fully # Set defaults for each parameter for this instance, and fully
# populate initial parameter list for configuration: # populate initial parameter list for configuration:
@@ -1316,7 +1369,9 @@ class Field:
for key in kwargs.keys(): for key in kwargs.keys():
if key not in Field.valid_params.keys(): if key not in Field.valid_params.keys():
## dbg(indent=0, suspend=0) ## dbg(indent=0, suspend=0)
raise AttributeError('invalid keyword argument "%s"' % key) ae = AttributeError('invalid keyword argument "%s"' % key)
ae.attribute = key
raise ae
## if self._index is not None: dbg('field index:', self._index) ## if self._index is not None: dbg('field index:', self._index)
## dbg('parameters:', indent=1) ## dbg('parameters:', indent=1)
@@ -1384,7 +1439,9 @@ class Field:
# Verify proper numeric format params: # Verify proper numeric format params:
if self._groupdigits and self._groupChar == self._decimalChar: if self._groupdigits and self._groupChar == self._decimalChar:
## dbg(indent=0, suspend=0) ## dbg(indent=0, suspend=0)
raise AttributeError("groupChar '%s' cannot be the same as decimalChar '%s'" % (self._groupChar, self._decimalChar)) ae = AttributeError("groupChar '%s' cannot be the same as decimalChar '%s'" % (self._groupChar, self._decimalChar))
ae.attribute = self._groupChar
raise ae
# Now go do validation, semantic and inter-dependency parameter processing: # Now go do validation, semantic and inter-dependency parameter processing:
@@ -1457,7 +1514,9 @@ class Field:
continue continue
if not self.IsValid(choice): if not self.IsValid(choice):
## dbg(indent=0, suspend=0) ## dbg(indent=0, suspend=0)
raise ValueError('%s: "%s" is not a valid value for the control as specified.' % (str(self._index), choice)) ve = ValueError('%s: "%s" is not a valid value for the control as specified.' % (str(self._index), choice))
ve.value = choice
raise ve
self._hasList = True self._hasList = True
#### dbg("kwargs.has_key('fillChar')?", kwargs.has_key('fillChar'), "len(self._choices) > 0?", len(self._choices) > 0) #### dbg("kwargs.has_key('fillChar')?", kwargs.has_key('fillChar'), "len(self._choices) > 0?", len(self._choices) > 0)
@@ -1742,7 +1801,7 @@ class MaskedEditMixin:
'C': string.letters, 'C': string.letters,
'N': string.letters + string.digits, 'N': string.letters + string.digits,
'&': string.punctuation, '&': string.punctuation,
'*': ansichars '*': ansichars # to give it a value, but now allows any non-wxcontrol character
} }
## self._ignoreChange is used by MaskedComboBox, because ## self._ignoreChange is used by MaskedComboBox, because
@@ -1790,7 +1849,9 @@ class MaskedEditMixin:
key = key.replace('Color', 'Colour') # for b-c, and standard wxPython spelling key = key.replace('Color', 'Colour') # for b-c, and standard wxPython spelling
if key not in MaskedEditMixin.valid_ctrl_params.keys() + Field.valid_params.keys(): if key not in MaskedEditMixin.valid_ctrl_params.keys() + Field.valid_params.keys():
## dbg(indent=0, suspend=0) ## dbg(indent=0, suspend=0)
raise TypeError('Invalid keyword argument "%s" for control "%s"' % (key, self.name)) ae = AttributeError('Invalid keyword argument "%s" for control "%s"' % (key, self.name))
ae.attribute = key
raise ae
elif key in Field.valid_params.keys(): elif key in Field.valid_params.keys():
constraint_kwargs[key] = value constraint_kwargs[key] = value
else: else:
@@ -1818,7 +1879,9 @@ class MaskedEditMixin:
constraint_kwargs[param] = value constraint_kwargs[param] = value
elif autoformat and not autoformat in masktags.keys(): elif autoformat and not autoformat in masktags.keys():
raise AttributeError('invalid value for autoformat parameter: %s' % repr(autoformat)) ae = AttributeError('invalid value for autoformat parameter: %s' % repr(autoformat))
ae.attribute = autoformat
raise ae
else: else:
## dbg('autoformat not selected') ## dbg('autoformat not selected')
if kwargs.has_key('mask'): if kwargs.has_key('mask'):
@@ -1847,18 +1910,18 @@ class MaskedEditMixin:
field = fields[i] field = fields[i]
if not isinstance(field, Field): if not isinstance(field, Field):
## dbg(indent=0, suspend=0) ## dbg(indent=0, suspend=0)
raise AttributeError('invalid type for field parameter: %s' % repr(field)) raise TypeError('invalid type for field parameter: %s' % repr(field))
self._fields[i] = field self._fields[i] = field
elif type(fields) == types.DictionaryType: elif type(fields) == types.DictionaryType:
for index, field in fields.items(): for index, field in fields.items():
if not isinstance(field, Field): if not isinstance(field, Field):
## dbg(indent=0, suspend=0) ## dbg(indent=0, suspend=0)
raise AttributeError('invalid type for field parameter: %s' % repr(field)) raise TypeError('invalid type for field parameter: %s' % repr(field))
self._fields[index] = field self._fields[index] = field
else: else:
## dbg(indent=0, suspend=0) ## dbg(indent=0, suspend=0)
raise AttributeError('fields parameter must be a list or dictionary; not %s' % repr(fields)) raise TypeError('fields parameter must be a list or dictionary; not %s' % repr(fields))
# Assign constraint parameters for entire control: # Assign constraint parameters for entire control:
#### dbg('control constraints:', indent=1) #### dbg('control constraints:', indent=1)
@@ -2055,7 +2118,9 @@ class MaskedEditMixin:
parameters.) parameters.)
""" """
if field_index not in self._field_indices: if field_index not in self._field_indices:
raise IndexError('%s is not a valid field for control "%s".' % (str(field_index), self.name)) ie = IndexError('%s is not a valid field for control "%s".' % (str(field_index), self.name))
ie.index = field_index
raise ie
# set parameters as requested: # set parameters as requested:
self._fields[field_index]._SetParameters(**kwargs) self._fields[field_index]._SetParameters(**kwargs)
@@ -2096,11 +2161,15 @@ class MaskedEditMixin:
Routine provided for getting a parameter of an individual field. Routine provided for getting a parameter of an individual field.
""" """
if field_index not in self._field_indices: if field_index not in self._field_indices:
raise IndexError('%s is not a valid field for control "%s".' % (str(field_index), self.name)) ie = IndexError('%s is not a valid field for control "%s".' % (str(field_index), self.name))
ie.index = field_index
raise ie
elif Field.valid_params.has_key(paramname): elif Field.valid_params.has_key(paramname):
return self._fields[field_index]._GetParameter(paramname) return self._fields[field_index]._GetParameter(paramname)
else: else:
TypeError('"%s".GetFieldParameter: invalid parameter "%s"' % (self.name, paramname)) ae = AttributeError('"%s".GetFieldParameter: invalid parameter "%s"' % (self.name, paramname))
ae.attribute = paramname
raise ae
def _SetKeycodeHandler(self, keycode, func): def _SetKeycodeHandler(self, keycode, func):
@@ -2208,9 +2277,13 @@ class MaskedEditMixin:
s += ' ' s += ' '
self._ctrl_constraints._defaultValue += ' ' self._ctrl_constraints._defaultValue += ' '
# Now, go build up a dictionary of booleans, indexed by position, # Now, go build up a dictionary of booleans, indexed by position,
# indicating whether or not a given position is masked or not # indicating whether or not a given position is masked or not.
# Also, strip out any '|' chars, adjusting the mask as necessary,
# marking the appropriate positions for field boundaries:
ismasked = {} ismasked = {}
explicit_field_boundaries = []
i = 0 i = 0
while i < len(s): while i < len(s):
if s[i] == '\\': # if escaped character: if s[i] == '\\': # if escaped character:
@@ -2220,14 +2293,19 @@ class MaskedEditMixin:
if i+2 < len(s) and s[i+1] == '\\': if i+2 < len(s) and s[i+1] == '\\':
# if next char also a '\', char is a literal '\' # if next char also a '\', char is a literal '\'
s = s[:i] + s[i+1:] # elide the 2nd '\' as well s = s[:i] + s[i+1:] # elide the 2nd '\' as well
i += 1 # increment to next char
elif s[i] == '|':
s = s[:i] + s[i+1:] # elide the '|'
explicit_field_boundaries.append(i)
# keep index where it is:
else: # else if special char, mark position accordingly else: # else if special char, mark position accordingly
ismasked[i] = s[i] in maskchars ismasked[i] = s[i] in maskchars
#### dbg('ismasked[%d]:' % i, ismasked[i], s) #### dbg('ismasked[%d]:' % i, ismasked[i], s)
i += 1 # increment to next char i += 1 # increment to next char
#### dbg('ismasked:', ismasked) #### dbg('ismasked:', ismasked)
## dbg('new mask: "%s"' % s, indent=0) ## dbg('new mask: "%s"' % s, indent=0)
return s, ismasked return s, ismasked, explicit_field_boundaries
def _calcFieldExtents(self): def _calcFieldExtents(self):
@@ -2311,6 +2389,8 @@ class MaskedEditMixin:
while i < len(self._mask) and self._isMaskChar(i): while i < len(self._mask) and self._isMaskChar(i):
self._lookupField[i] = field_index self._lookupField[i] = field_index
i += 1 i += 1
if i in self._explicit_field_boundaries:
break
#### dbg('edit_end =', i) #### dbg('edit_end =', i)
edit_end = i edit_end = i
self._lookupField[i] = field_index self._lookupField[i] = field_index
@@ -2328,6 +2408,7 @@ class MaskedEditMixin:
mask=self._mask[edit_start:edit_end]) mask=self._mask[edit_start:edit_end])
pos = i pos = i
i = self._findNextEntry(pos, adjustInsert=False) # go to next field: i = self._findNextEntry(pos, adjustInsert=False) # go to next field:
#### dbg('next entry:', i)
if i > pos: if i > pos:
for j in range(pos, i+1): for j in range(pos, i+1):
self._lookupField[j] = field_index self._lookupField[j] = field_index
@@ -2348,7 +2429,10 @@ class MaskedEditMixin:
# Verify that all field indices specified are valid for mask: # Verify that all field indices specified are valid for mask:
for index in self._fields.keys(): for index in self._fields.keys():
if index not in [-1] + self._lookupField.values(): if index not in [-1] + self._lookupField.values():
raise IndexError('field %d is not a valid field for mask "%s"' % (index, self._mask)) ie = IndexError('field %d is not a valid field for mask "%s"' % (index, self._mask))
ie.index = index
raise ie
def _calcTemplate(self, reset_fillchar, reset_default): def _calcTemplate(self, reset_fillchar, reset_default):
@@ -2452,7 +2536,9 @@ class MaskedEditMixin:
## dbg('self._defaultValue:', self._defaultValue) ## dbg('self._defaultValue:', self._defaultValue)
if not self.IsEmpty(self._defaultValue) and not self.IsValid(self._defaultValue): if not self.IsEmpty(self._defaultValue) and not self.IsValid(self._defaultValue):
#### dbg(indent=0) #### dbg(indent=0)
raise ValueError('Default value of "%s" is not a valid value for control "%s"' % (self._defaultValue, self.name)) ve = ValueError('Default value of "%s" is not a valid value for control "%s"' % (self._defaultValue, self.name))
ve.value = self._defaultValue
raise ve
# if no fillchar change, but old value == old template, replace it: # if no fillchar change, but old value == old template, replace it:
if newvalue == old_template: if newvalue == old_template:
@@ -2540,10 +2626,17 @@ class MaskedEditMixin:
valid_paste, ignore, replace_to = self._validatePaste(choice, start, end) valid_paste, ignore, replace_to = self._validatePaste(choice, start, end)
if not valid_paste: if not valid_paste:
#### dbg(indent=0) #### dbg(indent=0)
raise ValueError('"%s" could not be entered into field %d of control "%s"' % (choice, index, self.name)) ve = ValueError('"%s" could not be entered into field %d of control "%s"' % (choice, index, self.name))
ve.value = choice
ve.index = index
raise ve
elif replace_to > end: elif replace_to > end:
#### dbg(indent=0) #### dbg(indent=0)
raise ValueError('"%s" will not fit into field %d of control "%s"' (choice, index, self.name)) ve = ValueError('"%s" will not fit into field %d of control "%s"' (choice, index, self.name))
ve.value = choice
ve.index = index
raise ve
#### dbg(choice, 'valid in field', index) #### dbg(choice, 'valid in field', index)
@@ -2568,7 +2661,7 @@ class MaskedEditMixin:
# Preprocess specified mask to expand {n} syntax, handle escaped # Preprocess specified mask to expand {n} syntax, handle escaped
# mask characters, etc and build the resulting positionally keyed # mask characters, etc and build the resulting positionally keyed
# dictionary for which positions are mask vs. template characters: # dictionary for which positions are mask vs. template characters:
self._mask, self.ismasked = self._processMask(mask) self._mask, self._ismasked, self._explicit_field_boundaries = self._processMask(mask)
self._masklength = len(self._mask) self._masklength = len(self._mask)
#### dbg('processed mask:', self._mask) #### dbg('processed mask:', self._mask)
@@ -2883,13 +2976,6 @@ class MaskedEditMixin:
## dbg("field length exceeded:",pos) ## dbg("field length exceeded:",pos)
keep_processing = False keep_processing = False
if keep_processing:
if self._isMaskChar(pos): ## Get string of allowed characters for validation
okchars = self._getAllowedChars(pos)
else:
## dbg('Not a valid position: pos = ', pos,"chars=",maskchars)
okchars = ""
key = self._adjustKey(pos, key) # apply formatting constraints to key: key = self._adjustKey(pos, key) # apply formatting constraints to key:
if self._keyhandlers.has_key(key): if self._keyhandlers.has_key(key):
@@ -2902,30 +2988,35 @@ class MaskedEditMixin:
## dbg(indent=0) ## dbg(indent=0)
return return
# else skip default processing, but do final formatting # else skip default processing, but do final formatting
if key < wx.WXK_SPACE or key > 255: if key in wx_control_keycodes:
## dbg('key < WXK_SPACE or key > 255') ## dbg('key in wx_control_keycodes')
event.Skip() # non alphanumeric event.Skip() # non-printable; let base control handle it
keep_processing = False keep_processing = False
else: else:
field = self._FindField(pos) field = self._FindField(pos)
## dbg("key ='%s'" % chr(key))
if chr(key) == ' ':
## dbg('okSpaces?', field._okSpaces)
pass
char = chr(key) # (must work if we got this far)
if 'unicode' in wx.PlatformInfo: if 'unicode' in wx.PlatformInfo:
char = char.decode(self._defaultEncoding) if key < 256:
char = chr(key) # (must work if we got this far)
char = char.decode(self._defaultEncoding)
else:
char = unichr(event.GetUnicodeKey())
dbg('unicode char:', char)
excludes = u'' excludes = u''
if type(field._excludeChars) != types.UnicodeType: if type(field._excludeChars) != types.UnicodeType:
excludes += field._excludeChars.decode(self._defaultEncoding) excludes += field._excludeChars.decode(self._defaultEncoding)
if type(self._ctrl_constraints) != types.UnicodeType: if type(self._ctrl_constraints) != types.UnicodeType:
excludes += self._ctrl_constraints._excludeChars.decode(self._defaultEncoding) excludes += self._ctrl_constraints._excludeChars.decode(self._defaultEncoding)
else: else:
char = chr(key) # (must work if we got this far)
excludes = field._excludeChars + self._ctrl_constraints._excludeChars excludes = field._excludeChars + self._ctrl_constraints._excludeChars
## dbg("key ='%s'" % chr(key))
if chr(key) == ' ':
## dbg('okSpaces?', field._okSpaces)
pass
if char in excludes: if char in excludes:
keep_processing = False keep_processing = False
@@ -3910,7 +4001,8 @@ class MaskedEditMixin:
def _findNextEntry(self,pos, adjustInsert=True): def _findNextEntry(self,pos, adjustInsert=True):
""" Find the insertion point for the next valid entry character position.""" """ Find the insertion point for the next valid entry character position."""
if self._isTemplateChar(pos): # if changing fields, pay attn to flag ## dbg('MaskedEditMixin::_findNextEntry', indent=1)
if self._isTemplateChar(pos) or pos in self._explicit_field_boundaries: # if changing fields, pay attn to flag
adjustInsert = adjustInsert adjustInsert = adjustInsert
else: # else within a field; flag not relevant else: # else within a field; flag not relevant
adjustInsert = False adjustInsert = False
@@ -3927,6 +4019,7 @@ class MaskedEditMixin:
slice = self._GetValue()[start:end] slice = self._GetValue()[start:end]
if field._insertRight and field.IsEmpty(slice): if field._insertRight and field.IsEmpty(slice):
pos = end pos = end
## dbg('final pos:', pos, indent=0)
return pos return pos
@@ -4447,7 +4540,7 @@ class MaskedEditMixin:
""" Returns True if the char at position pos is a special mask character (e.g. NCXaA#) """ Returns True if the char at position pos is a special mask character (e.g. NCXaA#)
""" """
if pos < self._masklength: if pos < self._masklength:
return self.ismasked[pos] return self._ismasked[pos]
else: else:
return False return False
@@ -4533,7 +4626,7 @@ class MaskedEditMixin:
okChars += ')' okChars += ')'
#### dbg('%s in %s?' % (char, okChars), char in okChars) #### dbg('%s in %s?' % (char, okChars), char in okChars)
approved = char in okChars approved = (self.maskdict[pos] == '*' or char in okChars)
if approved and checkRegex: if approved and checkRegex:
## dbg("checking appropriate regex's") ## dbg("checking appropriate regex's")
@@ -5558,9 +5651,13 @@ class MaskedEditMixin:
if raise_on_invalid: if raise_on_invalid:
## dbg(indent=0, suspend=0) ## dbg(indent=0, suspend=0)
if item == 'control': if item == 'control':
raise ValueError('"%s" will not fit into the control "%s"' % (paste_text, self.name)) ve = ValueError('"%s" will not fit into the control "%s"' % (paste_text, self.name))
ve.value = paste_text
raise ve
else: else:
raise ValueError('"%s" will not fit into the selection' % paste_text) ve = ValueError('"%s" will not fit into the selection' % paste_text)
ve.value = paste_text
raise ve
else: else:
## dbg(indent=0, suspend=0) ## dbg(indent=0, suspend=0)
return False, None, None return False, None, None
@@ -5615,13 +5712,18 @@ class MaskedEditMixin:
if not valid_paste and raise_on_invalid: if not valid_paste and raise_on_invalid:
## dbg('raising exception', indent=0, suspend=0) ## dbg('raising exception', indent=0, suspend=0)
raise ValueError('"%s" cannot be inserted into the control "%s"' % (paste_text, self.name)) ve = ValueError('"%s" cannot be inserted into the control "%s"' % (paste_text, self.name))
ve.value = paste_text
raise ve
elif i < len(paste_text): elif i < len(paste_text):
valid_paste = False valid_paste = False
if raise_on_invalid: if raise_on_invalid:
## dbg('raising exception', indent=0, suspend=0) ## dbg('raising exception', indent=0, suspend=0)
raise ValueError('"%s" will not fit into the control "%s"' % (paste_text, self.name)) ve = ValueError('"%s" will not fit into the control "%s"' % (paste_text, self.name))
ve.value = paste_text
raise ve
## dbg('valid_paste?', valid_paste) ## dbg('valid_paste?', valid_paste)
if valid_paste: if valid_paste:
@@ -5735,6 +5837,7 @@ class MaskedEditMixin:
sel_start = 0 sel_start = 0
## dbg('adjusted selection:', (sel_start, sel_to)) ## dbg('adjusted selection:', (sel_start, sel_to))
raise_on_invalid = raise_on_invalid or field._raiseOnInvalidPaste
try: try:
valid_paste, replacement_text, replace_to = self._validatePaste(paste_text, sel_start, sel_to, raise_on_invalid) valid_paste, replacement_text, replace_to = self._validatePaste(paste_text, sel_start, sel_to, raise_on_invalid)
except: except:
@@ -6599,6 +6702,19 @@ __i=0
## CHANGELOG: ## CHANGELOG:
## ==================== ## ====================
## Version 1.11
## 1. Added value member to ValueError exceptions, so that people can catch them
## and then display their own errors, and added attribute raiseOnInvalidPaste,
## so one doesn't have to subclass the controls simply to force generation of
## a ValueError on a bad paste operation.
## 2. Fixed handling of unicode charsets by converting to explicit control char
## set testing for passing those keystrokes to the base control, and then
## changing the semantics of the * maskchar to indicate any visible char.
## 3. Added '|' mask specification character, which allows splitting of contiguous
## mask characters into separate fields, allowing finer control of behavior
## of a control.
##
##
## Version 1.10 ## Version 1.10
## 1. Added handling for WXK_DELETE and WXK_INSERT, such that shift-delete ## 1. Added handling for WXK_DELETE and WXK_INSERT, such that shift-delete
## cuts, shift-insert pastes, and ctrl-insert copies. ## cuts, shift-insert pastes, and ctrl-insert copies.