X-Git-Url: https://git.saurik.com/wxWidgets.git/blobdiff_plain/281c105455c4e39672b5527e9f66de53ab163aec..27ee942feb598fc96f790d62bb8aac6d8c97cce5:/wxPython/wx/lib/masked/maskededit.py diff --git a/wxPython/wx/lib/masked/maskededit.py b/wxPython/wx/lib/masked/maskededit.py index be2753eb8b..b5696c7e1a 100644 --- a/wxPython/wx/lib/masked/maskededit.py +++ b/wxPython/wx/lib/masked/maskededit.py @@ -1,10 +1,10 @@ #---------------------------------------------------------------------------- # Name: maskededit.py # 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 # 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$ # License: wxWindows license #---------------------------------------------------------------------------- @@ -50,7 +50,9 @@ # o wxTimeCtrl -> TimeCtrl # -"""\ +__doc__ = """\ +contains MaskedEditMixin class that drives all the other masked controls. + ==================== Masked Edit Overview ==================== @@ -88,11 +90,11 @@ masked.Ctrl: will be masked.TextCtrl. Each of the above classes has its own set of arguments, but masked.Ctrl - provides a single "unified" interface for masked controls. Those for - masked.TextCtrl, masked.ComboBox and masked.IpAddrCtrl are all documented - below; the others have their own demo pages and interface descriptions. - (See end of following discussion for how to configure the wx.MaskedCtrl() - to select the above control types.) + provides a single "unified" interface for masked controls. + +What follows is a description of how to configure the generic masked.TextCtrl +and masked.ComboBox; masked.NumCtrl and masked.TimeCtrl have their own demo +pages and interface descriptions. ========================= @@ -110,8 +112,12 @@ mask a Allow lowercase letters only C Allow any letter, upper or lower X Allow string.letters, string.punctuation, string.digits - & Allow string.punctuation only - \* Allow any ansi character + & Allow string.punctuation only (doesn't include all unicode symbols) + \* 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 locale.setlocale(locale.LC_ALL, '') - The controls now also support (by popular demand) all "ansi" chars, - that is, all ascii codes between 32 and 255, by use of the * mask character. + The controls now also support (by popular demand) all "visible" characters, + 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 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, you can supply one to provide data transfer routines for the 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. ===================== ================================================================== @@ -696,6 +710,8 @@ Event Handling self._SetKeycodeHandler(WXK_UP, self.IncrementValue) self._SetKeyHandler('-', self._OnChangeSign) + (Setting a func of None removes any keyhandler for the given key.) + "Navigation" keys are assumed to change the cursor position, and therefore don't cause automatic motion of the cursor as insertable characters do. @@ -825,25 +841,65 @@ WXK_CTRL_Z = (ord('Z')+1) - ord('A') nav = ( wx.WXK_BACK, wx.WXK_LEFT, wx.WXK_RIGHT, wx.WXK_UP, wx.WXK_DOWN, wx.WXK_TAB, - wx.WXK_HOME, wx.WXK_END, wx.WXK_RETURN, wx.WXK_PRIOR, wx.WXK_NEXT + wx.WXK_HOME, wx.WXK_END, wx.WXK_RETURN, wx.WXK_PRIOR, wx.WXK_NEXT, + wx.WXK_NUMPAD_LEFT, wx.WXK_NUMPAD_RIGHT, wx.WXK_NUMPAD_UP, wx.WXK_NUMPAD_DOWN, + wx.WXK_NUMPAD_HOME, wx.WXK_NUMPAD_END, wx.WXK_NUMPAD_ENTER, wx.WXK_NUMPAD_PRIOR, wx.WXK_NUMPAD_NEXT ) control = ( - wx.WXK_BACK, wx.WXK_DELETE, WXK_CTRL_A, WXK_CTRL_C, WXK_CTRL_S, WXK_CTRL_V, + wx.WXK_BACK, wx.WXK_DELETE, wx.WXK_INSERT, + wx.WXK_NUMPAD_DELETE, wx.WXK_NUMPAD_INSERT, + WXK_CTRL_A, WXK_CTRL_C, WXK_CTRL_S, WXK_CTRL_V, 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 ## are defined. ## maskchars used to identify valid mask characters from all others -## #- allow numeric 0-9 only -## A- allow uppercase only. Combine with forceupper to force lowercase to upper -## a- allow lowercase only. Combine with forcelower to force upper to lowercase -## X- allow any character (string.letters, string.punctuation, string.digits) +## # - allow numeric 0-9 only +## A - allow uppercase only. Combine with forceupper to force lowercase to upper +## a - allow lowercase only. Combine with forcelower to force upper to lowercase +## 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: '|' is not a maskchar, in that it is a mask processing directive, and so +## does not appear here. ## maskchars = ("#","A","a","X","C","N",'*','&') ansichars = "" @@ -1270,12 +1326,13 @@ class Field: 'validRequired': False, ## Set to True to disallow input that results in an invalid value 'emptyInvalid': False, ## Set to True to make EMPTY = INVALID '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 # propagate down to each field: propagating_params = ('fillChar', 'groupChar', 'decimalChar','useParensForNegatives', - 'compareNoCase', 'emptyInvalid', 'validRequired') + 'compareNoCase', 'emptyInvalid', 'validRequired', 'raiseOnInvalidPaste') def __init__(self, **kwargs): """ @@ -1287,7 +1344,9 @@ class Field: for key in kwargs.keys(): if key not in Field.valid_params.keys(): #### 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 # populate initial parameter list for configuration: @@ -1314,7 +1373,9 @@ class Field: for key in kwargs.keys(): if key not in Field.valid_params.keys(): ## 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) ## dbg('parameters:', indent=1) @@ -1382,7 +1443,9 @@ class Field: # Verify proper numeric format params: if self._groupdigits and self._groupChar == self._decimalChar: ## 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: @@ -1455,7 +1518,9 @@ class Field: continue if not self.IsValid(choice): ## 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 #### dbg("kwargs.has_key('fillChar')?", kwargs.has_key('fillChar'), "len(self._choices) > 0?", len(self._choices) > 0) @@ -1700,26 +1765,39 @@ class MaskedEditMixin: ## Initially populated with navigation and function control keys: self._keyhandlers = { # default navigation keys and handlers: - wx.WXK_BACK: self._OnErase, - wx.WXK_LEFT: self._OnArrow, - wx.WXK_RIGHT: self._OnArrow, - wx.WXK_UP: self._OnAutoCompleteField, - wx.WXK_DOWN: self._OnAutoCompleteField, - wx.WXK_TAB: self._OnChangeField, - wx.WXK_HOME: self._OnHome, - wx.WXK_END: self._OnEnd, - wx.WXK_RETURN: self._OnReturn, - wx.WXK_PRIOR: self._OnAutoCompleteField, - wx.WXK_NEXT: self._OnAutoCompleteField, + wx.WXK_BACK: self._OnErase, + wx.WXK_LEFT: self._OnArrow, + wx.WXK_NUMPAD_LEFT: self._OnArrow, + wx.WXK_RIGHT: self._OnArrow, + wx.WXK_NUMPAD_RIGHT: self._OnArrow, + wx.WXK_UP: self._OnAutoCompleteField, + wx.WXK_NUMPAD_UP: self._OnAutoCompleteField, + wx.WXK_DOWN: self._OnAutoCompleteField, + wx.WXK_NUMPAD_DOWN: self._OnAutoCompleteField, + wx.WXK_TAB: self._OnChangeField, + wx.WXK_HOME: self._OnHome, + wx.WXK_NUMPAD_HOME: self._OnHome, + wx.WXK_END: self._OnEnd, + wx.WXK_NUMPAD_END: self._OnEnd, + wx.WXK_RETURN: self._OnReturn, + wx.WXK_NUMPAD_ENTER: self._OnReturn, + wx.WXK_PRIOR: self._OnAutoCompleteField, + wx.WXK_NUMPAD_PRIOR: self._OnAutoCompleteField, + wx.WXK_NEXT: self._OnAutoCompleteField, + wx.WXK_NUMPAD_NEXT: self._OnAutoCompleteField, # default function control keys and handlers: - wx.WXK_DELETE: self._OnErase, - WXK_CTRL_A: self._OnCtrl_A, - WXK_CTRL_C: self._OnCtrl_C, - WXK_CTRL_S: self._OnCtrl_S, - WXK_CTRL_V: self._OnCtrl_V, - WXK_CTRL_X: self._OnCtrl_X, - WXK_CTRL_Z: self._OnCtrl_Z, + wx.WXK_DELETE: self._OnDelete, + wx.WXK_NUMPAD_DELETE: self._OnDelete, + wx.WXK_INSERT: self._OnInsert, + wx.WXK_NUMPAD_INSERT: self._OnInsert, + + WXK_CTRL_A: self._OnCtrl_A, + WXK_CTRL_C: self._OnCtrl_C, + WXK_CTRL_S: self._OnCtrl_S, + WXK_CTRL_V: self._OnCtrl_V, + WXK_CTRL_X: self._OnCtrl_X, + WXK_CTRL_Z: self._OnCtrl_Z, } ## bind standard navigational and control keycodes to this instance, @@ -1739,7 +1817,7 @@ class MaskedEditMixin: 'C': string.letters, 'N': string.letters + string.digits, '&': string.punctuation, - '*': ansichars + '*': ansichars # to give it a value, but now allows any non-wxcontrol character } ## self._ignoreChange is used by MaskedComboBox, because @@ -1787,7 +1865,9 @@ class MaskedEditMixin: 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(): ## 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(): constraint_kwargs[key] = value else: @@ -1815,7 +1895,9 @@ class MaskedEditMixin: constraint_kwargs[param] = value 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: ## dbg('autoformat not selected') if kwargs.has_key('mask'): @@ -1844,18 +1926,18 @@ class MaskedEditMixin: field = fields[i] if not isinstance(field, Field): ## 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 elif type(fields) == types.DictionaryType: for index, field in fields.items(): if not isinstance(field, Field): ## 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 else: ## 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: #### dbg('control constraints:', indent=1) @@ -1989,7 +2071,7 @@ class MaskedEditMixin: width = self.GetSize().width height = self.GetBestSize().height ## dbg('setting client size to:', (width, height)) - self.SetBestFittingSize((width, height)) + self.SetInitialSize((width, height)) # Set value/type-specific formatting self._applyFormatting() @@ -2052,7 +2134,9 @@ class MaskedEditMixin: parameters.) """ 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: self._fields[field_index]._SetParameters(**kwargs) @@ -2073,7 +2157,7 @@ class MaskedEditMixin: # the outside size that does include the borders. What you are # calculating (in _CalcSize) is the client size, but the sizers # deal with the full size and so that is the minimum size that - # we need to set with SetBestFittingSize. The root of the problem is + # we need to set with SetInitialSize. The root of the problem is # that in _calcSize the current client size height is returned, # instead of a height based on the current font. So I suggest using # _calcSize to just get the width, and then use GetBestSize to @@ -2081,7 +2165,7 @@ class MaskedEditMixin: self.SetClientSize(self._CalcSize()) width = self.GetSize().width height = self.GetBestSize().height - self.SetBestFittingSize((width, height)) + self.SetInitialSize((width, height)) # Set value/type-specific formatting @@ -2093,11 +2177,15 @@ class MaskedEditMixin: Routine provided for getting a parameter of an individual field. """ 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): return self._fields[field_index]._GetParameter(paramname) 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): @@ -2106,7 +2194,10 @@ class MaskedEditMixin: used by the control. should take the event as argument and return False if no further action on the key is necessary. """ - self._keyhandlers[keycode] = func + if func: + self._keyhandlers[keycode] = func + elif self._keyhandlers.has_key(keycode): + del self._keyhandlers[keycode] def _SetKeyHandler(self, char, func): @@ -2126,6 +2217,9 @@ class MaskedEditMixin: self._nav.append(keycode) if handler: self._keyhandlers[keycode] = handler + elif self.keyhandlers.has_key(keycode): + del self._keyhandlers[keycode] + def _AddNavKey(self, char, handler=None): @@ -2154,6 +2248,8 @@ class MaskedEditMixin: self._nav.append(keycode) if func: self._keyhandlers[keycode] = func + elif self.keyhandlers.has_key(keycode): + del self._keyhandlers[keycode] def _processMask(self, mask): @@ -2197,9 +2293,13 @@ class MaskedEditMixin: s += ' ' self._ctrl_constraints._defaultValue += ' ' + # 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 = {} + explicit_field_boundaries = [] i = 0 while i < len(s): if s[i] == '\\': # if escaped character: @@ -2209,14 +2309,19 @@ class MaskedEditMixin: if i+2 < len(s) and s[i+1] == '\\': # if next char also a '\', char is a literal '\' 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 ismasked[i] = s[i] in maskchars -#### dbg('ismasked[%d]:' % i, ismasked[i], s) - i += 1 # increment to next char +#### dbg('ismasked[%d]:' % i, ismasked[i], s) + i += 1 # increment to next char #### dbg('ismasked:', ismasked) ## dbg('new mask: "%s"' % s, indent=0) - return s, ismasked + return s, ismasked, explicit_field_boundaries def _calcFieldExtents(self): @@ -2300,6 +2405,8 @@ class MaskedEditMixin: while i < len(self._mask) and self._isMaskChar(i): self._lookupField[i] = field_index i += 1 + if i in self._explicit_field_boundaries: + break #### dbg('edit_end =', i) edit_end = i self._lookupField[i] = field_index @@ -2317,6 +2424,7 @@ class MaskedEditMixin: mask=self._mask[edit_start:edit_end]) pos = i i = self._findNextEntry(pos, adjustInsert=False) # go to next field: +#### dbg('next entry:', i) if i > pos: for j in range(pos, i+1): self._lookupField[j] = field_index @@ -2337,7 +2445,10 @@ class MaskedEditMixin: # Verify that all field indices specified are valid for mask: for index in self._fields.keys(): 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): @@ -2441,7 +2552,9 @@ class MaskedEditMixin: ## dbg('self._defaultValue:', self._defaultValue) if not self.IsEmpty(self._defaultValue) and not self.IsValid(self._defaultValue): #### 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 newvalue == old_template: @@ -2529,10 +2642,17 @@ class MaskedEditMixin: valid_paste, ignore, replace_to = self._validatePaste(choice, start, end) if not valid_paste: #### 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: #### 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) @@ -2557,7 +2677,7 @@ class MaskedEditMixin: # Preprocess specified mask to expand {n} syntax, handle escaped # mask characters, etc and build the resulting positionally keyed # 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) #### dbg('processed mask:', self._mask) @@ -2611,6 +2731,12 @@ class MaskedEditMixin: keycode = ord(key) if not self._keyhandlers.has_key(keycode): self._SetKeyHandler(key, self._OnChangeSign) + elif self._isInt or self._isFloat: + signkeys = ['-', '+', ' ', '(', ')'] + for key in signkeys: + keycode = ord(key) + if self._keyhandlers.has_key(keycode) and self._keyhandlers[keycode] == self._OnChangeSign: + self._SetKeyHandler(key, None) @@ -2670,18 +2796,23 @@ class MaskedEditMixin: #### dbg('Registering numeric navigation and control handlers (if not already set)') if not self._keyhandlers.has_key(wx.WXK_DOWN): self._SetKeycodeHandler(wx.WXK_DOWN, self._OnChangeField) + if not self._keyhandlers.has_key(wx.WXK_NUMPAD_DOWN): + self._SetKeycodeHandler(wx.WXK_DOWN, self._OnChangeField) if not self._keyhandlers.has_key(wx.WXK_UP): self._SetKeycodeHandler(wx.WXK_UP, self._OnUpNumeric) # (adds "shift" to up arrow, and calls _OnChangeField) + if not self._keyhandlers.has_key(wx.WXK_NUMPAD_UP): + self._SetKeycodeHandler(wx.WXK_UP, self._OnUpNumeric) # (adds "shift" to up arrow, and calls _OnChangeField) # On ., truncate contents right of cursor to decimal point (if any) - # leaves cusor after decimal point if floating point, otherwise at 0. - if not self._keyhandlers.has_key(ord(self._decimalChar)): + # leaves cursor after decimal point if floating point, otherwise at 0. + if not self._keyhandlers.has_key(ord(self._decimalChar)) or self._keyhandlers[ord(self._decimalChar)] != self._OnDecimalPoint: self._SetKeyHandler(self._decimalChar, self._OnDecimalPoint) - if not self._keyhandlers.has_key(ord(self._shiftDecimalChar)): + + if not self._keyhandlers.has_key(ord(self._shiftDecimalChar)) or self._keyhandlers[ord(self._shiftDecimalChar)] != self._OnChangeField: self._SetKeyHandler(self._shiftDecimalChar, self._OnChangeField) # (Shift-'.' == '>' on US keyboards) # Allow selective insert of groupchar in numbers: - if not self._keyhandlers.has_key(ord(self._fields[0]._groupChar)): + if not self._keyhandlers.has_key(ord(self._fields[0]._groupChar)) or self._keyhandlers[ord(self._fields[0]._groupChar)] != self._OnGroupChar: self._SetKeyHandler(self._fields[0]._groupChar, self._OnGroupChar) ## dbg(indent=0, suspend=0) @@ -2865,13 +2996,6 @@ class MaskedEditMixin: ## dbg("field length exceeded:",pos) 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: if self._keyhandlers.has_key(key): @@ -2884,30 +3008,35 @@ class MaskedEditMixin: ## dbg(indent=0) return # else skip default processing, but do final formatting - if key < wx.WXK_SPACE or key > 255: -## dbg('key < WXK_SPACE or key > 255') - event.Skip() # non alphanumeric + if key in wx_control_keycodes: +## dbg('key in wx_control_keycodes') + event.Skip() # non-printable; let base control handle it keep_processing = False else: 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: - 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'' if type(field._excludeChars) != types.UnicodeType: excludes += field._excludeChars.decode(self._defaultEncoding) if type(self._ctrl_constraints) != types.UnicodeType: excludes += self._ctrl_constraints._excludeChars.decode(self._defaultEncoding) else: + char = chr(key) # (must work if we got this far) excludes = field._excludeChars + self._ctrl_constraints._excludeChars +## dbg("key ='%s'" % chr(key)) + if chr(key) == ' ': +## dbg('okSpaces?', field._okSpaces) + pass + + if char in excludes: keep_processing = False @@ -3096,25 +3225,25 @@ class MaskedEditMixin: keycode = event.GetKeyCode() sel_start, sel_to = self._GetSelection() entry_end = self._goEnd(getPosOnly=True) - if keycode in (wx.WXK_RIGHT, wx.WXK_DOWN): + if keycode in (wx.WXK_RIGHT, wx.WXK_DOWN, wx.WXK_NUMPAD_RIGHT, wx.WXK_NUMPAD_DOWN): if( ( not self._isTemplateChar(pos) and pos+1 > entry_end) or ( self._isTemplateChar(pos) and pos >= entry_end) ): ## dbg("can't advance", indent=0) return False elif self._isTemplateChar(pos): self._AdjustField(pos) - elif keycode in (wx.WXK_LEFT,wx.WXK_UP) and sel_start == sel_to and pos > 0 and self._isTemplateChar(pos-1): + elif keycode in (wx.WXK_LEFT, wx.WXK_UP, wx.WXK_NUMPAD_LEFT, wx.WXK_NUMPAD_UP) and sel_start == sel_to and pos > 0 and self._isTemplateChar(pos-1): ## dbg('adjusting field') self._AdjustField(pos) # treat as shifted up/down arrows as tab/reverse tab: - if event.ShiftDown() and keycode in (wx.WXK_UP, wx.WXK_DOWN): + if event.ShiftDown() and keycode in (wx.WXK_UP, wx.WXK_DOWN, wx.WXK_NUMPAD_UP, wx.WXK_NUMPAD_DOWN): # remove "shifting" and treat as (forward) tab: event.m_shiftDown = False keep_processing = self._OnChangeField(event) elif self._FindField(pos)._selectOnFieldEntry: - if( keycode in (wx.WXK_UP, wx.WXK_LEFT) + if( keycode in (wx.WXK_UP, wx.WXK_LEFT, wx.WXK_NUMPAD_UP, wx.WXK_NUMPAD_LEFT) and sel_start != 0 and self._isTemplateChar(sel_start-1) and sel_start != self._masklength @@ -3125,7 +3254,7 @@ class MaskedEditMixin: event.m_shiftDown = True event.m_ControlDown = True keep_processing = self._OnChangeField(event) - elif( keycode in (wx.WXK_DOWN, wx.WXK_RIGHT) + elif( keycode in (wx.WXK_DOWN, wx.WXK_RIGHT, wx.WXK_NUMPAD_DOWN, wx.WXK_NUMPAD_RIGHT) and sel_to != self._masklength and self._isTemplateChar(sel_to)): @@ -3138,8 +3267,8 @@ class MaskedEditMixin: ## dbg('using base ctrl event processing') event.Skip() else: - if( (sel_to == self._fields[0]._extent[0] and keycode == wx.WXK_LEFT) - or (sel_to == self._masklength and keycode == wx.WXK_RIGHT) ): + if( (sel_to == self._fields[0]._extent[0] and keycode in (wx.WXK_LEFT, wx.WXK_NUMPAD_LEFT) ) + or (sel_to == self._masklength and keycode in (wx.WXK_RIGHT, wx.WXK_NUMPAD_RIGHT) ) ): if not wx.Validator_IsSilent(): wx.Bell() else: @@ -3185,6 +3314,32 @@ class MaskedEditMixin: ## dbg(indent=0) return False + def _OnInsert(self, event=None): + """ Handles shift-insert and control-insert operations (paste and copy, respectively)""" +## dbg("MaskedEditMixin::_OnInsert", indent=1) + if event and isinstance(event, wx.KeyEvent): + if event.ShiftDown(): + self.Paste() + elif event.ControlDown(): + self.Copy() + # (else do nothing) + # (else do nothing) +## dbg(indent=0) + return False + + def _OnDelete(self, event=None): + """ Handles shift-delete and delete operations (cut and erase, respectively)""" +## dbg("MaskedEditMixin::_OnDelete", indent=1) + if event and isinstance(event, wx.KeyEvent): + if event.ShiftDown(): + self.Cut() + else: + self._OnErase(event) + else: + self._OnErase(event) +## dbg(indent=0) + return False + def _OnCtrl_Z(self, event=None): """ Handles ctrl-Z keypress in control and Undo operation on context menu. Should return False to skip other processing. """ @@ -3223,11 +3378,11 @@ class MaskedEditMixin: # If trying to erase beyond "legal" bounds, disallow operation: if( (sel_to == 0 and key == wx.WXK_BACK) or (self._signOk and sel_to == 1 and value[0] == ' ' and key == wx.WXK_BACK) - or (sel_to == self._masklength and sel_start == sel_to and key == wx.WXK_DELETE and not field._insertRight) + or (sel_to == self._masklength and sel_start == sel_to and key in (wx.WXK_DELETE, wx.WXK_NUMPAD_DELETE) and not field._insertRight) or (self._signOk and self._useParens and sel_start == sel_to and sel_to == self._masklength - 1 - and value[sel_to] == ' ' and key == wx.WXK_DELETE and not field._insertRight) ): + and value[sel_to] == ' ' and key in (wx.WXK_DELETE, wx.WXK_NUMPAD_DELETE) and not field._insertRight) ): if not wx.Validator_IsSilent(): wx.Bell() ## dbg(indent=0) @@ -3239,7 +3394,7 @@ class MaskedEditMixin: and sel_start >= start # and selection starts in field and ((sel_to == sel_start # and no selection and sel_to == end # and cursor at right edge - and key in (wx.WXK_BACK, wx.WXK_DELETE)) # and either delete or backspace key + and key in (wx.WXK_BACK, wx.WXK_DELETE, wx.WXK_NUMPAD_DELETE)) # and either delete or backspace key or # or (key == wx.WXK_BACK # backspacing and (sel_to == end # and selection ends at right edge @@ -3784,13 +3939,17 @@ class MaskedEditMixin: value = self._eraseSelection() integer = self._fields[0] start, end = integer._extent + sel_start, sel_to = self._GetSelection() #### dbg('adjusted pos:', pos) if chr(key) in ('-','+','(', ')') or (chr(key) == " " and pos == self._signpos): cursign = self._isNeg ## dbg('cursign:', cursign) if chr(key) in ('-','(', ')'): - self._isNeg = (not self._isNeg) ## flip value + if sel_start <= self._signpos: + self._isNeg = True + else: + self._isNeg = (not self._isNeg) ## flip value else: self._isNeg = False ## dbg('isNeg?', self._isNeg) @@ -3862,7 +4021,8 @@ class MaskedEditMixin: def _findNextEntry(self,pos, adjustInsert=True): """ 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 else: # else within a field; flag not relevant adjustInsert = False @@ -3879,6 +4039,7 @@ class MaskedEditMixin: slice = self._GetValue()[start:end] if field._insertRight and field.IsEmpty(slice): pos = end +## dbg('final pos:', pos, indent=0) return pos @@ -3909,21 +4070,21 @@ class MaskedEditMixin: ## dbg('choices:', field._choices) ## dbg('compareChoices:', field._compareChoices) choices, choice_required = field._compareChoices, field._choiceRequired - if keycode in (wx.WXK_PRIOR, wx.WXK_UP): + if keycode in (wx.WXK_PRIOR, wx.WXK_UP, wx.WXK_NUMPAD_PRIOR, wx.WXK_NUMPAD_UP): direction = -1 else: direction = 1 match_index, partial_match = self._autoComplete(direction, choices, text, compareNoCase=field._compareNoCase, current_index = field._autoCompleteIndex) if( match_index is None - and (keycode in self._autoCompleteKeycodes + [wx.WXK_PRIOR, wx.WXK_NEXT] - or (keycode in [wx.WXK_UP, wx.WXK_DOWN] and event.ShiftDown() ) ) ): + and (keycode in self._autoCompleteKeycodes + [wx.WXK_PRIOR, wx.WXK_NEXT, wx.WXK_NUMPAD_PRIOR, wx.WXK_NUMPAD_NEXT] + or (keycode in [wx.WXK_UP, wx.WXK_DOWN, wx.WXK_NUMPAD_UP, wx.WXK_NUMPAD_DOWN] and event.ShiftDown() ) ) ): # Select the 1st thing from the list: match_index = 0 if( match_index is not None - and ( keycode in self._autoCompleteKeycodes + [wx.WXK_PRIOR, wx.WXK_NEXT] - or (keycode in [wx.WXK_UP, wx.WXK_DOWN] and event.ShiftDown()) - or (keycode == wx.WXK_DOWN and partial_match) ) ): + and ( keycode in self._autoCompleteKeycodes + [wx.WXK_PRIOR, wx.WXK_NEXT, wx.WXK_NUMPAD_PRIOR, wx.WXK_NUMPAD_NEXT] + or (keycode in [wx.WXK_UP, wx.WXK_DOWN, wx.WXK_NUMPAD_UP, wx.WXK_NUMPAD_DOWN] and event.ShiftDown()) + or (keycode in [wx.WXK_DOWN, wx.WXK_NUMPAD_DOWN] and partial_match) ) ): # We're allowed to auto-complete: ## dbg('match found') @@ -3936,10 +4097,11 @@ class MaskedEditMixin: self._CheckValid() # recolor as appopriate - if keycode in (wx.WXK_UP, wx.WXK_DOWN, wx.WXK_LEFT, wx.WXK_RIGHT): + if keycode in (wx.WXK_UP, wx.WXK_DOWN, wx.WXK_LEFT, wx.WXK_RIGHT, + wx.WXK_NUMPAD_UP, wx.WXK_NUMPAD_DOWN, wx.WXK_NUMPAD_LEFT, wx.WXK_NUMPAD_RIGHT): # treat as left right arrow if unshifted, tab/shift tab if shifted. if event.ShiftDown(): - if keycode in (wx.WXK_DOWN, wx.WXK_RIGHT): + if keycode in (wx.WXK_DOWN, wx.WXK_RIGHT, wx.WXK_NUMPAD_DOWN, wx.WXK_NUMPAD_RIGHT): # remove "shifting" and treat as (forward) tab: event.m_shiftDown = False keep_processing = self._OnChangeField(event) @@ -4098,7 +4260,7 @@ class MaskedEditMixin: # first space for sign, and last one if using parens. if( self._signOk and ((pos == self._signpos and key in (ord('-'), ord('+'), ord(' ')) ) - or self._useParens and pos == self._masklength -1)): + or (self._useParens and pos == self._masklength -1))): ## dbg('adjusted pos:', pos, indent=0) return pos @@ -4106,6 +4268,7 @@ class MaskedEditMixin: field = self._FindField(pos) ## dbg('field._insertRight?', field._insertRight) +## if self._signOk: dbg('self._signpos:', self._signpos) if field._insertRight: # if allow right-insert start, end = field._extent slice = self._GetValue()[start:end].strip() @@ -4140,12 +4303,14 @@ class MaskedEditMixin: ## # restore selection ## self._SetSelection(sel_start, pos) - elif self._signOk and sel_start == 0: # if selected to beginning and signed, + # if selected to beginning and signed, and not changing sign explicitly: + elif self._signOk and sel_start == 0 and key not in (ord('-'), ord('+'), ord(' ')): # adjust to past reserved sign position: pos = self._fields[0]._extent[0] +## dbg('adjusting field to ', pos) self._SetInsertionPoint(pos) - # restore selection - self._SetSelection(pos, sel_to) + # but keep original selection, to allow replacement of any sign: + self._SetSelection(0, sel_to) else: pass # leave position/selection alone @@ -4396,7 +4561,7 @@ class MaskedEditMixin: """ Returns True if the char at position pos is a special mask character (e.g. NCXaA#) """ if pos < self._masklength: - return self.ismasked[pos] + return self._ismasked[pos] else: return False @@ -4482,7 +4647,7 @@ class MaskedEditMixin: 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: ## dbg("checking appropriate regex's") @@ -4526,11 +4691,11 @@ class MaskedEditMixin: if self._signOk: text, signpos, right_signpos = self._getSignedValue() ## dbg('text: "%s", signpos:' % text, signpos) + if text and signpos != self._signpos: + self._signpos = signpos if not text or text[signpos] not in ('-','('): self._isNeg = False ## dbg('no valid sign found; new sign:', self._isNeg) - if text and signpos != self._signpos: - self._signpos = signpos elif text and self._valid and not self._isNeg and text[signpos] in ('-', '('): ## dbg('setting _isNeg to True') self._isNeg = True @@ -5352,6 +5517,8 @@ class MaskedEditMixin: field = self._FindField(self._GetInsertionPoint()) edit_start, edit_end = field._extent if field._selectOnFieldEntry: + if self._isFloat or self._isInt and field == self._fields[0]: + edit_start = 0 self._SetInsertionPoint(edit_start) self._SetSelection(edit_start, edit_end) @@ -5368,8 +5535,8 @@ class MaskedEditMixin: if integer._selectOnFieldEntry: ## dbg('select on field entry:') - self._SetInsertionPoint(edit_start) - self._SetSelection(edit_start, edit_end) + self._SetInsertionPoint(0) + self._SetSelection(0, edit_end) elif integer._insertRight: ## dbg('moving insertion point to end') @@ -5505,9 +5672,13 @@ class MaskedEditMixin: if raise_on_invalid: ## dbg(indent=0, suspend=0) 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: - 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: ## dbg(indent=0, suspend=0) return False, None, None @@ -5562,13 +5733,18 @@ class MaskedEditMixin: if not valid_paste and raise_on_invalid: ## 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): valid_paste = False if raise_on_invalid: ## 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) if valid_paste: @@ -5682,6 +5858,7 @@ class MaskedEditMixin: sel_start = 0 ## dbg('adjusted selection:', (sel_start, sel_to)) + raise_on_invalid = raise_on_invalid or field._raiseOnInvalidPaste try: valid_paste, replacement_text, replace_to = self._validatePaste(paste_text, sel_start, sel_to, raise_on_invalid) except: @@ -6546,6 +6723,42 @@ __i=0 ## CHANGELOG: ## ==================== +## Version 1.12 +## 1. Added proper support for NUMPAD keypad keycodes for navigation and control. +## +## 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 +## 1. Added handling for WXK_DELETE and WXK_INSERT, such that shift-delete +## cuts, shift-insert pastes, and ctrl-insert copies. +## +## Version 1.9 +## 1. Now ignores kill focus events when being destroyed. +## 2. Added missing call to set insertion point on changing fields. +## 3. Modified SetKeyHandler() to accept None as means of removing one. +## 4. Fixed keyhandler processing for group and decimal character changes. +## 5. Fixed a problem that prevented input into the integer digit of a +## integerwidth=1 numctrl, if the current value was 0. +## 6. Fixed logic involving processing of "_signOk" flag, to remove default +## sign key handlers if false, so that SetAllowNegative(False) in the +## NumCtrl works properly. +## 7. Fixed selection logic for numeric controls so that if selectOnFieldEntry +## is true, and the integer portion of an integer format control is selected +## and the sign position is selected, the sign keys will always result in a +## negative value, rather than toggling the previous sign. +## +## ## Version 1.8 ## 1. Fixed bug involving incorrect variable name, causing combobox autocomplete to fail. ## 2. Added proper support for unicode version of wxPython