#----------------------------------------------------------------------------
# Name: maskededit.py
-# Authors: Jeff Childers, Will Sadkin
-# Email: jchilders_98@yahoo.com, wsadkin@parlancecorp.com
+# Authors: Will Sadkin, Jeff Childers
+# Email: wsadkin@parlancecorp.com, jchilders_98@yahoo.com
# Created: 02/11/2003
# Copyright: (c) 2003 by Jeff Childers, Will Sadkin, 2003
-# Portions: (c) 2002 by Will Sadkin, 2002-2006
+# Portions: (c) 2002 by Will Sadkin, 2002-2007
# RCS-ID: $Id$
-# License: wxWindows license
+# License: wxWidgets license
#----------------------------------------------------------------------------
# NOTE:
# MaskedEdit controls are based on a suggestion made on [wxPython-Users] by
#
#----------------------------------------------------------------------------
#
-# 03/30/2004 - Will Sadkin (wsadkin@nameconnector.com)
+# 03/30/2004 - Will Sadkin (wsadkin@parlancecorp.com)
#
# o Split out TextCtrl, ComboBox and IpAddrCtrl into their own files,
# o Reorganized code into masked package
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.
- ===================== ==================================================================
+
+ stopFieldChangeIfInvalid
+ False by default; tries to prevent navigation out of a field if its
+ current value is invalid. Can be used to create a hybrid of validation
+ settings, allowing intermediate invalid values in a field without
+ sacrificing ability to limit values as with validRequired.
+ NOTE: It is possible to end up with an invalid value when using
+ this option if focus is switched to some other control via mousing.
+ To avoid this, consider deriving a class that defines _LostFocus()
+ function that returns the control to a valid value when the focus
+ shifts. (AFAICT, The change in focus is unpreventable.)
+ ===================== =================================================================
Coloring Behavior
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, wx.WXK_INSERT, 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
)
'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
+ 'stopFieldChangeIfInvalid': False,## if True, disallow field navigation out of invalid field
}
# 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', 'raiseOnInvalidPaste')
+ 'compareNoCase', 'emptyInvalid', 'validRequired', 'raiseOnInvalidPaste',
+ 'stopFieldChangeIfInvalid')
def __init__(self, **kwargs):
"""
## 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._OnDelete,
- wx.WXK_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,
+ 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,
#### 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 cursor after decimal point if floating point, otherwise at 0.
char = char.decode(self._defaultEncoding)
else:
char = unichr(event.GetUnicodeKey())
- dbg('unicode char:', char)
+## dbg('unicode char:', char)
excludes = u''
if type(field._excludeChars) != types.UnicodeType:
excludes += field._excludeChars.decode(self._defaultEncoding)
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
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)):
## 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:
# 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)
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
## dbg(indent=0)
return False
+ field = self._FindField(sel_to)
+ index = field._index
+ field_start, field_end = field._extent
+ slice = self._GetValue()[field_start:field_end]
+
+## dbg('field._stopFieldChangeIfInvalid?', field._stopFieldChangeIfInvalid)
+## dbg('field.IsValid(slice)?', field.IsValid(slice))
+
+ if field._stopFieldChangeIfInvalid and not field.IsValid(slice):
+## dbg('field invalid; field change disallowed')
+ if not wx.Validator_IsSilent():
+ wx.Bell()
+## dbg(indent=0)
+ return False
+
if event.ShiftDown():
# NOTE: doesn't yet work with SHIFT-tab under wx; the control
# never sees this event! (But I've coded for it should it ever work,
# and it *does* work for '.' in IpAddrCtrl.)
- field = self._FindField(pos)
- index = field._index
- field_start = field._extent[0]
+
if pos < field_start:
## dbg('cursor before 1st field; cannot change to a previous field')
if not wx.Validator_IsSilent():
wx.Bell()
+## dbg(indent=0)
return False
if event.ControlDown():
else:
# "Go forward"
- field = self._FindField(sel_to)
- field_start, field_end = field._extent
if event.ControlDown():
## dbg('queuing select to end of field:', pos, field_end)
wx.CallAfter(self._SetInsertionPoint, pos)
wx.CallAfter(self._SetInsertionPoint, next_pos)
## dbg(indent=0)
return False
+## dbg(indent=0)
def _OnDecimalPoint(self, event):
## dbg('MaskedEditMixin::_OnDecimalPoint', indent=1)
+ field = self._FindField(self._GetInsertionPoint())
+ start, end = field._extent
+ slice = self._GetValue()[start:end]
+
+ if field._stopFieldChangeIfInvalid and not field.IsValid(slice):
+ if not wx.Validator_IsSilent():
+ wx.Bell()
+ return False
pos = self._adjustPos(self._GetInsertionPoint(), event.GetKeyCode())
def _findNextEntry(self,pos, adjustInsert=True):
""" Find the insertion point for the next valid entry character position."""
-## dbg('MaskedEditMixin::_findNextEntry', indent=1)
+## 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
## 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')
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)
#### dbg('field_len?', field_len)
#### dbg('pos==end; len (slice) < field_len?', len(slice) < field_len)
#### dbg('not field._moveOnFieldFull?', not field._moveOnFieldFull)
- if len(slice) == field_len and field._moveOnFieldFull:
+ if( len(slice) == field_len and field._moveOnFieldFull
+ and (not field._stopFieldChangeIfInvalid or
+ field._stopFieldChangeIfInvalid and field.IsValid(slice))):
# move cursor to next field:
pos = self._findNextEntry(pos)
self._SetInsertionPoint(pos)
# else make sure the user is not trying to type over a template character
# If they are, move them to the next valid entry position
elif self._isTemplateChar(pos):
- if( not field._moveOnFieldFull
- and (not self._signOk
- or (self._signOk
- and field._index == 0
- and pos > 0) ) ): # don't move to next field without explicit cursor movement
+ if( (not field._moveOnFieldFull
+ and (not self._signOk
+ or (self._signOk and field._index == 0 and pos > 0) ) )
+
+ or (field._stopFieldChangeIfInvalid
+ and not field.IsValid(self._GetValue()[start:end]) ) ):
+
+ # don't move to next field without explicit cursor movement
pass
else:
# find next valid position
#### dbg('field._moveOnFieldFull?', field._moveOnFieldFull)
#### dbg('len(fstr.lstrip()) == end-start?', len(fstr.lstrip()) == end-start)
if( field._moveOnFieldFull and pos == end
- and len(fstr.lstrip()) == end-start): # if field now full
+ and len(fstr.lstrip()) == end-start # if field now full
+ and (not field._stopFieldChangeIfInvalid # and we either don't care about valid
+ or (field._stopFieldChangeIfInvalid # or we do and the current field value is valid
+ and field.IsValid(fstr)))):
+
newpos = self._findNextEntry(end) # go to next field
else:
newpos = pos # else keep cursor at current position
if( field._insertRight # if insert-right field (but we didn't start at right edge)
and field._moveOnFieldFull # and should move cursor when full
- and len(newtext[start:end].strip()) == end-start): # and field now full
+ and len(newtext[start:end].strip()) == end-start # and field now full
+ and (not field._stopFieldChangeIfInvalid # and we either don't care about valid
+ or (field._stopFieldChangeIfInvalid # or we do and the current field value is valid
+ and field.IsValid(newtext[start:end].strip())))):
+
newpos = self._findNextEntry(end) # go to next field
## dbg('newpos = nextentry =', newpos)
else:
## CHANGELOG:
## ====================
+## Version 1.13
+## 1. Added parameter option stopFieldChangeIfInvalid, which can be used to relax the
+## validation rules for a control, but make best efforts to stop navigation out of
+## that field should its current value be invalid. Note: this does not prevent the
+## value from remaining invalid if focus for the control is lost, via mousing etc.
+##
+## 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,