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
.SetBestFittingSize((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
.SetBestFittingSize((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
)
229 Allow mixin to get the raw value of the control with this function.
230 REQUIRED by any class derived from MaskedEditMixin.
232 return self
.GetValue()
234 def _SetValue(self
, value
):
236 Allow mixin to set the raw value of the control with this function.
237 REQUIRED by any class derived from MaskedEditMixin.
239 # For wxComboBox, ensure that values are properly padded so that
240 # if varying length choices are supplied, they always show up
241 # in the window properly, and will be the appropriate length
243 if self
._ctrl
_constraints
._alignRight
:
244 value
= value
.rjust(self
._masklength
)
246 value
= value
.ljust(self
._masklength
)
248 # Record current selection and insertion point, for undo
249 self
._prevSelection
= self
._GetSelection
()
250 self
._prevInsertionPoint
= self
._GetInsertionPoint
()
251 wx
.ComboBox
.SetValue(self
, value
)
252 # text change events don't always fire, so we check validity here
253 # to make certain formatting is applied:
256 def SetValue(self
, value
):
258 This function redefines the externally accessible .SetValue to be
259 a smart "paste" of the text in question, so as not to corrupt the
260 masked control. NOTE: this must be done in the class derived
261 from the base wx control.
264 wx
.ComboBox
.SetValue(value
) # revert to base control behavior
267 # empty previous contents, replacing entire value:
268 self
._SetInsertionPoint
(0)
269 self
._SetSelection
(0, self
._masklength
)
271 if( len(value
) < self
._masklength
# value shorter than control
272 and (self
._isFloat
or self
._isInt
) # and it's a numeric control
273 and self
._ctrl
_constraints
._alignRight
): # and it's a right-aligned control
274 # try to intelligently "pad out" the value to the right size:
275 value
= self
._template
[0:self
._masklength
- len(value
)] + value
276 ## dbg('padded value = "%s"' % value)
278 # For wxComboBox, ensure that values are properly padded so that
279 # if varying length choices are supplied, they always show up
280 # in the window properly, and will be the appropriate length
282 elif self
._ctrl
_constraints
._alignRight
:
283 value
= value
.rjust(self
._masklength
)
285 value
= value
.ljust(self
._masklength
)
288 # make SetValue behave the same as if you had typed the value in:
290 value
, replace_to
= self
._Paste
(value
, raise_on_invalid
=True, just_return_value
=True)
292 self
._isNeg
= False # (clear current assumptions)
293 value
= self
._adjustFloat
(value
)
295 self
._isNeg
= False # (clear current assumptions)
296 value
= self
._adjustInt
(value
)
297 elif self
._isDate
and not self
.IsValid(value
) and self
._4digityear
:
298 value
= self
._adjustDate
(value
, fixcentury
=True)
300 # If date, year might be 2 digits vs. 4; try adjusting it:
301 if self
._isDate
and self
._4digityear
:
302 dateparts
= value
.split(' ')
303 dateparts
[0] = self
._adjustDate
(dateparts
[0], fixcentury
=True)
304 value
= string
.join(dateparts
, ' ')
305 ## dbg('adjusted value: "%s"' % value)
306 value
= self
._Paste
(value
, raise_on_invalid
=True, just_return_value
=True)
310 self
._SetValue
(value
)
311 #### dbg('queuing insertion after .SetValue', replace_to)
312 wx
.CallAfter(self
._SetInsertionPoint
, replace_to
)
313 wx
.CallAfter(self
._SetSelection
, replace_to
, replace_to
)
318 Allow mixin to refresh the base control with this function.
319 REQUIRED by any class derived from MaskedEditMixin.
321 wx
.ComboBox
.Refresh(self
)
325 This function redefines the externally accessible .Refresh() to
326 validate the contents of the masked control as it refreshes.
327 NOTE: this must be done in the class derived from the base wx control.
333 def _IsEditable(self
):
335 Allow mixin to determine if the base control is editable with this function.
336 REQUIRED by any class derived from MaskedEditMixin.
338 return not self
.__readonly
343 This function redefines the externally accessible .Cut to be
344 a smart "erase" of the text in question, so as not to corrupt the
345 masked control. NOTE: this must be done in the class derived
346 from the base wx control.
349 self
._Cut
() # call the mixin's Cut method
351 wx
.ComboBox
.Cut(self
) # else revert to base control behavior
356 This function redefines the externally accessible .Paste to be
357 a smart "paste" of the text in question, so as not to corrupt the
358 masked control. NOTE: this must be done in the class derived
359 from the base wx control.
362 self
._Paste
() # call the mixin's Paste method
364 wx
.ComboBox
.Paste(self
) # else revert to base control behavior
369 This function defines the undo operation for the control. (The default
375 wx
.ComboBox
.Undo() # else revert to base control behavior
377 def Append( self
, choice
, clientData
=None ):
379 This base control function override is necessary so the control can keep track
380 of any additions to the list of choices, because wx.ComboBox doesn't have an
381 accessor for the choice list. The code here is the same as in the
382 SetParameters() mixin function, but is done for the individual value
383 as appended, so the list can be built incrementally without speed penalty.
386 if type(choice
) not in (types
.StringType
, types
.UnicodeType
):
387 raise TypeError('%s: choices must be a sequence of strings' % str(self
._index
))
388 elif not self
.IsValid(choice
):
389 raise ValueError('%s: "%s" is not a valid value for the control as specified.' % (str(self
._index
), choice
))
391 if not self
._ctrl
_constraints
._choices
:
392 self
._ctrl
_constraints
._compareChoices
= []
393 self
._ctrl
_constraints
._choices
= []
396 compareChoice
= choice
.strip()
398 if self
._ctrl
_constraints
._compareNoCase
:
399 compareChoice
= compareChoice
.lower()
401 if self
._ctrl
_constraints
._alignRight
:
402 choice
= choice
.rjust(self
._masklength
)
404 choice
= choice
.ljust(self
._masklength
)
405 if self
._ctrl
_constraints
._fillChar
!= ' ':
406 choice
= choice
.replace(' ', self
._fillChar
)
407 ## dbg('updated choice:', choice)
410 self
._ctrl
_constraints
._compareChoices
.append(compareChoice
)
411 self
._ctrl
_constraints
._choices
.append(choice
)
412 self
._choices
= self
._ctrl
_constraints
._choices
# (for shorthand)
414 if( not self
.IsValid(choice
) and
415 (not self
._ctrl
_constraints
.IsEmpty(choice
) or
416 (self
._ctrl
_constraints
.IsEmpty(choice
) and self
._ctrl
_constraints
._validRequired
) ) ):
417 raise ValueError('"%s" is not a valid value for the control "%s" as specified.' % (choice
, self
.name
))
419 wx
.ComboBox
.Append(self
, choice
, clientData
)
422 def AppendItems( self
, choices
):
424 AppendItems() is handled in terms of Append, to avoid code replication.
426 for choice
in choices
:
432 This base control function override is necessary so the derived control can
433 keep track of any additions to the list of choices, because wx.ComboBox
434 doesn't have an accessor for the choice list.
438 self
._ctrl
_constraints
._autoCompleteIndex
= -1
439 if self
._ctrl
_constraints
._choices
:
440 self
.SetCtrlParameters(choices
=[])
441 wx
.ComboBox
.Clear(self
)
444 def _OnCtrlParametersChanged(self
):
446 This overrides the mixin's default OnCtrlParametersChanged to detect
447 changes in choice list, so masked.Combobox can update the base control:
449 if self
.controlInitialized
and self
._choices
!= self
._ctrl
_constraints
._choices
:
450 wx
.ComboBox
.Clear(self
)
451 self
._choices
= self
._ctrl
_constraints
._choices
452 for choice
in self
._choices
:
453 wx
.ComboBox
.Append( self
, choice
)
456 # Not all wx platform implementations have .GetMark, so we make the following test,
457 # and fall back to our old hack if they don't...
459 if not hasattr(wx
.ComboBox
, 'GetMark'):
462 This function is a hack to make up for the fact that wx.ComboBox has no
463 method for returning the selected portion of its edit control. It
464 works, but has the nasty side effect of generating lots of intermediate
467 ## dbg(suspend=1) # turn off debugging around this function
468 ## dbg('MaskedComboBox::GetMark', indent=1)
471 return 0, 0 # no selection possible for editing
472 ## sel_start, sel_to = wxComboBox.GetMark(self) # what I'd *like* to have!
473 sel_start
= sel_to
= self
.GetInsertionPoint()
474 ## dbg("current sel_start:", sel_start)
475 value
= self
.GetValue()
476 ## dbg('value: "%s"' % value)
478 self
._ignoreChange
= True # tell _OnTextChange() to ignore next event (if any)
480 wx
.ComboBox
.Cut(self
)
481 newvalue
= self
.GetValue()
482 ## dbg("value after Cut operation:", newvalue)
484 if newvalue
!= value
: # something was selected; calculate extent
485 ## dbg("something selected")
486 sel_to
= sel_start
+ len(value
) - len(newvalue
)
487 wx
.ComboBox
.SetValue(self
, value
) # restore original value and selection (still ignoring change)
488 wx
.ComboBox
.SetInsertionPoint(self
, sel_start
)
489 wx
.ComboBox
.SetMark(self
, sel_start
, sel_to
)
491 self
._ignoreChange
= False # tell _OnTextChange() to pay attn again
493 ## dbg('computed selection:', sel_start, sel_to, indent=0, suspend=0)
494 return sel_start
, sel_to
497 ## dbg('MaskedComboBox::GetMark()', indent = 1)
498 ret
= wx
.ComboBox
.GetMark(self
)
499 ## dbg('returned', ret, indent=0)
503 def SetSelection(self
, index
):
505 Necessary override for bookkeeping on choice selection, to keep current value
508 ## dbg('MaskedComboBox::SetSelection(%d)' % index)
510 self
._prevValue
= self
._curValue
511 self
._curValue
= self
._choices
[index
]
512 self
._ctrl
_constraints
._autoCompleteIndex
= index
513 wx
.ComboBox
.SetSelection(self
, index
)
516 def _OnKeyDownInComboBox(self
, event
):
518 This function is necessary because navigation and control key
519 events do not seem to normally be seen by the wxComboBox's
520 EVT_CHAR routine. (Tabs don't seem to be visible no matter
523 if event
.GetKeyCode() in self
._nav
+ self
._control
:
527 event
.Skip() # let mixin default KeyDown behavior occur
530 def _OnSelectChoice(self
, event
):
532 This function appears to be necessary, because the processing done
533 on the text of the control somehow interferes with the combobox's
534 selection mechanism for the arrow keys.
536 ## dbg('MaskedComboBox::OnSelectChoice', indent=1)
542 value
= self
.GetValue().strip()
544 if self
._ctrl
_constraints
._compareNoCase
:
545 value
= value
.lower()
547 if event
.GetKeyCode() == wx
.WXK_UP
:
551 match_index
, partial_match
= self
._autoComplete
(
553 self
._ctrl
_constraints
._compareChoices
,
555 self
._ctrl
_constraints
._compareNoCase
,
556 current_index
= self
._ctrl
_constraints
._autoCompleteIndex
)
557 if match_index
is not None:
558 ## dbg('setting selection to', match_index)
559 # issue appropriate event to outside:
560 self
._OnAutoSelect
(self
._ctrl
_constraints
, match_index
=match_index
)
562 keep_processing
= False
564 pos
= self
._adjustPos
(self
._GetInsertionPoint
(), event
.GetKeyCode())
565 field
= self
._FindField
(pos
)
566 if self
.IsEmpty() or not field
._hasList
:
567 ## dbg('selecting 1st value in list')
568 self
._OnAutoSelect
(self
._ctrl
_constraints
, match_index
=0)
570 keep_processing
= False
572 # attempt field-level auto-complete
574 keep_processing
= self
._OnAutoCompleteField
(event
)
575 ## dbg('keep processing?', keep_processing, indent=0)
576 return keep_processing
579 def _OnAutoSelect(self
, field
, match_index
):
581 Override mixin (empty) autocomplete handler, so that autocompletion causes
582 combobox to update appropriately.
584 ## dbg('MaskedComboBox::OnAutoSelect', field._index, indent=1)
585 ## field._autoCompleteIndex = match_index
586 if field
== self
._ctrl
_constraints
:
587 self
.SetSelection(match_index
)
588 ## dbg('issuing combo selection event')
589 self
.GetEventHandler().ProcessEvent(
590 MaskedComboBoxSelectEvent( self
.GetId(), match_index
, self
) )
592 ## dbg('field._autoCompleteIndex:', match_index)
593 ## dbg('self.GetSelection():', self.GetSelection())
594 end
= self
._goEnd
(getPosOnly
=True)
595 ## dbg('scheduling set of end position to:', end)
596 # work around bug in wx 2.5
597 wx
.CallAfter(self
.SetInsertionPoint
, 0)
598 wx
.CallAfter(self
.SetInsertionPoint
, end
)
602 def _OnReturn(self
, event
):
604 For wx.ComboBox, it seems that if you hit return when the dropdown is
605 dropped, the event that dismisses the dropdown will also blank the
606 control, because of the implementation of wxComboBox. So this function
607 examines the selection and if it is -1, and the value according to
608 (the base control!) is a value in the list, then it schedules a
609 programmatic wxComboBox.SetSelection() call to pick the appropriate
610 item in the list. (and then does the usual OnReturn bit.)
612 ## dbg('MaskedComboBox::OnReturn', indent=1)
613 ## dbg('current value: "%s"' % self.GetValue(), 'current index:', self.GetSelection())
614 if self
.GetSelection() == -1 and self
.GetValue().lower().strip() in self
._ctrl
_constraints
._compareChoices
:
615 wx
.CallAfter(self
.SetSelection
, self
._ctrl
_constraints
._autoCompleteIndex
)
617 event
.m_keyCode
= wx
.WXK_TAB
622 class ComboBox( BaseMaskedComboBox
, MaskedEditAccessorsMixin
):
624 The "user-visible" masked combobox control, this class is
625 identical to the BaseMaskedComboBox class it's derived from.
626 (This extra level of inheritance allows us to add the generic
627 set of masked edit parameters only to this class while allowing
628 other classes to derive from the "base" masked combobox control,
629 and provide a smaller set of valid accessor functions.)
630 See BaseMaskedComboBox for available methods.
635 class PreMaskedComboBox( BaseMaskedComboBox
, MaskedEditAccessorsMixin
):
637 This class exists to support the use of XRC subclassing.
639 # This should really be wx.EVT_WINDOW_CREATE but it is not
640 # currently delivered for native controls on all platforms, so
641 # we'll use EVT_SIZE instead. It should happen shortly after the
642 # control is created as the control is set to its "best" size.
643 _firstEventType
= wx
.EVT_SIZE
646 pre
= wx
.PreComboBox()
648 self
.Bind(self
._firstEventType
, self
.OnCreate
)
651 def OnCreate(self
, evt
):
652 self
.Unbind(self
._firstEventType
)
657 ## ====================
659 ## 1. Made definition of "hack" GetMark conditional on base class not
660 ## implementing it properly, to allow for migration in wx code base
661 ## while taking advantage of improvements therein for some platforms.
664 ## 1. Converted docstrings to reST format, added doc for ePyDoc.
665 ## 2. Renamed helper functions, vars etc. not intended to be visible in public
666 ## interface to code.
669 ## 1. Added .SetFont() method that properly resizes control
670 ## 2. Modified control to support construction via XRC mechanism.
671 ## 3. Added AppendItems() to conform with latest combobox.