Updates to MaskedEdit controls from Will Sadkin:

maskededit.py:
    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.

  numctrl.py, demo / MaskedNumCtrl.py:
    In response to user request, added limitOnFieldChange feature, so that
    out-of-bounds values can be temporarily added to the control, but should
    navigation be attempted out of an invalid field, it will not navigate,
    and if focus is lost on a control so limited with an invalid value, it
    will change the value to the nearest bound.

  combobox.py:
    Added handler for EVT_COMBOBOX to address apparently inconsistent behavior
    of control when the dropdown control is used to do a selection.

  textctrl.py
    Added support for ChangeValue() function, similar to that of the base
    control, added in wxPython 2.7.1.1.


git-svn-id: https://svn.wxwidgets.org/svn/wx/wxWidgets/branches/WX_2_8_BRANCH@45743 c3d73ce0-8a6f-49c7-b76d-6d57e0e08775
This commit is contained in:
Robin Dunn
2007-05-02 01:00:30 +00:00
parent 3ead8c473e
commit e75f43c577
4 changed files with 216 additions and 33 deletions

View File

@@ -162,6 +162,8 @@ class BaseMaskedComboBox( wx.ComboBox, MaskedEditMixin ):
self.Bind(wx.EVT_KEY_DOWN, self._OnKeyDownInComboBox ) ## for special processing of up/down keys
self.Bind(wx.EVT_KEY_DOWN, self._OnKeyDown ) ## for processing the rest of the control keys
## (next in evt chain)
self.Bind(wx.EVT_COMBOBOX, self._OnDropdownSelect) ## to bring otherwise completely independent base
## ctrl selection into maskededit framework
self.Bind(wx.EVT_TEXT, self._OnTextChange ) ## color control appropriately & keep
## track of previous value for undo
@@ -531,6 +533,18 @@ class BaseMaskedComboBox( wx.ComboBox, MaskedEditMixin ):
event.Skip() # let mixin default KeyDown behavior occur
def _OnDropdownSelect(self, event):
"""
This function appears to be necessary because dropdown selection seems to
manipulate the contents of the control in an inconsistent way, properly
changing the selection index, but *not* the value. (!) Calling SetSelection()
on a selection event for the same selection would seem like a nop, but it seems to
fix the problem.
"""
self.SetSelection(event.GetSelection())
event.Skip()
def _OnSelectChoice(self, event):
"""
This function appears to be necessary, because the processing done
@@ -659,6 +673,10 @@ class PreMaskedComboBox( BaseMaskedComboBox, MaskedEditAccessorsMixin ):
__i = 0
## CHANGELOG:
## ====================
## Version 1.4
## 1. Added handler for EVT_COMBOBOX to address apparently inconsistent behavior
## of control when the dropdown control is used to do a selection.
##
## Version 1.3
## 1. Made definition of "hack" GetMark conditional on base class not
## implementing it properly, to allow for migration in wx code base

View File

@@ -1,12 +1,12 @@
#----------------------------------------------------------------------------
# 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
@@ -27,7 +27,7 @@
#
#----------------------------------------------------------------------------
#
# 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
@@ -337,7 +337,18 @@ to individual fields:
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
@@ -1327,12 +1338,14 @@ class Field:
'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):
"""
@@ -3021,7 +3034,7 @@ class MaskedEditMixin:
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)
@@ -3767,6 +3780,21 @@ class MaskedEditMixin:
## 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():
@@ -3775,13 +3803,12 @@ class MaskedEditMixin:
# 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():
@@ -3821,8 +3848,6 @@ class MaskedEditMixin:
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)
@@ -3888,10 +3913,19 @@ class MaskedEditMixin:
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())
@@ -4280,7 +4314,9 @@ class MaskedEditMixin:
#### 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)
@@ -4317,11 +4353,14 @@ class MaskedEditMixin:
# 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
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
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
@@ -5092,7 +5131,11 @@ class MaskedEditMixin:
#### 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
@@ -5165,7 +5208,11 @@ class MaskedEditMixin:
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:
@@ -6723,6 +6770,12 @@ __i=0
## 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.
##

View File

