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.