diff --git a/wxPython/wx/lib/masked/combobox.py b/wxPython/wx/lib/masked/combobox.py
index 33ac41ad2d..598d5796d5 100644
--- a/wxPython/wx/lib/masked/combobox.py
+++ b/wxPython/wx/lib/masked/combobox.py
@@ -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
diff --git a/wxPython/wx/lib/masked/maskededit.py b/wxPython/wx/lib/masked/maskededit.py
index b5696c7e1a..b2b77840e3 100644
--- a/wxPython/wx/lib/masked/maskededit.py
+++ b/wxPython/wx/lib/masked/maskededit.py
@@ -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())
@@ -4021,7 +4055,7 @@ class MaskedEditMixin:
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
@@ -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
- 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
@@ -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.
##
diff --git a/wxPython/wx/lib/masked/numctrl.py b/wxPython/wx/lib/masked/numctrl.py
index 73a837039b..dfcebc9065 100644
--- a/wxPython/wx/lib/masked/numctrl.py
+++ b/wxPython/wx/lib/masked/numctrl.py
@@ -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 True 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 True 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.
##
diff --git a/wxPython/wx/lib/masked/textctrl.py b/wxPython/wx/lib/masked/textctrl.py
index d374d7b247..9504da9036 100644
--- a/wxPython/wx/lib/masked/textctrl.py
+++ b/wxPython/wx/lib/masked/textctrl.py
@@ -152,29 +152,35 @@ 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()
- wx.TextCtrl.SetValue(self, value)
+ 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:
- wx.TextCtrl.SetValue(self, value) # revert to base control behavior
+ 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
# empty previous contents, replacing entire value:
@@ -221,14 +227,20 @@ class BaseMaskedTextCtrl( wx.TextCtrl, MaskedEditMixin ):
## dbg('exception thrown', indent=0)
raise
- self._SetValue(value) # note: to preserve similar capability, .SetValue()
- # does not change IsModified()
+ 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
wx.CallAfter(self._SetInsertionPoint, replace_to)
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.