@@ -2,7 +2,7 @@
# Name: wxPython.lib.masked.numctrl.py
# Author: Will Sadkin
# Created: 09/06/2003
# Copyright: (c) 2003 by Will Sadkin
# Copyright: (c) 2003-2007 by Will Sadkin
# RCS-ID: $Id$
# License: wxWidgets license
#----------------------------------------------------------------------------
@@ -81,6 +81,7 @@ masked.NumCtrl:
min = None,
max = None,
limited = False,
limitOnFieldChange = False,
selectOnEntry = True,
foregroundColour = "Black",
signedForegroundColour = "Red",
@@ -155,6 +156,12 @@ masked.NumCtrl:
If False and bounds are set, out-of-bounds values will
result in a background colored with the current invalidBackgroundColour.
limitOnFieldChange
An alternative to limited, this boolean indicates whether or not a
field change should be allowed if the value in the control
is out of bounds. If True, and control focus is lost, this will also
cause the control to take on the nearest bound value.
selectOnEntry
Boolean indicating whether or not the value in each field of the
control should be automatically selected (for replacement) when
@@ -312,6 +319,18 @@ IsLimited()
Returns <I>True</I> if the control is currently limiting the
value to fall within the current bounds.
SetLimitOnFieldChange()
If called with a value of True, will cause the control to allow
out-of-bounds values, but will prevent field change if attempted
via navigation, and if the control loses focus, it will change
the value to the nearest bound.
GetLimitOnFieldChange()
IsLimitedOnFieldChange()
Returns <I>True</I> if the control is currently limiting the
value on field change.
SetAllowNone(bool)
If called with a value of True, this function will cause the control
@@ -390,7 +409,7 @@ MININT = -maxint-1
from wx.tools.dbg import Logger
from wx.lib.masked import MaskedEditMixin, Field, BaseMaskedTextCtrl
dbg = Logger()
##dbg = Logger()
##dbg(enable=1)
#----------------------------------------------------------------------------
@@ -442,6 +461,7 @@ class NumCtrlAccessorsMixin:
'emptyInvalid',
'validFunc',
'validRequired',
'stopFieldChangeIfInvalid',
)
for param in exposed_basectrl_params:
propname = param[0].upper() + param[1:]
@@ -478,6 +498,7 @@ class NumCtrl(BaseMaskedTextCtrl, NumCtrlAccessorsMixin):
'min': None, # by default, no bounds set
'max': None,
'limited': False, # by default, no limiting even if bounds set
'limitOnFieldChange': False, # by default, don't limit if changing fields, even if bounds set
'allowNone': False, # by default, don't allow empty value
'selectOnEntry': True, # by default, select the value of each field on entry
'foregroundColour': "Black",
@@ -759,6 +780,12 @@ class NumCtrl(BaseMaskedTextCtrl, NumCtrlAccessorsMixin):
maskededit_kwargs['validRequired'] = False
self._limited = kwargs['limited']
if kwargs.has_key('limitOnFieldChange'):
if kwargs['limitOnFieldChange'] and not self._limitOnFieldChange:
maskededit_kwargs['stopFieldChangeIfInvalid'] = True
elif kwargs['limitOnFieldChange'] and self._limitOnFieldChange:
maskededit_kwargs['stopFieldChangeIfInvalid'] = False
## dbg('maskededit_kwargs:', maskededit_kwargs)
if maskededit_kwargs.keys():
self.SetCtrlParameters(**maskededit_kwargs)
@@ -923,6 +950,43 @@ class NumCtrl(BaseMaskedTextCtrl, NumCtrlAccessorsMixin):
wx.CallAfter(self.SetInsertionPoint, sel_start) # preserve current selection/position
wx.CallAfter(self.SetSelection, sel_start, sel_to)
def _OnChangeField(self, event):
"""
This routine enhances the base masked control _OnFieldChange(). It's job
is to ensure limits are imposed if limitOnFieldChange is enabled.
"""
## dbg('NumCtrl::_OnFieldChange', indent=1)
if self._limitOnFieldChange and not (self._min <= self.GetValue() <= self._max):
self._disallowValue()
## dbg('oob - field change disallowed',indent=0)
return False
else:
## dbg(indent=0)
return MaskedEditMixin._OnChangeField(self, event) # call the baseclass function
def _LostFocus(self):
"""
On loss of focus, if limitOnFieldChange is set, ensure value conforms to limits.
"""
## dbg('NumCtrl::_LostFocus', indent=1)
if self._limitOnFieldChange:
## dbg("limiting on loss of focus")
value = self.GetValue()
if self._min is not None and value < self._min:
## dbg('Set to min value:', self._min)
self._SetValue(self._toGUI(self._min))
elif self._max is not None and value > self._max:
## dbg('Setting to max value:', self._max)
self._SetValue(self._toGUI(self._max))
# (else do nothing.)
# (else do nothing.)
## dbg(indent=0)
return True
def _SetValue(self, value):
"""
This routine supersedes the base masked control _SetValue(). It is
@@ -1346,7 +1410,32 @@ class NumCtrl(BaseMaskedTextCtrl, NumCtrlAccessorsMixin):
def GetLimited(self):
""" (For regularization of property accessors) """
return self.IsLimited
return self.IsLimited()
def SetLimitOnFieldChange(self, limit):
"""
If called with a value of True, this function will cause the control
to prevent navigation out of the current field if its value is out-of-bounds,
and limit the value to fall within the bounds currently specified if the
control loses focus.
If called with a value of False, this function will disable value
limiting, but coloring of out-of-bounds values will still take
place if bounds have been set for the control.
"""
self.SetParameters(limitOnFieldChange = limit)
def IsLimitedOnFieldChange(self):
"""
Returns True if the control is currently limiting the
value to fall within the current bounds.
"""
return self._limitOnFieldChange
def GetLimitOnFieldChange(self):
""" (For regularization of property accessors) """
return self.IsLimitedOnFieldChange()
def IsInBounds(self, value=None):
@@ -1794,6 +1883,13 @@ __i=0
## 1. Add support for printf-style format specification.
## 2. Add option for repositioning on 'illegal' insertion point.
##
## Version 1.4
## 1. In response to user request, added limitOnFieldChange feature, so that
## out-of-bounds values can be temporarily added to the control, but should
## navigation be attempted out of an invalid field, it will not navigate,
## and if focus is lost on a control so limited with an invalid value, it
## will change the value to the nearest bound.
##
## Version 1.3
## 1. fixed to allow space for a group char.
##

View File

@@ -152,28 +152,34 @@ class BaseMaskedTextCtrl( wx.TextCtrl, MaskedEditMixin ):
return self.GetValue()
def _SetValue(self, value):
def _SetValue(self, value, use_change_value=False):
"""
Allow mixin to set the raw value of the control with this function.
REQUIRED by any class derived from MaskedEditMixin.
"""
## dbg('MaskedTextCtrl::_SetValue("%(value)s")' % locals(), indent=1)
## dbg('MaskedTextCtrl::_SetValue("%(value)s", use_change_value=%(use_change_value)d)' % locals(), indent=1)
# Record current selection and insertion point, for undo
self._prevSelection = self._GetSelection()
self._prevInsertionPoint = self._GetInsertionPoint()
if use_change_value:
wx.TextCtrl.ChangeValue(self, value)
else:
wx.TextCtrl.SetValue(self, value)
## dbg(indent=0)
def SetValue(self, value):
def SetValue(self, value, use_change_value=False):
"""
This function redefines the externally accessible .SetValue() to be
a smart "paste" of the text in question, so as not to corrupt the
masked control. NOTE: this must be done in the class derived
from the base wx control.
"""
## dbg('MaskedTextCtrl::SetValue = "%s"' % value, indent=1)
## dbg('MaskedTextCtrl::SetValue("%(value)s", use_change_value=%(use_change_value)d)' % locals(), indent=1)
if not self._mask:
if use_change_value:
wx.TextCtrl.ChangeValue(self, value) # revert to base control behavior
else:
wx.TextCtrl.SetValue(self, value) # revert to base control behavior
return
@@ -221,7 +227,7 @@ class BaseMaskedTextCtrl( wx.TextCtrl, MaskedEditMixin ):
## dbg('exception thrown', indent=0)
raise
self._SetValue(value) # note: to preserve similar capability, .SetValue()
self._SetValue(value, use_change_value) # note: to preserve similar capability, .SetValue()
# does not change IsModified()
#### dbg('queuing insertion after .SetValue', replace_to)
# set selection to last char replaced by paste
@@ -229,6 +235,12 @@ class BaseMaskedTextCtrl( wx.TextCtrl, MaskedEditMixin ):
wx.CallAfter(self._SetSelection, replace_to, replace_to)
## dbg(indent=0)
def ChangeValue(self, value):
"""
Provided to accomodate similar functionality added to base control in wxPython 2.7.1.1.
"""
self.SetValue(value, use_change_value=True)
def SetFont(self, *args, **kwargs):
""" Set the font, then recalculate control size, if appropriate. """
@@ -372,6 +384,10 @@ class PreMaskedTextCtrl( BaseMaskedTextCtrl, MaskedEditAccessorsMixin ):
__i=0
## CHANGELOG:
## ====================
## Version 1.3
## - Added support for ChangeValue() function, similar to that of the base
## control, added in wxPython 2.7.1.1.
##
## Version 1.2
## - Converted docstrings to reST format, added doc for ePyDoc.
## removed debugging override functions.