# be a good place to implement the 2.3 logger class
from wx.tools.dbg import Logger
##dbg = Logger()
-##dbg(enable=0)
+##dbg(enable=1)
## ---------- ---------- ---------- ---------- ---------- ---------- ----------
## Because calling SetSelection programmatically does not fire EVT_COMBOBOX
this event was generated."""
return self.__selection
+class MaskedComboBoxEventHandler(wx.EvtHandler):
+ """
+ This handler ensures that the derived control can react to events
+ from the base control before any external handlers run, to ensure
+ proper behavior.
+ """
+ def __init__(self, combobox):
+ wx.EvtHandler.__init__(self)
+ self.combobox = combobox
+ combobox.PushEventHandler(self)
+ self.Bind(wx.EVT_SET_FOCUS, self.combobox._OnFocus ) ## defeat automatic full selection
+ self.Bind(wx.EVT_KILL_FOCUS, self.combobox._OnKillFocus ) ## run internal validator
+ self.Bind(wx.EVT_LEFT_DCLICK, self.combobox._OnDoubleClick) ## select field under cursor on dclick
+ self.Bind(wx.EVT_RIGHT_UP, self.combobox._OnContextMenu ) ## bring up an appropriate context menu
+ self.Bind(wx.EVT_CHAR, self.combobox._OnChar ) ## handle each keypress
+ self.Bind(wx.EVT_KEY_DOWN, self.combobox._OnKeyDownInComboBox ) ## for special processing of up/down keys
+ self.Bind(wx.EVT_KEY_DOWN, self.combobox._OnKeyDown ) ## for processing the rest of the control keys
+ ## (next in evt chain)
+ self.Bind(wx.EVT_COMBOBOX, self.combobox._OnDropdownSelect ) ## to bring otherwise completely independent base
+ ## ctrl selection into maskededit framework
+ self.Bind(wx.EVT_TEXT, self.combobox._OnTextChange ) ## color control appropriately & keep
+ ## track of previous value for undo
+
+
class BaseMaskedComboBox( wx.ComboBox, MaskedEditMixin ):
"""
self.SetClientSize(self._CalcSize())
width = self.GetSize().width
height = self.GetBestSize().height
- self.SetBestFittingSize((width, height))
+ self.SetInitialSize((width, height))
if value:
self._SetKeycodeHandler(wx.WXK_UP, self._OnSelectChoice)
self._SetKeycodeHandler(wx.WXK_DOWN, self._OnSelectChoice)
+ self.replace_next_combobox_event = False
+ self.correct_selection = -1
+
if setupEventHandling:
- ## Setup event handlers
- self.Bind(wx.EVT_SET_FOCUS, self._OnFocus ) ## defeat automatic full selection
- self.Bind(wx.EVT_KILL_FOCUS, self._OnKillFocus ) ## run internal validator
- self.Bind(wx.EVT_LEFT_DCLICK, self._OnDoubleClick) ## select field under cursor on dclick
- self.Bind(wx.EVT_RIGHT_UP, self._OnContextMenu ) ## bring up an appropriate context menu
- self.Bind(wx.EVT_CHAR, self._OnChar ) ## handle each keypress
- 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_TEXT, self._OnTextChange ) ## color control appropriately & keep
- ## track of previous value for undo
+ ## Setup event handling functions through event handler object,
+ ## to guarantee processing prior to giving event callbacks from
+ ## outside the class:
+ self.evt_handler = MaskedComboBoxEventHandler(self)
+ self.Bind(wx.EVT_WINDOW_DESTROY, self.OnWindowDestroy )
return "<MaskedComboBox: %s>" % self.GetValue()
+ def OnWindowDestroy(self, event):
+ # clean up associated event handler object:
+ if self.RemoveEventHandler(self.evt_handler):
+ self.evt_handler.Destroy()
+ event.Skip()
+
+
def _CalcSize(self, size=None):
"""
Calculate automatic size if allowed; augment base mixin function
width = self.GetSize().width
height = self.GetBestSize().height
## dbg('setting client size to:', (width, height))
- self.SetBestFittingSize((width, height))
+ self.SetInitialSize((width, height))
def _GetSelection(self):
Allow mixin to get the text selection of this control.
REQUIRED by any class derived from MaskedEditMixin.
"""
+## dbg('MaskedComboBox::_GetSelection()')
return self.GetMark()
def _SetSelection(self, sel_start, sel_to):
Allow mixin to set the text selection of this control.
REQUIRED by any class derived from MaskedEditMixin.
"""
+## dbg('MaskedComboBox::_SetSelection: setting mark to (%d, %d)' % (sel_start, sel_to))
return self.SetMark( sel_start, sel_to )
def _GetInsertionPoint(self):
- return self.GetInsertionPoint()
+## dbg('MaskedComboBox::_GetInsertionPoint()', indent=1)
+## ret = self.GetInsertionPoint()
+ # work around new bug in 2.5, in which the insertion point
+ # returned is always at the right side of the selection,
+ # rather than the start, as is the case with TextCtrl.
+ ret = self.GetMark()[0]
+## dbg('returned', ret, indent=0)
+ return ret
def _SetInsertionPoint(self, pos):
+## dbg('MaskedComboBox::_SetInsertionPoint(%d)' % pos)
self.SetInsertionPoint(pos)
+ def IsEmpty(*args, **kw):
+ return MaskedEditMixin.IsEmpty(*args, **kw)
+
+
def _GetValue(self):
"""
Allow mixin to get the raw value of the control with this function.
# Record current selection and insertion point, for undo
self._prevSelection = self._GetSelection()
self._prevInsertionPoint = self._GetInsertionPoint()
+## dbg('MaskedComboBox::_SetValue(%s), selection beforehand: %d' % (value, self.GetSelection()))
wx.ComboBox.SetValue(self, value)
+## dbg('MaskedComboBox::_SetValue(%s), selection now: %d' % (value, self.GetSelection()))
# text change events don't always fire, so we check validity here
# to make certain formatting is applied:
self._CheckValid()
masked control. NOTE: this must be done in the class derived
from the base wx control.
"""
+## dbg('MaskedComboBox::SetValue(%s)' % value, indent=1)
if not self._mask:
wx.ComboBox.SetValue(value) # revert to base control behavior
+## dbg('no mask; deferring to base class', indent=0)
return
# else...
# empty previous contents, replacing entire value:
+## dbg('MaskedComboBox::SetValue: selection beforehand: %d' % (self.GetSelection()))
self._SetInsertionPoint(0)
self._SetSelection(0, self._masklength)
dateparts = value.split(' ')
dateparts[0] = self._adjustDate(dateparts[0], fixcentury=True)
value = string.join(dateparts, ' ')
-## dbg('adjusted value: "%s"' % value)
value = self._Paste(value, raise_on_invalid=True, just_return_value=True)
else:
raise
+## dbg('adjusted value: "%s"' % value)
- self._SetValue(value)
-#### dbg('queuing insertion after .SetValue', replace_to)
- wx.CallAfter(self._SetInsertionPoint, replace_to)
- wx.CallAfter(self._SetSelection, replace_to, replace_to)
+ # Attempt to compensate for fact that calling .SetInsertionPoint() makes the
+ # selection index -1, even if the resulting set value is in the list.
+ # So, if we are setting a value that's in the list, use index selection instead.
+ if value in self._choices:
+ index = self._choices.index(value)
+ self._prevValue = self._curValue
+ self._curValue = self._choices[index]
+ self._ctrl_constraints._autoCompleteIndex = index
+ self.SetSelection(index)
+ else:
+ self._SetValue(value)
+#### dbg('queuing insertion after .SetValue', replace_to)
+ wx.CallAfter(self._SetInsertionPoint, replace_to)
+ wx.CallAfter(self._SetSelection, replace_to, replace_to)
+## dbg(indent=0)
def _Refresh(self):
wx.ComboBox.Append( self, choice )
- def GetMark(self):
- """
- This function is a hack to make up for the fact that wx.ComboBox has no
- method for returning the selected portion of its edit control. It
- works, but has the nasty side effect of generating lots of intermediate
- events.
- """
-## dbg(suspend=1) # turn off debugging around this function
-## dbg('MaskedComboBox::GetMark', indent=1)
- if self.__readonly:
-## dbg(indent=0)
- return 0, 0 # no selection possible for editing
-## sel_start, sel_to = wxComboBox.GetMark(self) # what I'd *like* to have!
- sel_start = sel_to = self.GetInsertionPoint()
-## dbg("current sel_start:", sel_start)
- value = self.GetValue()
-## dbg('value: "%s"' % value)
-
- self._ignoreChange = True # tell _OnTextChange() to ignore next event (if any)
-
- wx.ComboBox.Cut(self)
- newvalue = self.GetValue()
-## dbg("value after Cut operation:", newvalue)
-
- if newvalue != value: # something was selected; calculate extent
-## dbg("something selected")
- sel_to = sel_start + len(value) - len(newvalue)
- wx.ComboBox.SetValue(self, value) # restore original value and selection (still ignoring change)
- wx.ComboBox.SetInsertionPoint(self, sel_start)
- wx.ComboBox.SetMark(self, sel_start, sel_to)
-
- self._ignoreChange = False # tell _OnTextChange() to pay attn again
-
-## dbg('computed selection:', sel_start, sel_to, indent=0, suspend=0)
- return sel_start, sel_to
+ # Not all wx platform implementations have .GetMark, so we make the following test,
+ # and fall back to our old hack if they don't...
+ #
+ if not hasattr(wx.ComboBox, 'GetMark'):
+ def GetMark(self):
+ """
+ This function is a hack to make up for the fact that wx.ComboBox has no
+ method for returning the selected portion of its edit control. It
+ works, but has the nasty side effect of generating lots of intermediate
+ events.
+ """
+## dbg(suspend=1) # turn off debugging around this function
+## dbg('MaskedComboBox::GetMark', indent=1)
+ if self.__readonly:
+## dbg(indent=0)
+ return 0, 0 # no selection possible for editing
+## sel_start, sel_to = wxComboBox.GetMark(self) # what I'd *like* to have!
+ sel_start = sel_to = self.GetInsertionPoint()
+## dbg("current sel_start:", sel_start)
+ value = self.GetValue()
+## dbg('value: "%s"' % value)
+
+ self._ignoreChange = True # tell _OnTextChange() to ignore next event (if any)
+
+ wx.ComboBox.Cut(self)
+ newvalue = self.GetValue()
+## dbg("value after Cut operation:", newvalue)
+
+ if newvalue != value: # something was selected; calculate extent
+## dbg("something selected")
+ sel_to = sel_start + len(value) - len(newvalue)
+ wx.ComboBox.SetValue(self, value) # restore original value and selection (still ignoring change)
+ wx.ComboBox.SetInsertionPoint(self, sel_start)
+ wx.ComboBox.SetMark(self, sel_start, sel_to)
+
+ self._ignoreChange = False # tell _OnTextChange() to pay attn again
+
+## dbg('computed selection:', sel_start, sel_to, indent=0, suspend=0)
+ return sel_start, sel_to
+ else:
+ def GetMark(self):
+## dbg('MaskedComboBox::GetMark()', indent = 1)
+ ret = wx.ComboBox.GetMark(self)
+## dbg('returned', ret, indent=0)
+ return ret
def SetSelection(self, index):
Necessary override for bookkeeping on choice selection, to keep current value
current.
"""
-## dbg('MaskedComboBox::SetSelection(%d)' % index)
+## dbg('MaskedComboBox::SetSelection(%d)' % index, indent=1)
if self._mask:
self._prevValue = self._curValue
- self._curValue = self._choices[index]
self._ctrl_constraints._autoCompleteIndex = index
+ if index != -1:
+ self._curValue = self._choices[index]
+ else:
+ self._curValue = None
wx.ComboBox.SetSelection(self, index)
+## dbg('selection now: %d' % self.GetCurrentSelection(), indent=0)
def _OnKeyDownInComboBox(self, event):
"""
- This function is necessary because navigation and control key
- events do not seem to normally be seen by the wxComboBox's
- EVT_CHAR routine. (Tabs don't seem to be visible no matter
- what... {:-( )
+ This function is necessary because navigation and control key events
+ do not seem to normally be seen by the wxComboBox's EVT_CHAR routine.
+ (Tabs don't seem to be visible no matter what, except for CB_READONLY
+ controls, for some bizarre reason... {:-( )
"""
+ key = event.GetKeyCode()
+## dbg('MaskedComboBox::OnKeyDownInComboBox(%d)' % key)
if event.GetKeyCode() in self._nav + self._control:
- self._OnChar(event)
- return
+ if not self._IsEditable():
+ # WANTS_CHARS with CB_READONLY apparently prevents navigation on WXK_TAB;
+ # ensure we can still navigate properly, as maskededit mixin::OnChar assumes
+ # that event.Skip() will just work, but it doesn't:
+ if self._keyhandlers.has_key(key):
+ self._keyhandlers[key](event)
+ # else pass
+ else:
+## dbg('calling OnChar()')
+ self._OnChar(event)
else:
event.Skip() # let mixin default KeyDown behavior occur
+## dbg(indent=0)
+
+
+ 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.
+ """
+## dbg('MaskedComboBox::OnDropdownSelect(%d)' % event.GetSelection(), indent=1)
+ if self.replace_next_combobox_event:
+## dbg('replacing EVT_COMBOBOX')
+ self.replace_next_combobox_event = False
+ self._OnAutoSelect(self._ctrl_constraints, self.correct_selection)
+ else:
+## dbg('skipping EVT_COMBOBOX')
+ event.Skip()
+## dbg(indent=0)
def _OnSelectChoice(self, event):
Override mixin (empty) autocomplete handler, so that autocompletion causes
combobox to update appropriately.
"""
-## dbg('MaskedComboBox::OnAutoSelect', field._index, indent=1)
+## dbg('MaskedComboBox::OnAutoSelect(%d, %d)' % (field._index, match_index), indent=1)
## field._autoCompleteIndex = match_index
if field == self._ctrl_constraints:
self.SetSelection(match_index)
MaskedComboBoxSelectEvent( self.GetId(), match_index, self ) )
self._CheckValid()
## dbg('field._autoCompleteIndex:', match_index)
-## dbg('self.GetSelection():', self.GetSelection())
+## dbg('self.GetCurrentSelection():', self.GetCurrentSelection())
end = self._goEnd(getPosOnly=True)
## dbg('scheduling set of end position to:', end)
# work around bug in wx 2.5
item in the list. (and then does the usual OnReturn bit.)
"""
## dbg('MaskedComboBox::OnReturn', indent=1)
-## dbg('current value: "%s"' % self.GetValue(), 'current index:', self.GetSelection())
- if self.GetSelection() == -1 and self.GetValue().lower().strip() in self._ctrl_constraints._compareChoices:
- wx.CallAfter(self.SetSelection, self._ctrl_constraints._autoCompleteIndex)
-
+## dbg('current value: "%s"' % self.GetValue(), 'current selection:', self.GetCurrentSelection())
+ if self.GetCurrentSelection() == -1 and self.GetValue().lower().strip() in self._ctrl_constraints._compareChoices:
+## dbg('attempting to correct the selection to make it %d' % self._ctrl_constraints._autoCompleteIndex)
+## wx.CallAfter(self.SetSelection, self._ctrl_constraints._autoCompleteIndex)
+ self.replace_next_combobox_event = True
+ self.correct_selection = self._ctrl_constraints._autoCompleteIndex
event.m_keyCode = wx.WXK_TAB
event.Skip()
## dbg(indent=0)
+ def _LostFocus(self):
+## dbg('MaskedComboBox::LostFocus; Selection=%d, value="%s"' % (self.GetSelection(), self.GetValue()))
+ if self.GetCurrentSelection() == -1 and self.GetValue().lower().strip() in self._ctrl_constraints._compareChoices:
+## dbg('attempting to correct the selection to make it %d' % self._ctrl_constraints._autoCompleteIndex)
+ wx.CallAfter(self.SetSelection, self._ctrl_constraints._autoCompleteIndex)
+
+
class ComboBox( BaseMaskedComboBox, MaskedEditAccessorsMixin ):
"""
The "user-visible" masked combobox control, this class is
__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.
+## NOTE: due to misbehavior of wx.ComboBox re: losing all concept of the
+## current selection index if SetInsertionPoint() is called, which is required
+## to support masked .SetValue(), this control is flaky about retaining selection
+## information. I can't truly fix this without major changes to the base control,
+## but I've tried to compensate as best I can.
+## TODO: investigate replacing base control with ComboCtrl instead...
+## 2. Fixed navigation in readonly masked combobox, which was not working because
+## the base control doesn't do navigation if style=CB_READONLY|WANTS_CHARS.
+##
+##
+## 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
+## while taking advantage of improvements therein for some platforms.
+##
## Version 1.2
## 1. Converted docstrings to reST format, added doc for ePyDoc.
## 2. Renamed helper functions, vars etc. not intended to be visible in public