1 #----------------------------------------------------------------------------
2 # Name: masked.combobox.py
4 # Email: wsadkin@nameconnector.com
6 # Copyright: (c) 2003 by Will Sadkin, 2003
8 # License: wxWidgets license
9 #----------------------------------------------------------------------------
11 # This masked edit class allows for the semantics of masked controls
12 # to be applied to combo boxes.
14 #----------------------------------------------------------------------------
17 Provides masked edit capabilities within a ComboBox format, as well as
18 a base class from which you can derive masked comboboxes tailored to a specific
19 function. See maskededit module overview for how to configure the control.
22 import wx
, types
, string
23 from wx
.lib
.masked
import *
25 # jmg 12/9/03 - when we cut ties with Py 2.2 and earlier, this would
26 # be a good place to implement the 2.3 logger class
27 from wx
.tools
.dbg
import Logger
31 ## ---------- ---------- ---------- ---------- ---------- ---------- ----------
32 ## Because calling SetSelection programmatically does not fire EVT_COMBOBOX
33 ## events, we have to do it ourselves when we auto-complete.
34 class MaskedComboBoxSelectEvent(wx
.PyCommandEvent
):
36 Because calling SetSelection programmatically does not fire EVT_COMBOBOX
37 events, the derived control has to do it itself when it auto-completes.
39 def __init__(self
, id, selection
= 0, object=None):
40 wx
.PyCommandEvent
.__init
__(self
, wx
.wxEVT_COMMAND_COMBOBOX_SELECTED
, id)
42 self
.__selection
= selection
43 self
.SetEventObject(object)
45 def GetSelection(self
):
46 """Retrieve the value of the control at the time
47 this event was generated."""
48 return self
.__selection
51 class BaseMaskedComboBox( wx
.ComboBox
, MaskedEditMixin
):
53 Base class for generic masked edit comboboxes; allows auto-complete of values.
54 It is not meant to be instantiated directly, but rather serves as a base class
55 for any subsequent refinements.
57 def __init__( self
, parent
, id=-1, value
= '',
58 pos
= wx
.DefaultPosition
,
59 size
= wx
.DefaultSize
,
61 style
= wx
.CB_DROPDOWN
,
62 validator
= wx
.DefaultValidator
,
63 name
= "maskedComboBox",
64 setupEventHandling
= True, ## setup event handling by default):
68 kwargs
['choices'] = choices
## set up maskededit to work with choice list too
70 ## Since combobox completion is case-insensitive, always validate same way
71 if not kwargs
.has_key('compareNoCase'):
72 kwargs
['compareNoCase'] = True
74 MaskedEditMixin
.__init
__( self
, name
, **kwargs
)
76 self
._choices
= self
._ctrl
_constraints
._choices
77 ## dbg('self._choices:', self._choices)
79 if self
._ctrl
_constraints
._alignRight
:
80 choices
= [choice
.rjust(self
._masklength
) for choice
in choices
]
82 choices
= [choice
.ljust(self
._masklength
) for choice
in choices
]
84 wx
.ComboBox
.__init
__(self
, parent
, id, value
='',
86 choices
=choices
, style
=style|wx
.WANTS_CHARS
,
89 self
.controlInitialized
= True
91 self
._PostInit
(style
=style
, setupEventHandling
=setupEventHandling
,
92 name
=name
, value
=value
, **kwargs
)
95 def _PostInit(self
, style
=wx
.CB_DROPDOWN
,
96 setupEventHandling
= True, ## setup event handling by default):
97 name
= "maskedComboBox", value
='', **kwargs
):
99 # This is necessary, because wxComboBox currently provides no
100 # method for determining later if this was specified in the
101 # constructor for the control...
102 self
.__readonly
= style
& wx
.CB_READONLY
== wx
.CB_READONLY
104 if not hasattr(self
, 'controlInitialized'):
106 self
.controlInitialized
= True ## must have been called via XRC, therefore base class is constructed
107 if not kwargs
.has_key('choices'):
109 kwargs
['choices'] = choices
## set up maskededit to work with choice list too
112 ## Since combobox completion is case-insensitive, always validate same way
113 if not kwargs
.has_key('compareNoCase'):
114 kwargs
['compareNoCase'] = True
116 MaskedEditMixin
.__init
__( self
, name
, **kwargs
)
118 self
._choices
= self
._ctrl
_constraints
._choices
119 ## dbg('self._choices:', self._choices)
121 if self
._ctrl
_constraints
._alignRight
:
122 choices
= [choice
.rjust(self
._masklength
) for choice
in choices
]
124 choices
= [choice
.ljust(self
._masklength
) for choice
in choices
]
125 wx
.ComboBox
.Clear(self
)
126 wx
.ComboBox
.AppendItems(self
, choices
)
129 # Set control font - fixed width by default
133 self
.SetClientSize(self
._CalcSize
())
134 width
= self
.GetSize().width
135 height
= self
.GetBestSize().height
136 self
.SetInitialSize((width
, height
))
140 # ensure value is width of the mask of the control:
141 if self
._ctrl
_constraints
._alignRight
:
142 value
= value
.rjust(self
._masklength
)
144 value
= value
.ljust(self
._masklength
)
147 self
.SetStringSelection(value
)
149 self
._SetInitialValue
(value
)
152 self
._SetKeycodeHandler
(wx
.WXK_UP
, self
._OnSelectChoice
)
153 self
._SetKeycodeHandler
(wx
.WXK_DOWN
, self
._OnSelectChoice
)
155 if setupEventHandling
:
156 ## Setup event handlers
157 self
.Bind(wx
.EVT_SET_FOCUS
, self
._OnFocus
) ## defeat automatic full selection
158 self
.Bind(wx
.EVT_KILL_FOCUS
, self
._OnKillFocus
) ## run internal validator
159 self
.Bind(wx
.EVT_LEFT_DCLICK
, self
._OnDoubleClick
) ## select field under cursor on dclick
160 self
.Bind(wx
.EVT_RIGHT_UP
, self
._OnContextMenu
) ## bring up an appropriate context menu
161 self
.Bind(wx
.EVT_CHAR
, self
._OnChar
) ## handle each keypress
162 self
.Bind(wx
.EVT_KEY_DOWN
, self
._OnKeyDownInComboBox
) ## for special processing of up/down keys
163 self
.Bind(wx
.EVT_KEY_DOWN
, self
._OnKeyDown
) ## for processing the rest of the control keys
164 ## (next in evt chain)
165 self
.Bind(wx
.EVT_TEXT
, self
._OnTextChange
) ## color control appropriately & keep
166 ## track of previous value for undo
171 return "<MaskedComboBox: %s>" % self
.GetValue()
174 def _CalcSize(self
, size
=None):
176 Calculate automatic size if allowed; augment base mixin function
177 to account for the selector button.
179 size
= self
._calcSize
(size
)
180 return (size
[0]+20, size
[1])
183 def SetFont(self
, *args
, **kwargs
):
184 """ Set the font, then recalculate control size, if appropriate. """
185 wx
.ComboBox
.SetFont(self
, *args
, **kwargs
)
187 ## dbg('calculated size:', self._CalcSize())
188 self
.SetClientSize(self
._CalcSize
())
189 width
= self
.GetSize().width
190 height
= self
.GetBestSize().height
191 ## dbg('setting client size to:', (width, height))
192 self
.SetInitialSize((width
, height
))
195 def _GetSelection(self
):
197 Allow mixin to get the text selection of this control.
198 REQUIRED by any class derived from MaskedEditMixin.
200 ## dbg('MaskedComboBox::_GetSelection()')
201 return self
.GetMark()
203 def _SetSelection(self
, sel_start
, sel_to
):
205 Allow mixin to set the text selection of this control.
206 REQUIRED by any class derived from MaskedEditMixin.
208 ## dbg('MaskedComboBox::_SetSelection: setting mark to (%d, %d)' % (sel_start, sel_to))
209 return self
.SetMark( sel_start
, sel_to
)
212 def _GetInsertionPoint(self
):
213 ## dbg('MaskedComboBox::_GetInsertionPoint()', indent=1)
214 ## ret = self.GetInsertionPoint()
215 # work around new bug in 2.5, in which the insertion point
216 # returned is always at the right side of the selection,
217 # rather than the start, as is the case with TextCtrl.
218 ret
= self
.GetMark()[0]
219 ## dbg('returned', ret, indent=0)
222 def _SetInsertionPoint(self
, pos
):
223 ## dbg('MaskedComboBox::_SetInsertionPoint(%d)' % pos)
224 self
.SetInsertionPoint(pos
)
227 def IsEmpty(*args
, **kw
):
228 return MaskedEditMixin
.IsEmpty(*args
, **kw
)
233 Allow mixin to get the raw value of the control with this function.
234 REQUIRED by any class derived from MaskedEditMixin.
236 return self
.GetValue()
238 def _SetValue(self
, value
):
240 Allow mixin to set the raw value of the control with this function.
241 REQUIRED by any class derived from MaskedEditMixin.
243 # For wxComboBox, ensure that values are properly padded so that
244 # if varying length choices are supplied, they always show up
245 # in the window properly, and will be the appropriate length
247 if self
._ctrl
_constraints
._alignRight
:
248 value
= value
.rjust(self
._masklength
)
250 value
= value
.ljust(self
._masklength
)
252 # Record current selection and insertion point, for undo
253 self
._prevSelection
= self
._GetSelection
()
254 self
._prevInsertionPoint
= self
._GetInsertionPoint
()
255 wx
.ComboBox
.SetValue(self
, value
)
256 # text change events don't always fire, so we check validity here
257 # to make certain formatting is applied:
260 def SetValue(self
, value
):
262 This function redefines the externally accessible .SetValue to be
263 a smart "paste" of the text in question, so as not to corrupt the
264 masked control. NOTE: this must be done in the class derived
265 from the base wx control.
268 wx
.ComboBox
.SetValue(value
) # revert to base control behavior
271 # empty previous contents, replacing entire value:
272 self
._SetInsertionPoint
(0)
273 self
._SetSelection
(0, self
._masklength
)
275 if( len(value
) < self
._masklength
# value shorter than control
276 and (self
._isFloat
or self
._isInt
) # and it's a numeric control
277 and self
._ctrl
_constraints
._alignRight
): # and it's a right-aligned control
278 # try to intelligently "pad out" the value to the right size:
279 value
= self
._template
[0:self
._masklength
- len(value
)] + value
280 ## dbg('padded value = "%s"' % value)
282 # For wxComboBox, ensure that values are properly padded so that
283 # if varying length choices are supplied, they always show up
284 # in the window properly, and will be the appropriate length
286 elif self
._ctrl
_constraints
._alignRight
:
287 value
= value
.rjust(self
._masklength
)
289 value
= value
.ljust(self
._masklength
)
292 # make SetValue behave the same as if you had typed the value in:
294 value
, replace_to
= self
._Paste
(value
, raise_on_invalid
=True, just_return_value
=True)
296 self
._isNeg
= False # (clear current assumptions)
297 value
= self
._adjustFloat
(value
)
299 self
._isNeg
= False # (clear current assumptions)
300 value
= self
._adjustInt
(value
)
301 elif self
._isDate
and not self
.IsValid(value
) and self
._4digityear
:
302 value
= self
._adjustDate
(value
, fixcentury
=True)
304 # If date, year might be 2 digits vs. 4; try adjusting it:
305 if self
._isDate
and self
._4digityear
:
306 dateparts
= value
.split(' ')
307 dateparts
[0] = self
._adjustDate
(dateparts
[0], fixcentury
=True)
308 value
= string
.join(dateparts
, ' ')
309 ## dbg('adjusted value: "%s"' % value)
310 value
= self
._Paste
(value
, raise_on_invalid
=True, just_return_value
=True)
314 self
._SetValue
(value
)
315 #### dbg('queuing insertion after .SetValue', replace_to)
316 wx
.CallAfter(self
._SetInsertionPoint
, replace_to
)
317 wx
.CallAfter(self
._SetSelection
, replace_to
, replace_to
)
322 Allow mixin to refresh the base control with this function.
323 REQUIRED by any class derived from MaskedEditMixin.
325 wx
.ComboBox
.Refresh(self
)
329 This function redefines the externally accessible .Refresh() to
330 validate the contents of the masked control as it refreshes.
331 NOTE: this must be done in the class derived from the base wx control.
337 def _IsEditable(self
):
339 Allow mixin to determine if the base control is editable with this function.
340 REQUIRED by any class derived from MaskedEditMixin.
342 return not self
.__readonly
347 This function redefines the externally accessible .Cut to be
348 a smart "erase" of the text in question, so as not to corrupt the
349 masked control. NOTE: this must be done in the class derived
350 from the base wx control.
353 self
._Cut
() # call the mixin's Cut method
355 wx
.ComboBox
.Cut(self
) # else revert to base control behavior
360 This function redefines the externally accessible .Paste to be
361 a smart "paste" of the text in question, so as not to corrupt the
362 masked control. NOTE: this must be done in the class derived
363 from the base wx control.
366 self
._Paste
() # call the mixin's Paste method
368 wx
.ComboBox
.Paste(self
) # else revert to base control behavior
373 This function defines the undo operation for the control. (The default
379 wx
.ComboBox
.Undo() # else revert to base control behavior
381 def Append( self
, choice
, clientData
=None ):
383 This base control function override is necessary so the control can keep track
384 of any additions to the list of choices, because wx.ComboBox doesn't have an
385 accessor for the choice list. The code here is the same as in the
386 SetParameters() mixin function, but is done for the individual value
387 as appended, so the list can be built incrementally without speed penalty.
390 if type(choice
) not in (types
.StringType
, types
.UnicodeType
):
391 raise TypeError('%s: choices must be a sequence of strings' % str(self
._index
))
392 elif not self
.IsValid(choice
):
393 raise ValueError('%s: "%s" is not a valid value for the control as specified.' % (str(self
._index
), choice
))
395 if not self
._ctrl
_constraints
._choices
:
396 self
._ctrl
_constraints
._compareChoices
= []
397 self
._ctrl
_constraints
._choices
= []
400 compareChoice
= choice
.strip()
402 if self
._ctrl
_constraints
._compareNoCase
:
403 compareChoice
= compareChoice
.lower()
405 if self
._ctrl
_constraints
._alignRight
:
406 choice
= choice
.rjust(self
._masklength
)
408 choice
= choice
.ljust(self
._masklength
)
409 if self
._ctrl
_constraints
._fillChar
!= ' ':
410 choice
= choice
.replace(' ', self
._fillChar
)
411 ## dbg('updated choice:', choice)
414 self
._ctrl
_constraints
._compareChoices
.append(compareChoice
)
415 self
._ctrl
_constraints
._choices
.append(choice
)
416 self
._choices
= self
._ctrl
_constraints
._choices
# (for shorthand)
418 if( not self
.IsValid(choice
) and
419 (not self
._ctrl
_constraints
.IsEmpty(choice
) or
420 (self
._ctrl
_constraints
.IsEmpty(choice
) and self
._ctrl
_constraints
._validRequired
) ) ):
421 raise ValueError('"%s" is not a valid value for the control "%s" as specified.' % (choice
, self
.name
))
423 wx
.ComboBox
.Append(self
, choice
, clientData
)
426 def AppendItems( self
, choices
):
428 AppendItems() is handled in terms of Append, to avoid code replication.
430 for choice
in choices
:
436 This base control function override is necessary so the derived control can
437 keep track of any additions to the list of choices, because wx.ComboBox
438 doesn't have an accessor for the choice list.
442 self
._ctrl
_constraints
._autoCompleteIndex
= -1
443 if self
._ctrl
_constraints
._choices
:
444 self
.SetCtrlParameters(choices
=[])
445 wx
.ComboBox
.Clear(self
)
448 def _OnCtrlParametersChanged(self
):
450 This overrides the mixin's default OnCtrlParametersChanged to detect
451 changes in choice list, so masked.Combobox can update the base control:
453 if self
.controlInitialized
and self
._choices
!= self
._ctrl
_constraints
._choices
:
454 wx
.ComboBox
.Clear(self
)
455 self
._choices
= self
._ctrl
_constraints
._choices
456 for choice
in self
._choices
:
457 wx
.ComboBox
.Append( self
, choice
)
460 # Not all wx platform implementations have .GetMark, so we make the following test,
461 # and fall back to our old hack if they don't...
463 if not hasattr(wx
.ComboBox
, 'GetMark'):
466 This function is a hack to make up for the fact that wx.ComboBox has no
467 method for returning the selected portion of its edit control. It
468 works, but has the nasty side effect of generating lots of intermediate
471 ## dbg(suspend=1) # turn off debugging around this function
472 ## dbg('MaskedComboBox::GetMark', indent=1)
475 return 0, 0 # no selection possible for editing
476 ## sel_start, sel_to = wxComboBox.GetMark(self) # what I'd *like* to have!
477 sel_start
= sel_to
= self
.GetInsertionPoint()
478 ## dbg("current sel_start:", sel_start)
479 value
= self
.GetValue()
480 ## dbg('value: "%s"' % value)
482 self
._ignoreChange
= True # tell _OnTextChange() to ignore next event (if any)
484 wx
.ComboBox
.Cut(self
)
485 newvalue
= self
.GetValue()
486 ## dbg("value after Cut operation:", newvalue)
488 if newvalue
!= value
: # something was selected; calculate extent
489 ## dbg("something selected")
490 sel_to
= sel_start
+ len(value
) - len(newvalue
)
491 wx
.ComboBox
.SetValue(self
, value
) # restore original value and selection (still ignoring change)
492 wx
.ComboBox
.SetInsertionPoint(self
, sel_start
)
493 wx
.ComboBox
.SetMark(self
, sel_start
, sel_to
)
495 self
._ignoreChange
= False # tell _OnTextChange() to pay attn again
497 ## dbg('computed selection:', sel_start, sel_to, indent=0, suspend=0)
498 return sel_start
, sel_to
501 ## dbg('MaskedComboBox::GetMark()', indent = 1)
502 ret
= wx
.ComboBox
.GetMark(self
)
503 ## dbg('returned', ret, indent=0)
507 def SetSelection(self
, index
):
509 Necessary override for bookkeeping on choice selection, to keep current value
512 ## dbg('MaskedComboBox::SetSelection(%d)' % index)
514 self
._prevValue
= self
._curValue
515 self
._curValue
= self
._choices
[index
]
516 self
._ctrl
_constraints
._autoCompleteIndex
= index
517 wx
.ComboBox
.SetSelection(self
, index
)
520 def _OnKeyDownInComboBox(self
, event
):
522 This function is necessary because navigation and control key
523 events do not seem to normally be seen by the wxComboBox's
524 EVT_CHAR routine. (Tabs don't seem to be visible no matter
527 if event
.GetKeyCode() in self
._nav
+ self
._control
:
531 event
.Skip() # let mixin default KeyDown behavior occur
534 def _OnSelectChoice(self
, event
):
536 This function appears to be necessary, because the processing done
537 on the text of the control somehow interferes with the combobox's
538 selection mechanism for the arrow keys.
540 ## dbg('MaskedComboBox::OnSelectChoice', indent=1)
546 value
= self
.GetValue().strip()
548 if self
._ctrl
_constraints
._compareNoCase
:
549 value
= value
.lower()
551 if event
.GetKeyCode() == wx
.WXK_UP
:
555 match_index
, partial_match
= self
._autoComplete
(
557 self
._ctrl
_constraints
._compareChoices
,
559 self
._ctrl
_constraints
._compareNoCase
,
560 current_index
= self
._ctrl
_constraints
._autoCompleteIndex
)
561 if match_index
is not None:
562 ## dbg('setting selection to', match_index)
563 # issue appropriate event to outside:
564 self
._OnAutoSelect
(self
._ctrl
_constraints
, match_index
=match_index
)
566 keep_processing
= False
568 pos
= self
._adjustPos
(self
._GetInsertionPoint
(), event
.GetKeyCode())
569 field
= self
._FindField
(pos
)
570 if self
.IsEmpty() or not field
._hasList
:
571 ## dbg('selecting 1st value in list')
572 self
._OnAutoSelect
(self
._ctrl
_constraints
, match_index
=0)
574 keep_processing
= False
576 # attempt field-level auto-complete
578 keep_processing
= self
._OnAutoCompleteField
(event
)
579 ## dbg('keep processing?', keep_processing, indent=0)
580 return keep_processing
583 def _OnAutoSelect(self
, field
, match_index
):
585 Override mixin (empty) autocomplete handler, so that autocompletion causes
586 combobox to update appropriately.
588 ## dbg('MaskedComboBox::OnAutoSelect', field._index, indent=1)
589 ## field._autoCompleteIndex = match_index
590 if field
== self
._ctrl
_constraints
:
591 self
.SetSelection(match_index
)
592 ## dbg('issuing combo selection event')
593 self
.GetEventHandler().ProcessEvent(
594 MaskedComboBoxSelectEvent( self
.GetId(), match_index
, self
) )
596 ## dbg('field._autoCompleteIndex:', match_index)
597 ## dbg('self.GetSelection():', self.GetSelection())
598 end
= self
._goEnd
(getPosOnly
=True)
599 ## dbg('scheduling set of end position to:', end)
600 # work around bug in wx 2.5
601 wx
.CallAfter(self
.SetInsertionPoint
, 0)
602 wx
.CallAfter(self
.SetInsertionPoint
, end
)
606 def _OnReturn(self
, event
):
608 For wx.ComboBox, it seems that if you hit return when the dropdown is
609 dropped, the event that dismisses the dropdown will also blank the
610 control, because of the implementation of wxComboBox. So this function
611 examines the selection and if it is -1, and the value according to
612 (the base control!) is a value in the list, then it schedules a
613 programmatic wxComboBox.SetSelection() call to pick the appropriate
614 item in the list. (and then does the usual OnReturn bit.)
616 ## dbg('MaskedComboBox::OnReturn', indent=1)
617 ## dbg('current value: "%s"' % self.GetValue(), 'current index:', self.GetSelection())
618 if self
.GetSelection() == -1 and self
.GetValue().lower().strip() in self
._ctrl
_constraints
._compareChoices
:
619 wx
.CallAfter(self
.SetSelection
, self
._ctrl
_constraints
._autoCompleteIndex
)
621 event
.m_keyCode
= wx
.WXK_TAB
626 class ComboBox( BaseMaskedComboBox
, MaskedEditAccessorsMixin
):
628 The "user-visible" masked combobox control, this class is
629 identical to the BaseMaskedComboBox class it's derived from.
630 (This extra level of inheritance allows us to add the generic
631 set of masked edit parameters only to this class while allowing
632 other classes to derive from the "base" masked combobox control,
633 and provide a smaller set of valid accessor functions.)
634 See BaseMaskedComboBox for available methods.
639 class PreMaskedComboBox( BaseMaskedComboBox
, MaskedEditAccessorsMixin
):
641 This class exists to support the use of XRC subclassing.
643 # This should really be wx.EVT_WINDOW_CREATE but it is not
644 # currently delivered for native controls on all platforms, so
645 # we'll use EVT_SIZE instead. It should happen shortly after the
646 # control is created as the control is set to its "best" size.
647 _firstEventType
= wx
.EVT_SIZE
650 pre
= wx
.PreComboBox()
652 self
.Bind(self
._firstEventType
, self
.OnCreate
)
655 def OnCreate(self
, evt
):
656 self
.Unbind(self
._firstEventType
)
661 ## ====================
663 ## 1. Made definition of "hack" GetMark conditional on base class not
664 ## implementing it properly, to allow for migration in wx code base
665 ## while taking advantage of improvements therein for some platforms.
668 ## 1. Converted docstrings to reST format, added doc for ePyDoc.
669 ## 2. Renamed helper functions, vars etc. not intended to be visible in public
670 ## interface to code.
673 ## 1. Added .SetFont() method that properly resizes control
674 ## 2. Modified control to support construction via XRC mechanism.
675 ## 3. Added AppendItems() to conform with latest combobox.