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 return self
.GetMark()
202 def _SetSelection(self
, sel_start
, sel_to
):
204 Allow mixin to set the text selection of this control.
205 REQUIRED by any class derived from MaskedEditMixin.
207 return self
.SetMark( sel_start
, sel_to
)
210 def _GetInsertionPoint(self
):
211 return self
.GetInsertionPoint()
213 def _SetInsertionPoint(self
, pos
):
214 self
.SetInsertionPoint(pos
)
219 Allow mixin to get the raw value of the control with this function.
220 REQUIRED by any class derived from MaskedEditMixin.
222 return self
.GetValue()
224 def _SetValue(self
, value
):
226 Allow mixin to set the raw value of the control with this function.
227 REQUIRED by any class derived from MaskedEditMixin.
229 # For wxComboBox, ensure that values are properly padded so that
230 # if varying length choices are supplied, they always show up
231 # in the window properly, and will be the appropriate length
233 if self
._ctrl
_constraints
._alignRight
:
234 value
= value
.rjust(self
._masklength
)
236 value
= value
.ljust(self
._masklength
)
238 # Record current selection and insertion point, for undo
239 self
._prevSelection
= self
._GetSelection
()
240 self
._prevInsertionPoint
= self
._GetInsertionPoint
()
241 wx
.ComboBox
.SetValue(self
, value
)
242 # text change events don't always fire, so we check validity here
243 # to make certain formatting is applied:
246 def SetValue(self
, value
):
248 This function redefines the externally accessible .SetValue to be
249 a smart "paste" of the text in question, so as not to corrupt the
250 masked control. NOTE: this must be done in the class derived
251 from the base wx control.
254 wx
.ComboBox
.SetValue(value
) # revert to base control behavior
257 # empty previous contents, replacing entire value:
258 self
._SetInsertionPoint
(0)
259 self
._SetSelection
(0, self
._masklength
)
261 if( len(value
) < self
._masklength
# value shorter than control
262 and (self
._isFloat
or self
._isInt
) # and it's a numeric control
263 and self
._ctrl
_constraints
._alignRight
): # and it's a right-aligned control
264 # try to intelligently "pad out" the value to the right size:
265 value
= self
._template
[0:self
._masklength
- len(value
)] + value
266 ## dbg('padded value = "%s"' % value)
268 # For wxComboBox, ensure that values are properly padded so that
269 # if varying length choices are supplied, they always show up
270 # in the window properly, and will be the appropriate length
272 elif self
._ctrl
_constraints
._alignRight
:
273 value
= value
.rjust(self
._masklength
)
275 value
= value
.ljust(self
._masklength
)
278 # make SetValue behave the same as if you had typed the value in:
280 value
, replace_to
= self
._Paste
(value
, raise_on_invalid
=True, just_return_value
=True)
282 self
._isNeg
= False # (clear current assumptions)
283 value
= self
._adjustFloat
(value
)
285 self
._isNeg
= False # (clear current assumptions)
286 value
= self
._adjustInt
(value
)
287 elif self
._isDate
and not self
.IsValid(value
) and self
._4digityear
:
288 value
= self
._adjustDate
(value
, fixcentury
=True)
290 # If date, year might be 2 digits vs. 4; try adjusting it:
291 if self
._isDate
and self
._4digityear
:
292 dateparts
= value
.split(' ')
293 dateparts
[0] = self
._adjustDate
(dateparts
[0], fixcentury
=True)
294 value
= string
.join(dateparts
, ' ')
295 ## dbg('adjusted value: "%s"' % value)
296 value
= self
._Paste
(value
, raise_on_invalid
=True, just_return_value
=True)
300 self
._SetValue
(value
)
301 #### dbg('queuing insertion after .SetValue', replace_to)
302 wx
.CallAfter(self
._SetInsertionPoint
, replace_to
)
303 wx
.CallAfter(self
._SetSelection
, replace_to
, replace_to
)
308 Allow mixin to refresh the base control with this function.
309 REQUIRED by any class derived from MaskedEditMixin.
311 wx
.ComboBox
.Refresh(self
)
315 This function redefines the externally accessible .Refresh() to
316 validate the contents of the masked control as it refreshes.
317 NOTE: this must be done in the class derived from the base wx control.
323 def _IsEditable(self
):
325 Allow mixin to determine if the base control is editable with this function.
326 REQUIRED by any class derived from MaskedEditMixin.
328 return not self
.__readonly
333 This function redefines the externally accessible .Cut to be
334 a smart "erase" of the text in question, so as not to corrupt the
335 masked control. NOTE: this must be done in the class derived
336 from the base wx control.
339 self
._Cut
() # call the mixin's Cut method
341 wx
.ComboBox
.Cut(self
) # else revert to base control behavior
346 This function redefines the externally accessible .Paste to be
347 a smart "paste" of the text in question, so as not to corrupt the
348 masked control. NOTE: this must be done in the class derived
349 from the base wx control.
352 self
._Paste
() # call the mixin's Paste method
354 wx
.ComboBox
.Paste(self
) # else revert to base control behavior
359 This function defines the undo operation for the control. (The default
365 wx
.ComboBox
.Undo() # else revert to base control behavior
367 def Append( self
, choice
, clientData
=None ):
369 This base control function override is necessary so the control can keep track
370 of any additions to the list of choices, because wx.ComboBox doesn't have an
371 accessor for the choice list. The code here is the same as in the
372 SetParameters() mixin function, but is done for the individual value
373 as appended, so the list can be built incrementally without speed penalty.
376 if type(choice
) not in (types
.StringType
, types
.UnicodeType
):
377 raise TypeError('%s: choices must be a sequence of strings' % str(self
._index
))
378 elif not self
.IsValid(choice
):
379 raise ValueError('%s: "%s" is not a valid value for the control as specified.' % (str(self
._index
), choice
))
381 if not self
._ctrl
_constraints
._choices
:
382 self
._ctrl
_constraints
._compareChoices
= []
383 self
._ctrl
_constraints
._choices
= []
386 compareChoice
= choice
.strip()
388 if self
._ctrl
_constraints
._compareNoCase
:
389 compareChoice
= compareChoice
.lower()
391 if self
._ctrl
_constraints
._alignRight
:
392 choice
= choice
.rjust(self
._masklength
)
394 choice
= choice
.ljust(self
._masklength
)
395 if self
._ctrl
_constraints
._fillChar
!= ' ':
396 choice
= choice
.replace(' ', self
._fillChar
)
397 ## dbg('updated choice:', choice)
400 self
._ctrl
_constraints
._compareChoices
.append(compareChoice
)
401 self
._ctrl
_constraints
._choices
.append(choice
)
402 self
._choices
= self
._ctrl
_constraints
._choices
# (for shorthand)
404 if( not self
.IsValid(choice
) and
405 (not self
._ctrl
_constraints
.IsEmpty(choice
) or
406 (self
._ctrl
_constraints
.IsEmpty(choice
) and self
._ctrl
_constraints
._validRequired
) ) ):
407 raise ValueError('"%s" is not a valid value for the control "%s" as specified.' % (choice
, self
.name
))
409 wx
.ComboBox
.Append(self
, choice
, clientData
)
412 def AppendItems( self
, choices
):
414 AppendItems() is handled in terms of Append, to avoid code replication.
416 for choice
in choices
:
422 This base control function override is necessary so the derived control can
423 keep track of any additions to the list of choices, because wx.ComboBox
424 doesn't have an accessor for the choice list.
428 self
._ctrl
_constraints
._autoCompleteIndex
= -1
429 if self
._ctrl
_constraints
._choices
:
430 self
.SetCtrlParameters(choices
=[])
431 wx
.ComboBox
.Clear(self
)
434 def _OnCtrlParametersChanged(self
):
436 This overrides the mixin's default OnCtrlParametersChanged to detect
437 changes in choice list, so masked.Combobox can update the base control:
439 if self
.controlInitialized
and self
._choices
!= self
._ctrl
_constraints
._choices
:
440 wx
.ComboBox
.Clear(self
)
441 self
._choices
= self
._ctrl
_constraints
._choices
442 for choice
in self
._choices
:
443 wx
.ComboBox
.Append( self
, choice
)
448 This function is a hack to make up for the fact that wx.ComboBox has no
449 method for returning the selected portion of its edit control. It
450 works, but has the nasty side effect of generating lots of intermediate
453 ## dbg(suspend=1) # turn off debugging around this function
454 ## dbg('MaskedComboBox::GetMark', indent=1)
457 return 0, 0 # no selection possible for editing
458 ## sel_start, sel_to = wxComboBox.GetMark(self) # what I'd *like* to have!
459 sel_start
= sel_to
= self
.GetInsertionPoint()
460 ## dbg("current sel_start:", sel_start)
461 value
= self
.GetValue()
462 ## dbg('value: "%s"' % value)
464 self
._ignoreChange
= True # tell _OnTextChange() to ignore next event (if any)
466 wx
.ComboBox
.Cut(self
)
467 newvalue
= self
.GetValue()
468 ## dbg("value after Cut operation:", newvalue)
470 if newvalue
!= value
: # something was selected; calculate extent
471 ## dbg("something selected")
472 sel_to
= sel_start
+ len(value
) - len(newvalue
)
473 wx
.ComboBox
.SetValue(self
, value
) # restore original value and selection (still ignoring change)
474 wx
.ComboBox
.SetInsertionPoint(self
, sel_start
)
475 wx
.ComboBox
.SetMark(self
, sel_start
, sel_to
)
477 self
._ignoreChange
= False # tell _OnTextChange() to pay attn again
479 ## dbg('computed selection:', sel_start, sel_to, indent=0, suspend=0)
480 return sel_start
, sel_to
483 def SetSelection(self
, index
):
485 Necessary override for bookkeeping on choice selection, to keep current value
488 ## dbg('MaskedComboBox::SetSelection(%d)' % index)
490 self
._prevValue
= self
._curValue
491 self
._curValue
= self
._choices
[index
]
492 self
._ctrl
_constraints
._autoCompleteIndex
= index
493 wx
.ComboBox
.SetSelection(self
, index
)
496 def _OnKeyDownInComboBox(self
, event
):
498 This function is necessary because navigation and control key
499 events do not seem to normally be seen by the wxComboBox's
500 EVT_CHAR routine. (Tabs don't seem to be visible no matter
503 if event
.GetKeyCode() in self
._nav
+ self
._control
:
507 event
.Skip() # let mixin default KeyDown behavior occur
510 def _OnSelectChoice(self
, event
):
512 This function appears to be necessary, because the processing done
513 on the text of the control somehow interferes with the combobox's
514 selection mechanism for the arrow keys.
516 ## dbg('MaskedComboBox::OnSelectChoice', indent=1)
522 value
= self
.GetValue().strip()
524 if self
._ctrl
_constraints
._compareNoCase
:
525 value
= value
.lower()
527 if event
.GetKeyCode() == wx
.WXK_UP
:
531 match_index
, partial_match
= self
._autoComplete
(
533 self
._ctrl
_constraints
._compareChoices
,
535 self
._ctrl
_constraints
._compareNoCase
,
536 current_index
= self
._ctrl
_constraints
._autoCompleteIndex
)
537 if match_index
is not None:
538 ## dbg('setting selection to', match_index)
539 # issue appropriate event to outside:
540 self
._OnAutoSelect
(self
._ctrl
_constraints
, match_index
=match_index
)
542 keep_processing
= False
544 pos
= self
._adjustPos
(self
._GetInsertionPoint
(), event
.GetKeyCode())
545 field
= self
._FindField
(pos
)
546 if self
.IsEmpty() or not field
._hasList
:
547 ## dbg('selecting 1st value in list')
548 self
._OnAutoSelect
(self
._ctrl
_constraints
, match_index
=0)
550 keep_processing
= False
552 # attempt field-level auto-complete
554 keep_processing
= self
._OnAutoCompleteField
(event
)
555 ## dbg('keep processing?', keep_processing, indent=0)
556 return keep_processing
559 def _OnAutoSelect(self
, field
, match_index
):
561 Override mixin (empty) autocomplete handler, so that autocompletion causes
562 combobox to update appropriately.
564 ## dbg('MaskedComboBox::OnAutoSelect', field._index, indent=1)
565 ## field._autoCompleteIndex = match_index
566 if field
== self
._ctrl
_constraints
:
567 self
.SetSelection(match_index
)
568 ## dbg('issuing combo selection event')
569 self
.GetEventHandler().ProcessEvent(
570 MaskedComboBoxSelectEvent( self
.GetId(), match_index
, self
) )
572 ## dbg('field._autoCompleteIndex:', match_index)
573 ## dbg('self.GetSelection():', self.GetSelection())
574 end
= self
._goEnd
(getPosOnly
=True)
575 ## dbg('scheduling set of end position to:', end)
576 # work around bug in wx 2.5
577 wx
.CallAfter(self
.SetInsertionPoint
, 0)
578 wx
.CallAfter(self
.SetInsertionPoint
, end
)
582 def _OnReturn(self
, event
):
584 For wx.ComboBox, it seems that if you hit return when the dropdown is
585 dropped, the event that dismisses the dropdown will also blank the
586 control, because of the implementation of wxComboBox. So this function
587 examines the selection and if it is -1, and the value according to
588 (the base control!) is a value in the list, then it schedules a
589 programmatic wxComboBox.SetSelection() call to pick the appropriate
590 item in the list. (and then does the usual OnReturn bit.)
592 ## dbg('MaskedComboBox::OnReturn', indent=1)
593 ## dbg('current value: "%s"' % self.GetValue(), 'current index:', self.GetSelection())
594 if self
.GetSelection() == -1 and self
.GetValue().lower().strip() in self
._ctrl
_constraints
._compareChoices
:
595 wx
.CallAfter(self
.SetSelection
, self
._ctrl
_constraints
._autoCompleteIndex
)
597 event
.m_keyCode
= wx
.WXK_TAB
602 class ComboBox( BaseMaskedComboBox
, MaskedEditAccessorsMixin
):
604 The "user-visible" masked combobox control, this class is
605 identical to the BaseMaskedComboBox class it's derived from.
606 (This extra level of inheritance allows us to add the generic
607 set of masked edit parameters only to this class while allowing
608 other classes to derive from the "base" masked combobox control,
609 and provide a smaller set of valid accessor functions.)
610 See BaseMaskedComboBox for available methods.
615 class PreMaskedComboBox( BaseMaskedComboBox
, MaskedEditAccessorsMixin
):
617 This class exists to support the use of XRC subclassing.
619 # This should really be wx.EVT_WINDOW_CREATE but it is not
620 # currently delivered for native controls on all platforms, so
621 # we'll use EVT_SIZE instead. It should happen shortly after the
622 # control is created as the control is set to its "best" size.
623 _firstEventType
= wx
.EVT_SIZE
626 pre
= wx
.PreComboBox()
628 self
.Bind(self
._firstEventType
, self
.OnCreate
)
631 def OnCreate(self
, evt
):
632 self
.Unbind(self
._firstEventType
)
637 ## ====================
639 ## 1. Converted docstrings to reST format, added doc for ePyDoc.
640 ## 2. Renamed helper functions, vars etc. not intended to be visible in public
641 ## interface to code.
644 ## 1. Added .SetFont() method that properly resizes control
645 ## 2. Modified control to support construction via XRC mechanism.
646 ## 3. Added AppendItems() to conform with latest combobox.