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.)
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
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
if self._autofit:
## dbg('setting client size to:', self._CalcSize())
- size = self._CalcSize()
- self.SetSizeHints(size)
- self.SetClientSize(size)
+ self.SetClientSize(self._CalcSize())
+ self.SetSizeHints(self.GetSize())
# Set value/type-specific formatting
self._applyFormatting()
self._SetInitialValue()
if self._autofit:
- size = self._CalcSize()
- self.SetSizeHints(size)
- self.SetClientSize(size)
+ self.SetClientSize(self._CalcSize())
+ self.SetSizeHints(self.GetSize())
# Set value/type-specific formatting
self._applyFormatting()
## 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:
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:
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
## 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)
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
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:
"""
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,
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:
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
## 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()
#
## 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
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:
if not wx.Validator_IsSilent():
wx.Bell()
## dbg(indent=0)
- return False
+ return None, -1
# else...
text = self._eraseSelection()
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:
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
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:
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)
## 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
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:
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
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):
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())
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):
## 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.
+##
## 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.
## 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.
##
##
## 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