from wx.tools.dbg import Logger
dbg = Logger()
-##dbg(enable=0)
+##dbg(enable=1)
## ---------- ---------- ---------- ---------- ---------- ---------- ----------
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()
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()
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
## 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')
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:
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)
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):
"""
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:
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
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)
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)
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
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
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)
pos = pos+2
if newvalue != value:
+## dbg('old value: "%s"\nnew value: "%s"' % (value, newvalue))
self._SetValue(newvalue)
self._SetInsertionPoint(pos)
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
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(newtext.rstrip()))
match_field = self._ctrl_constraints
if self._ctrl_constraints._insertRight:
# adjust position to just after partial match in control:
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()
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
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):
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
## 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.