SetSizeHints fix
[wxWidgets.git] / wxPython / wx / lib / masked / maskededit.py
index 1c528c9c666624aac4faf61675cd09cf0959291d..a3801d6eb9da960f706966c6abc978d48b09b43a 100644 (file)
@@ -86,7 +86,7 @@
     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 wxMaskedCtrl()
+    (See end of following discussion for how to configure the wx.MaskedCtrl()
     to select the above control types.)
 
 
@@ -528,7 +528,7 @@ Naming Conventions
                         the function for getting the start and end of the
                         current text selection.  The reason for this is
                         that not all controls have the same function name for
-                        doing this; eg. wxTextCtrl uses .GetSelection(),
+                        doing this; eg. wx.TextCtrl uses .GetSelection(),
                         whereas we had to write a .GetMark() function for
                         wxComboBox, because .GetSelection() for the control
                         gets the currently selected list item from the combo
@@ -757,7 +757,7 @@ import  wx
 from wx.tools.dbg import Logger
 
 dbg = Logger()
-##dbg(enable=0)
+##dbg(enable=1)
 
 ## ---------- ---------- ---------- ---------- ---------- ---------- ----------
 
@@ -1581,7 +1581,7 @@ class Field:
 class MaskedEditMixin:
     """
     This class allows us to abstract the masked edit functionality that could
-    be associated with any text entry control. (eg. wxTextCtrl, wxComboBox, etc.)
+    be associated with any text entry control. (eg. wx.TextCtrl, wx.ComboBox, etc.)
     """
     valid_ctrl_params = {
               'mask': 'XXXXXXXXXXXXX',          ## mask string for formatting this control
@@ -1914,10 +1914,12 @@ class MaskedEditMixin:
                         self._prevValue = newvalue  # disallow undo of sign type
 
             if self._autofit:
-##                dbg('setting client size to:', self._CalcSize())
-                size = self._CalcSize()
-                self.SetSizeHints(size)
-                self.SetClientSize(size)
+##                dbg('calculated size:', self._CalcSize())
+                self.SetClientSize(self._CalcSize())
+                width = self.GetSize().width
+                height = self.GetBestSize().height
+##                dbg('setting client size to:', (width, height))
+                self.SetBestFittingSize((width, height))
 
             # Set value/type-specific formatting
             self._applyFormatting()
@@ -1992,9 +1994,23 @@ class MaskedEditMixin:
                 self._SetInitialValue()
 
                 if self._autofit:
-                    size = self._CalcSize()
-                    self.SetSizeHints(size)
-                    self.SetClientSize(size)
+                    # this is tricky, because, as Robin explains:
+                    # "Basically there are two sizes to deal with, that are potentially 
+                    #  different.  The client size is the inside size and may, depending
+                    #  on platform, exclude the borders and such.  The normal size is
+                    #  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
+                    #  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
+                    #  get the height."
+                    self.SetClientSize(self._CalcSize())
+                    width = self.GetSize().width
+                    height = self.GetBestSize().height
+                    self.SetBestFittingSize((width, height))
+
 
             # Set value/type-specific formatting
             self._applyFormatting()
@@ -2644,7 +2660,7 @@ class MaskedEditMixin:
                 sizing_text += 'M'
 ####            dbg('len(sizing_text):', len(sizing_text), 'sizing_text: "%s"' % sizing_text)
             w, h = self.GetTextExtent(sizing_text)
-            size = (w+4, self.GetClientSize().height)
+            size = (w+4, self.GetSize().height)
 ####            dbg('size:', size, indent=0)
         return size
 
@@ -2684,14 +2700,15 @@ class MaskedEditMixin:
 ##            dbg(indent=0)
             return bValid
 
-        ##! WS: For some inexplicable reason, every wxTextCtrl.SetValue
-        ## call is generating two (2) EVT_TEXT events.
+        ##! WS: For some inexplicable reason, every wx.TextCtrl.SetValue
+        ## call is generating two (2) EVT_TEXT events.  On certain platforms,
+        ## (eg. linux/GTK) the 1st is an empty string value.
         ## This is the only mechanism I can find to mask this problem:
-        if newvalue == self._curValue:
+        if newvalue == self._curValue or len(newvalue) == 0:
 ##            dbg('ignoring bogus text change event', indent=0)
             pass
         else:
-##            dbg('curvalue: "%s", newvalue: "%s"' % (self._curValue, newvalue))
+##            dbg('curvalue: "%s", newvalue: "%s", len(newvalue): %d' % (self._curValue, newvalue, len(newvalue)))
             if self._Change():
                 if self._signOk and self._isNeg and newvalue.find('-') == -1 and newvalue.find('(') == -1:
 ##                    dbg('clearing self._isNeg')
@@ -2865,6 +2882,8 @@ class MaskedEditMixin:
                         if newfield != field and newfield._selectOnFieldEntry:
 ##                            dbg('queuing selection: (%d, %d)' % (newfield._extent[0], newfield._extent[1]))
                             wx.CallAfter(self._SetSelection, newfield._extent[0], newfield._extent[1])
+                        else:
+                            wx.CallAfter(self._SetSelection, newpos, new_select_to)
                     keep_processing = False
 
             elif keep_processing:
@@ -3090,7 +3109,7 @@ class MaskedEditMixin:
     def _OnCtrl_A(self,event=None):
         """ Handles ctrl-a keypress in control. Should return False to skip other processing. """
         end = self._goEnd(getPosOnly=True)
-        if not event or event.ShiftDown():
+        if not event or (isinstance(event, wx.KeyEvent) and event.ShiftDown()):
             wx.CallAfter(self._SetInsertionPoint, 0)
             wx.CallAfter(self._SetSelection, 0, self._masklength)
         else:
@@ -3099,7 +3118,7 @@ class MaskedEditMixin:
         return False
 
 
-    def _OnErase(self, event=None):
+    def _OnErase(self, event=None, just_return_value=False):
         """ Handles backspace and delete keypress in control. Should return False to skip other processing."""
 ##        dbg("MaskedEditMixin::_OnErase", indent=1)
         sel_start, sel_to = self._GetSelection()                   ## check for a range of selected text
@@ -3317,6 +3336,11 @@ class MaskedEditMixin:
 ##            dbg(indent=0)
             return False
 
+        if just_return_value:
+##            dbg(indent=0)
+            return newstr
+
+        # else...
 ##        dbg('setting value (later) to', newstr)
         wx.CallAfter(self._SetValue, newstr)
 ##        dbg('setting insertion point (later) to', pos)
@@ -3396,12 +3420,12 @@ class MaskedEditMixin:
 
     def _OnReturn(self, event):
          """
-         Changes the event to look like a tab event, so we can then call
-         event.Skip() on it, and have the parent form "do the right thing."
+         Swallows the return, issues a Navigate event instead, since
+         masked controls are "single line" by defn.
          """
 ##         dbg('MaskedEditMixin::OnReturn')
-         event.m_keyCode = wx.WXK_TAB
-         event.Skip()
+         self.Navigate(True)
+         return False
 
 
     def _OnHome(self,event):
@@ -3482,9 +3506,6 @@ class MaskedEditMixin:
         """
         Primarily handles TAB events, but can be used for any key that
         designer wants to change fields within a masked edit control.
-        NOTE: at the moment, although coded to handle shift-TAB and
-        control-shift-TAB, these events are not sent to the controls
-        by the framework.
         """
 ##        dbg('MaskedEditMixin::_OnChangeField', indent = 1)
         # determine end of current field:
@@ -3496,7 +3517,10 @@ class MaskedEditMixin:
             self._AdjustField(pos)
             if event.GetKeyCode() == wx.WXK_TAB:
 ##                dbg('tab to next ctrl')
-                event.Skip()
+                # As of 2.5.2, you don't call event.Skip() to do
+                # this, but instead force explicit navigation, if
+                # wx.TE_PROCESS_TAB is used (like in the masked edits)
+                self.Navigate(True)
             #else: do nothing
 ##            dbg(indent=0)
             return False
@@ -3530,7 +3554,10 @@ class MaskedEditMixin:
                 self._AdjustField(pos)
                 if event.GetKeyCode() == wx.WXK_TAB:
 ##                    dbg('tab to previous ctrl')
-                    event.Skip()
+                    # As of 2.5.2, you don't call event.Skip() to do
+                    # this, but instead force explicit navigation, if
+                    # wx.TE_PROCESS_TAB is used (like in the masked edits)
+                    self.Navigate(False)
                 else:
 ##                    dbg('position at beginning')
                     wx.CallAfter(self._SetInsertionPoint, field_start)
@@ -3576,7 +3603,10 @@ class MaskedEditMixin:
                     self._AdjustField(pos)
                     if event.GetKeyCode() == wx.WXK_TAB:
 ##                        dbg('tab to next ctrl')
-                        event.Skip()
+                        # As of 2.5.2, you don't call event.Skip() to do
+                        # this, but instead force explicit navigation, if
+                        # wx.TE_PROCESS_TAB is used (like in the masked edits)
+                        self.Navigate(True)
                     else:
 ##                        dbg('position at end')
                         wx.CallAfter(self._SetInsertionPoint, field_end)
@@ -3590,7 +3620,10 @@ class MaskedEditMixin:
                         self._AdjustField(pos)
                         if event.GetKeyCode() == wx.WXK_TAB:
 ##                            dbg('tab to next ctrl')
-                            event.Skip()
+                            # As of 2.5.2, you don't call event.Skip() to do
+                            # this, but instead force explicit navigation, if
+                            # wx.TE_PROCESS_TAB is used (like in the masked edits)
+                            self.Navigate(True)
                         #else: do nothing
 ##                        dbg(indent=0)
                         return False
@@ -3636,6 +3669,8 @@ class MaskedEditMixin:
             if fraction._selectOnFieldEntry:
 ##                dbg('queuing selection after decimal point to:', (start, end))
                 wx.CallAfter(self._SetSelection, start, end)
+            else:
+                wx.CallAfter(self._SetSelection, start, start)
             keep_processing = False
 
         if self._isInt:      ## handle integer value, truncate from current position
@@ -3650,6 +3685,7 @@ class MaskedEditMixin:
             if newstr.find(')') != -1:
                 newpos -= 1     # (don't move past right paren)
             wx.CallAfter(self._SetInsertionPoint, newpos)
+            wx.CallAfter(self._SetSelection, newpos, newpos)
             keep_processing = False
 ##        dbg(indent=0)
 
@@ -3945,6 +3981,7 @@ class MaskedEditMixin:
                     pos = pos+2
 
         if newvalue != value:
+##            dbg('old value: "%s"\nnew value: "%s"' % (value, newvalue))
             self._SetValue(newvalue)
             self._SetInsertionPoint(pos)
 
@@ -4038,6 +4075,8 @@ class MaskedEditMixin:
                     self._SetInsertionPoint(pos)
                     if pos < sel_to:    # restore selection
                         self._SetSelection(pos, sel_to)
+                    else:
+                        self._SetSelection(pos, pos)
 ##        dbg('adjusted pos:', pos, indent=0)
         return pos
 
@@ -4711,11 +4750,28 @@ class MaskedEditMixin:
         newtext = ""
         newpos = pos
 
+        # if >= 2 chars selected in a right-insert field, do appropriate erase on field,
+        # then set selection to end, and do usual right insert.
+        if sel_start != sel_to and sel_to >= sel_start+2:
+            field = self._FindField(sel_start)
+            if( field._insertRight                          # if right-insert
+                and field._allowInsert                      # and allow insert at any point in field
+                and field == self._FindField(sel_to) ):     # and selection all in same field
+                text = self._OnErase(just_return_value=True)    # remove selection before insert
+##                dbg('text after (left)erase: "%s"' % text)
+                pos = sel_start = sel_to
+
         if pos != sel_start and sel_start == sel_to:
             # adjustpos must have moved the position; make selection match:
             sel_start = sel_to = pos
 
 ##        dbg('field._insertRight?', field._insertRight)
+##        dbg('field._allowInsert?', field._allowInsert)
+##        dbg('sel_start, end', sel_start, end)
+        if sel_start < end:
+##            dbg('text[sel_start] != field._fillChar?', text[sel_start] != field._fillChar)
+            pass
+
         if( field._insertRight                                  # field allows right insert
             and ((sel_start, sel_to) == field._extent           # and whole field selected
                  or (sel_start == sel_to                        # or nothing selected
@@ -4871,7 +4927,8 @@ class MaskedEditMixin:
                 if match_index is not None and partial_match:
                     matched_str = newtext
                     newtext = self._ctrl_constraints._choices[match_index]
-                    new_select_to = self._ctrl_constraints._extent[1]
+                    edit_end = self._ctrl_constraints._extent[1]
+                    new_select_to = min(edit_end, len(newvalue.rstrip()))
                     match_field = self._ctrl_constraints
                     if self._ctrl_constraints._insertRight:
                         # adjust position to just after partial match in control:
@@ -4890,13 +4947,15 @@ class MaskedEditMixin:
         """
         This event handler is currently necessary to work around new default
         behavior as of wxPython2.3.3;
-        The TAB key auto selects the entire contents of the wxTextCtrl *after*
+        The TAB key auto selects the entire contents of the wx.TextCtrl *after*
         the EVT_SET_FOCUS event occurs; therefore we can't query/adjust the selection
         *here*, because it hasn't happened yet.  So to prevent this behavior, and
         preserve the correct selection when the focus event is not due to tab,
         we need to pull the following trick:
         """
 ##        dbg('MaskedEditMixin::_OnFocus')
+        if self.IsBeingDeleted() or self.GetParent().IsBeingDeleted():
+            return 
         wx.CallAfter(self._fixSelection)
         event.Skip()
         self.Refresh()
@@ -4999,7 +5058,7 @@ class MaskedEditMixin:
                 if self._isFloat and groupcharpos > self._decimalpos:
                     # 1st one found on right-hand side is past decimal point
 ##                    dbg('groupchar in fraction; illegal')
-                    valid = False
+                    return False
                 elif self._isFloat:
                     integer = value[:self._decimalpos].strip()
                 else:
@@ -5153,14 +5212,17 @@ class MaskedEditMixin:
 
         The trouble is that, a priori, there's no explicit notification of
         why the focus event we received.  However, the whole reason we need to
-        do this is because the default behavior on TAB traveral in a wxTextCtrl is
+        do this is because the default behavior on TAB traveral in a wx.TextCtrl is
         now to select the entire contents of the window, something we don't want.
         So we can *now* test the selection range, and if it's "the whole text"
         we can assume the cause, change the insertion point to the start of
         the control, and deselect.
         """
 ##        dbg('MaskedEditMixin::_fixSelection', indent=1)
-        if not self._mask or not self._IsEditable():
+        # can get here if called with wx.CallAfter after underlying 
+        # control has been destroyed on close, but after focus
+        # events
+        if not self or not self._mask or not self._IsEditable():
 ##            dbg(indent=0)
             return
 
@@ -5258,11 +5320,11 @@ class MaskedEditMixin:
 ##        dbg('current value: "%s"' % value)
         sel_start, sel_to = self._GetSelection()                   ## check for a range of selected text
 ##        dbg('selected text: "%s"' % value[sel_start:sel_to].strip())
-        do = wxTextDataObject()
+        do = wx.TextDataObject()
         do.SetText(value[sel_start:sel_to].strip())
-        wxTheClipboard.Open()
-        wxTheClipboard.SetData(do)
-        wxTheClipboard.Close()
+        wx.TheClipboard.Open()
+        wx.TheClipboard.SetData(do)
+        wx.TheClipboard.Close()
 
         if sel_to - sel_start != 0:
             self._OnErase()
@@ -5274,27 +5336,27 @@ class MaskedEditMixin:
 #
 ##    def _Copy( self ):
 ##        """
-##        Override the wxTextCtrl's .Copy function, with our own
+##        Override the wx.TextCtrl's .Copy function, with our own
 ##        that does validation.  Need to strip trailing spaces.
 ##        """
 ##        sel_start, sel_to = self._GetSelection()
 ##        select_len = sel_to - sel_start
-##        textval = wxTextCtrl._GetValue(self)
+##        textval = wx.TextCtrl._GetValue(self)
 ##
-##        do = wxTextDataObject()
+##        do = wx.TextDataObject()
 ##        do.SetText(textval[sel_start:sel_to].strip())
-##        wxTheClipboard.Open()
-##        wxTheClipboard.SetData(do)
-##        wxTheClipboard.Close()
+##        wx.TheClipboard.Open()
+##        wx.TheClipboard.SetData(do)
+##        wx.TheClipboard.Close()
 
 
     def _getClipboardContents( self ):
         """ Subroutine for getting the current contents of the clipboard.
         """
-        do = wxTextDataObject()
-        wxTheClipboard.Open()
-        success = wxTheClipboard.GetData(do)
-        wxTheClipboard.Close()
+        do = wx.TextDataObject()
+        wx.TheClipboard.Open()
+        success = wx.TheClipboard.GetData(do)
+        wx.TheClipboard.Close()
 
         if not success:
             return None
@@ -5424,11 +5486,31 @@ class MaskedEditMixin:
             field = self._FindField(sel_start)
             edit_start, edit_end = field._extent
             new_pos = None
-            if field._allowInsert and sel_to <= edit_end and sel_start + len(paste_text) < edit_end:
+            if field._allowInsert and sel_to <= edit_end and (sel_start + len(paste_text) < edit_end or field._insertRight):
+                if field._insertRight:
+                    # want to paste to the left; see if it will fit:
+                    left_text = self._GetValue()[edit_start:sel_start].lstrip()
+##                    dbg('len(left_text):', len(left_text))
+##                    dbg('len(paste_text):', len(paste_text))
+##                    dbg('sel_start - (len(left_text) + len(paste_text)) >= edit_start?', sel_start - (len(left_text) + len(paste_text)) >= edit_start)
+                    if sel_start - (len(left_text) - (sel_to - sel_start) + len(paste_text)) >= edit_start:
+                        # will fit! create effective paste text, and move cursor back to do so:
+                        paste_text = left_text + paste_text
+                        sel_start -= len(left_text)
+                        paste_text = paste_text.rjust(sel_to - sel_start)
+##                        dbg('modified paste_text to be: "%s"' % paste_text)
+##                        dbg('modified selection to:', (sel_start, sel_to))
+                    else:
+##                        dbg("won't fit left;", 'paste text remains: "%s"' % paste_text)
+                        pass
+                else:
+                    paste_text = paste_text + self._GetValue()[sel_to:edit_end].rstrip()
+##                    dbg("allow insert, but not insert right;", 'paste text set to: "%s"' % paste_text)
+
+
                 new_pos = sel_start + len(paste_text)   # store for subsequent positioning
-                paste_text = paste_text + self._GetValue()[sel_to:edit_end].rstrip()
 ##                dbg('paste within insertable field; adjusted paste_text: "%s"' % paste_text, 'end:', edit_end)
-                sel_to = sel_start + len(paste_text)
+##                dbg('expanded selection to:', (sel_start, sel_to))
 
             # Another special case: paste won't fit, but it's a right-insert field where entire
             # non-empty value is selected, and there's room if the selection is expanded leftward:
@@ -5491,7 +5573,7 @@ class MaskedEditMixin:
                 if not wx.Validator_IsSilent():
                     wx.Bell()
 ##                dbg(indent=0)
-                return False
+                return None, -1
             # else...
             text = self._eraseSelection()
 
@@ -5522,17 +5604,19 @@ class MaskedEditMixin:
                     wx.CallAfter(self._SetInsertionPoint, new_pos)
             else:
 ##                dbg(indent=0)
-                return new_text
+                return new_text, replace_to
         elif just_return_value:
 ##            dbg(indent=0)
-            return self._GetValue()
+            return self._GetValue(), sel_to
 ##        dbg(indent=0)
 
-    def _Undo(self):
+    def _Undo(self, value=None, prev=None, just_return_results=False):
         """ Provides an Undo() method in base controls. """
 ##        dbg("MaskedEditMixin::_Undo", indent=1)
-        value = self._GetValue()
-        prev = self._prevValue
+        if value is None:
+            value = self._GetValue()
+        if prev is None:
+            prev = self._prevValue
 ##        dbg('current value:  "%s"' % value)
 ##        dbg('previous value: "%s"' % prev)
         if prev is None:
@@ -5589,6 +5673,7 @@ class MaskedEditMixin:
                 for next_op in range(len(code_5tuples)-1, -1, -1):
                     op, i1, i2, j1, j2 = code_5tuples[next_op]
 ##                    dbg('value[i1:i2]: "%s"' % value[i1:i2], 'template[i1:i2] "%s"' % self._template[i1:i2])
+                    field = self._FindField(i2)
                     if op == 'insert' and prev[j1:j2] != self._template[j1:j2]:
 ##                        dbg('insert found: selection =>', (j1, j2))
                         sel_start = j1
@@ -5596,9 +5681,8 @@ class MaskedEditMixin:
                         diff_found = True
                         break
                     elif op == 'delete' and value[i1:i2] != self._template[i1:i2]:
-                        field = self._FindField(i2)
                         edit_start, edit_end = field._extent
-                        if field._insertRight and i2 == edit_end:
+                        if field._insertRight and (field._allowInsert or i2 == edit_end):
                             sel_start = i2
                             sel_to = i2
                         else:
@@ -5608,23 +5692,39 @@ class MaskedEditMixin:
                         diff_found = True
                         break
                     elif op == 'replace':
-##                        dbg('replace found: selection =>', (j1, j2))
-                        sel_start = j1
-                        sel_to = j2
+                        if not prev[i1:i2].strip() and field._insertRight:
+                            sel_start = sel_to = j2
+                        else:
+                            sel_start = j1
+                            sel_to = j2
+##                        dbg('replace found: selection =>', (sel_start, sel_to))
                         diff_found = True
                         break
 
 
                 if diff_found:
                     # now go forwards, looking for earlier changes:
+##                    dbg('searching forward...')
                     for next_op in range(len(code_5tuples)):
                         op, i1, i2, j1, j2 = code_5tuples[next_op]
                         field = self._FindField(i1)
                         if op == 'equal':
                             continue
                         elif op == 'replace':
-##                            dbg('setting sel_start to', i1)
-                            sel_start = i1
+                            if field._insertRight:
+                                # if replace with spaces in an insert-right control, ignore "forward" replace
+                                if not prev[i1:i2].strip():
+                                    continue
+                                elif j1 < i1:
+##                                    dbg('setting sel_start to', j1)
+                                    sel_start = j1
+                                else:
+##                                    dbg('setting sel_start to', i1)
+                                    sel_start = i1
+                            else:
+##                                dbg('setting sel_start to', i1)
+                                sel_start = i1
+##                            dbg('saw replace; breaking')
                             break
                         elif op == 'insert' and not value[i1:i2]:
 ##                            dbg('forward %s found' % op)
@@ -5636,9 +5736,21 @@ class MaskedEditMixin:
 ##                                dbg('setting sel_start to inserted space:', j1)
                                 sel_start = j1
                                 break
-                        elif op == 'delete' and field._insertRight and not value[i1:i2].lstrip():
-                            continue
+                        elif op == 'delete':
+##                            dbg('delete; field._insertRight?', field._insertRight, 'value[%d:%d].lstrip: "%s"' % (i1,i2,value[i1:i2].lstrip()))
+                            if field._insertRight:
+                                if value[i1:i2].lstrip():
+##                                    dbg('setting sel_start to ', j1)
+                                    sel_start = j1
+##                                    dbg('breaking loop')
+                                    break
+                                else:
+                                    continue
+                            else:
+##                                dbg('saw delete; breaking')
+                                break
                         else:
+##                            dbg('unknown code!')
                             # we've got what we need
                             break
 
@@ -5692,15 +5804,22 @@ class MaskedEditMixin:
 
                 prev_sel_start, prev_sel_to = self._prevSelection
                 field = self._FindField(sel_start)
-
-                if self._signOk and (self._prevValue[sel_start] in ('-', '(', ')')
-                                     or self._curValue[sel_start] in ('-', '(', ')')):
+                if( self._signOk
+                      and sel_start < self._masklength
+                      and (prev[sel_start] in ('-', '(', ')')
+                                     or value[sel_start] in ('-', '(', ')')) ):
                     # change of sign; leave cursor alone...
+##                    dbg("prev[sel_start] in ('-', '(', ')')?", prev[sel_start] in ('-', '(', ')'))
+##                    dbg("value[sel_start] in ('-', '(', ')')?", value[sel_start] in ('-', '(', ')'))
+##                    dbg('setting selection to previous one')
                     sel_start, sel_to = self._prevSelection
 
-                elif field._groupdigits and (self._curValue[sel_start:sel_to] == field._groupChar
-                                             or self._prevValue[sel_start:sel_to] == field._groupChar):
+                elif field._groupdigits and (value[sel_start:sel_to] == field._groupChar
+                                             or prev[sel_start:sel_to] == field._groupChar):
                     # do not highlight grouping changes
+##                    dbg('value[sel_start:sel_to] == field._groupChar?', value[sel_start:sel_to] == field._groupChar)
+##                    dbg('prev[sel_start:sel_to] == field._groupChar?', prev[sel_start:sel_to] == field._groupChar)
+##                    dbg('setting selection to previous one')
                     sel_start, sel_to = self._prevSelection
 
                 else:
@@ -5712,7 +5831,13 @@ class MaskedEditMixin:
 
                     if prev_select_len >= calc_select_len:
                         # old selection was bigger; trust it:
-                        sel_start, sel_to = self._prevSelection
+##                        dbg('prev_select_len >= calc_select_len?', prev_select_len >= calc_select_len)
+                        if not field._insertRight:
+##                            dbg('setting selection to previous one')
+                            sel_start, sel_to = self._prevSelection
+                        else:
+                            sel_to = self._prevSelection[1]
+##                            dbg('setting selection to', (sel_start, sel_to))
 
                     elif( sel_to > prev_sel_to                  # calculated select past last selection
                           and prev_sel_to < len(self._template) # and prev_sel_to not at end of control
@@ -5743,27 +5868,44 @@ class MaskedEditMixin:
                             test_sel_start, test_sel_to = prev_sel_start, prev_sel_to
 
 ##                        dbg('test selection:', (test_sel_start, test_sel_to))
-##                        dbg('calc change: "%s"' % self._prevValue[sel_start:sel_to])
-##                        dbg('test change: "%s"' % self._prevValue[test_sel_start:test_sel_to])
+##                        dbg('calc change: "%s"' % prev[sel_start:sel_to])
+##                        dbg('test change: "%s"' % prev[test_sel_start:test_sel_to])
 
                         # if calculated selection spans characters, and same characters
                         # "before" the previous insertion point are present there as well,
                         # select the ones related to the last known selection instead.
                         if( sel_start != sel_to
                             and test_sel_to < len(self._template)
-                            and self._prevValue[test_sel_start:test_sel_to] == self._prevValue[sel_start:sel_to] ):
+                            and prev[test_sel_start:test_sel_to] == prev[sel_start:sel_to] ):
 
                             sel_start, sel_to = test_sel_start, test_sel_to
 
+                # finally, make sure that the old and new values are
+                # different where we say they're different:
+                while( sel_to - 1 > 0
+                        and sel_to > sel_start
+                        and value[sel_to-1:] == prev[sel_to-1:]):
+                    sel_to -= 1
+                while( sel_start + 1 < self._masklength
+                        and sel_start < sel_to
+                        and value[:sel_start+1] == prev[:sel_start+1]):
+                    sel_start += 1
+
 ##            dbg('sel_start, sel_to:', sel_start, sel_to)
-##            dbg('previous value: "%s"' % self._prevValue)
-            self._SetValue(self._prevValue)
+##            dbg('previous value: "%s"' % prev)
+##            dbg(indent=0)
+            if just_return_results:
+                return prev, (sel_start, sel_to)
+            # else...
+            self._SetValue(prev)
             self._SetInsertionPoint(sel_start)
             self._SetSelection(sel_start, sel_to)
+
         else:
 ##            dbg('no difference between previous value')
-            pass
-##        dbg(indent=0)
+##            dbg(indent=0)
+            if just_return_results:
+                return prev, self._GetSelection()
 
 
     def _OnClear(self, event):
@@ -5773,31 +5915,31 @@ class MaskedEditMixin:
 
     def _OnContextMenu(self, event):
 ##        dbg('MaskedEditMixin::OnContextMenu()', indent=1)
-        menu = wxMenu()
-        menu.Append(wxID_UNDO, "Undo", "")
+        menu = wx.Menu()
+        menu.Append(wx.ID_UNDO, "Undo", "")
         menu.AppendSeparator()
-        menu.Append(wxID_CUT, "Cut", "")
-        menu.Append(wxID_COPY, "Copy", "")
-        menu.Append(wxID_PASTE, "Paste", "")
-        menu.Append(wxID_CLEAR, "Delete", "")
+        menu.Append(wx.ID_CUT, "Cut", "")
+        menu.Append(wx.ID_COPY, "Copy", "")
+        menu.Append(wx.ID_PASTE, "Paste", "")
+        menu.Append(wx.ID_CLEAR, "Delete", "")
         menu.AppendSeparator()
-        menu.Append(wxID_SELECTALL, "Select All", "")
+        menu.Append(wx.ID_SELECTALL, "Select All", "")
 
-        EVT_MENU(menu, wxID_UNDO, self._OnCtrl_Z)
-        EVT_MENU(menu, wxID_CUT, self._OnCtrl_X)
-        EVT_MENU(menu, wxID_COPY, self._OnCtrl_C)
-        EVT_MENU(menu, wxID_PASTE, self._OnCtrl_V)
-        EVT_MENU(menu, wxID_CLEAR, self._OnClear)
-        EVT_MENU(menu, wxID_SELECTALL, self._OnCtrl_A)
+        wx.EVT_MENU(menu, wx.ID_UNDO, self._OnCtrl_Z)
+        wx.EVT_MENU(menu, wx.ID_CUT, self._OnCtrl_X)
+        wx.EVT_MENU(menu, wx.ID_COPY, self._OnCtrl_C)
+        wx.EVT_MENU(menu, wx.ID_PASTE, self._OnCtrl_V)
+        wx.EVT_MENU(menu, wx.ID_CLEAR, self._OnClear)
+        wx.EVT_MENU(menu, wx.ID_SELECTALL, self._OnCtrl_A)
 
         # ## WSS: The base control apparently handles
-        # enable/disable of wID_CUT, wxID_COPY, wxID_PASTE
-        # and wxID_CLEAR menu items even if the menu is one
+        # enable/disable of wx.ID_CUT, wx.ID_COPY, wx.ID_PASTE
+        # and wx.ID_CLEAR menu items even if the menu is one
         # we created.  However, it doesn't do undo properly,
         # so we're keeping track of previous values ourselves.
         # Therefore, we have to override the default update for
         # that item on the menu:
-        EVT_UPDATE_UI(self, wxID_UNDO, self._UndoUpdateUI)
+        wx.EVT_UPDATE_UI(self, wx.ID_UNDO, self._UndoUpdateUI)
         self._contextMenu = menu
 
         self.PopupMenu(menu, event.GetPosition())
@@ -5807,9 +5949,9 @@ class MaskedEditMixin:
 
     def _UndoUpdateUI(self, event):
         if self._prevValue is None or self._prevValue == self._curValue:
-            self._contextMenu.Enable(wxID_UNDO, False)
+            self._contextMenu.Enable(wx.ID_UNDO, False)
         else:
-            self._contextMenu.Enable(wxID_UNDO, True)
+            self._contextMenu.Enable(wx.ID_UNDO, True)
 
 
     def _OnCtrlParametersChanged(self):
@@ -6284,6 +6426,19 @@ i=1
 
 ## CHANGELOG:
 ## ====================
+##  Version 1.7
+##  1. Fixed intra-right-insert-field erase, such that it doesn't leave a hole, but instead
+##     shifts the text to the left accordingly.
+##  2. Fixed _SetValue() to place cursor after last character inserted, rather than end of
+##     mask.
+##  3. Fixed some incorrect undo behavior for right-insert fields, and allowed derived classes
+##     (eg. numctrl) to pass modified values for undo processing (to handle/ignore grouping
+##     chars properly.)
+##  4. Fixed autoselect behavior to work similarly to (2) above, so that combobox
+##     selection will only select the non-empty text, as per request.
+##  5. Fixed tabbing to work with 2.5.2 semantics.
+##  6. Fixed size calculation to handle changing fonts
+##
 ##  Version 1.6
 ##  1. Reorganized masked controls into separate package, renamed things accordingly
 ##  2. Split actual controls out of this file into their own files.
@@ -6352,7 +6507,7 @@ i=1
 ##      non-british spellings still supported for backward-compatibility.
 ##  20. Added '&' mask specification character for punctuation only (no letters
 ##      or digits).
-##  21. Added (in a separate file) wxMaskedCtrl() factory function to provide
+##  21. Added (in a separate file) wx.MaskedCtrl() factory function to provide
 ##      unified interface to the masked edit subclasses.
 ##
 ##
@@ -6376,7 +6531,7 @@ i=1
 ##   2. Fixed EUDATE* autoformats, fixed IsDateType mask list, and added ability to
 ##      use 3-char months for dates, and EUDATETIME, and EUDATEMILTIME autoformats.
 ##   3. Made all date autoformats automatically pick implied "datestyle".
-##   4. Added IsModified override, since base wxTextCtrl never reports modified if
+##   4. Added IsModified override, since base wx.TextCtrl never reports modified if
 ##      .SetValue used to change the value, which is what the masked edit controls
 ##      use internally.
 ##   5. Fixed bug in date position adjustment on 2 to 4 digit date conversion when