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 from wx
.lib
.masked
import *
19 # jmg 12/9/03 - when we cut ties with Py 2.2 and earlier, this would
20 # be a good place to implement the 2.3 logger class
21 from wx
.tools
.dbg
import Logger
25 ## ---------- ---------- ---------- ---------- ---------- ---------- ----------
26 ## Because calling SetSelection programmatically does not fire EVT_COMBOBOX
27 ## events, we have to do it ourselves when we auto-complete.
28 class MaskedComboBoxSelectEvent(wx
.PyCommandEvent
):
29 def __init__(self
, id, selection
= 0, object=None):
30 wx
.PyCommandEvent
.__init
__(self
, wx
.wxEVT_COMMAND_COMBOBOX_SELECTED
, id)
32 self
.__selection
= selection
33 self
.SetEventObject(object)
35 def GetSelection(self
):
36 """Retrieve the value of the control at the time
37 this event was generated."""
38 return self
.__selection
41 class BaseMaskedComboBox( wx
.ComboBox
, MaskedEditMixin
):
43 This masked edit control adds the ability to use a masked input
44 on a combobox, and do auto-complete of such values.
46 def __init__( self
, parent
, id=-1, value
= '',
47 pos
= wx
.DefaultPosition
,
48 size
= wx
.DefaultSize
,
50 style
= wx
.CB_DROPDOWN
,
51 validator
= wx
.DefaultValidator
,
52 name
= "maskedComboBox",
53 setupEventHandling
= True, ## setup event handling by default):
57 # This is necessary, because wxComboBox currently provides no
58 # method for determining later if this was specified in the
59 # constructor for the control...
60 self
.__readonly
= style
& wx
.CB_READONLY
== wx
.CB_READONLY
62 kwargs
['choices'] = choices
## set up maskededit to work with choice list too
64 ## Since combobox completion is case-insensitive, always validate same way
65 if not kwargs
.has_key('compareNoCase'):
66 kwargs
['compareNoCase'] = True
68 MaskedEditMixin
.__init
__( self
, name
, **kwargs
)
70 self
._choices
= self
._ctrl
_constraints
._choices
71 ## dbg('self._choices:', self._choices)
73 if self
._ctrl
_constraints
._alignRight
:
74 choices
= [choice
.rjust(self
._masklength
) for choice
in choices
]
76 choices
= [choice
.ljust(self
._masklength
) for choice
in choices
]
78 wx
.ComboBox
.__init
__(self
, parent
, id, value
='',
80 choices
=choices
, style
=style|wx
.WANTS_CHARS
,
84 self
.controlInitialized
= True
86 # Set control font - fixed width by default
90 self
.SetClientSize(self
._CalcSize
())
93 # ensure value is width of the mask of the control:
94 if self
._ctrl
_constraints
._alignRight
:
95 value
= value
.rjust(self
._masklength
)
97 value
= value
.ljust(self
._masklength
)
100 self
.SetStringSelection(value
)
102 self
._SetInitialValue
(value
)
105 self
._SetKeycodeHandler
(wx
.WXK_UP
, self
.OnSelectChoice
)
106 self
._SetKeycodeHandler
(wx
.WXK_DOWN
, self
.OnSelectChoice
)
108 if setupEventHandling
:
109 ## Setup event handlers
110 self
.Bind(wx
.EVT_SET_FOCUS
, self
._OnFocus
) ## defeat automatic full selection
111 self
.Bind(wx
.EVT_KILL_FOCUS
, self
._OnKillFocus
) ## run internal validator
112 self
.Bind(wx
.EVT_LEFT_DCLICK
, self
._OnDoubleClick
) ## select field under cursor on dclick
113 self
.Bind(wx
.EVT_RIGHT_UP
, self
._OnContextMenu
) ## bring up an appropriate context menu
114 self
.Bind(wx
.EVT_CHAR
, self
._OnChar
) ## handle each keypress
115 self
.Bind(wx
.EVT_KEY_DOWN
, self
.OnKeyDown
) ## for special processing of up/down keys
116 self
.Bind(wx
.EVT_KEY_DOWN
, self
._OnKeyDown
) ## for processing the rest of the control keys
117 ## (next in evt chain)
118 self
.Bind(wx
.EVT_TEXT
, self
._OnTextChange
) ## color control appropriately & keep
119 ## track of previous value for undo
124 return "<MaskedComboBox: %s>" % self
.GetValue()
127 def _CalcSize(self
, size
=None):
129 Calculate automatic size if allowed; augment base mixin function
130 to account for the selector button.
132 size
= self
._calcSize
(size
)
133 return (size
[0]+20, size
[1])
136 def _GetSelection(self
):
138 Allow mixin to get the text selection of this control.
139 REQUIRED by any class derived from MaskedEditMixin.
141 return self
.GetMark()
143 def _SetSelection(self
, sel_start
, sel_to
):
145 Allow mixin to set the text selection of this control.
146 REQUIRED by any class derived from MaskedEditMixin.
148 return self
.SetMark( sel_start
, sel_to
)
151 def _GetInsertionPoint(self
):
152 return self
.GetInsertionPoint()
154 def _SetInsertionPoint(self
, pos
):
155 self
.SetInsertionPoint(pos
)
160 Allow mixin to get the raw value of the control with this function.
161 REQUIRED by any class derived from MaskedEditMixin.
163 return self
.GetValue()
165 def _SetValue(self
, value
):
167 Allow mixin to set the raw value of the control with this function.
168 REQUIRED by any class derived from MaskedEditMixin.
170 # For wxComboBox, ensure that values are properly padded so that
171 # if varying length choices are supplied, they always show up
172 # in the window properly, and will be the appropriate length
174 if self
._ctrl
_constraints
._alignRight
:
175 value
= value
.rjust(self
._masklength
)
177 value
= value
.ljust(self
._masklength
)
179 # Record current selection and insertion point, for undo
180 self
._prevSelection
= self
._GetSelection
()
181 self
._prevInsertionPoint
= self
._GetInsertionPoint
()
182 wx
.ComboBox
.SetValue(self
, value
)
183 # text change events don't always fire, so we check validity here
184 # to make certain formatting is applied:
187 def SetValue(self
, value
):
189 This function redefines the externally accessible .SetValue to be
190 a smart "paste" of the text in question, so as not to corrupt the
191 masked control. NOTE: this must be done in the class derived
192 from the base wx control.
195 wx
.ComboBox
.SetValue(value
) # revert to base control behavior
198 # empty previous contents, replacing entire value:
199 self
._SetInsertionPoint
(0)
200 self
._SetSelection
(0, self
._masklength
)
202 if( len(value
) < self
._masklength
# value shorter than control
203 and (self
._isFloat
or self
._isInt
) # and it's a numeric control
204 and self
._ctrl
_constraints
._alignRight
): # and it's a right-aligned control
205 # try to intelligently "pad out" the value to the right size:
206 value
= self
._template
[0:self
._masklength
- len(value
)] + value
207 ## dbg('padded value = "%s"' % value)
209 # For wxComboBox, ensure that values are properly padded so that
210 # if varying length choices are supplied, they always show up
211 # in the window properly, and will be the appropriate length
213 elif self
._ctrl
_constraints
._alignRight
:
214 value
= value
.rjust(self
._masklength
)
216 value
= value
.ljust(self
._masklength
)
219 # make SetValue behave the same as if you had typed the value in:
221 value
= self
._Paste
(value
, raise_on_invalid
=True, just_return_value
=True)
223 self
._isNeg
= False # (clear current assumptions)
224 value
= self
._adjustFloat
(value
)
226 self
._isNeg
= False # (clear current assumptions)
227 value
= self
._adjustInt
(value
)
228 elif self
._isDate
and not self
.IsValid(value
) and self
._4digityear
:
229 value
= self
._adjustDate
(value
, fixcentury
=True)
231 # If date, year might be 2 digits vs. 4; try adjusting it:
232 if self
._isDate
and self
._4digityear
:
233 dateparts
= value
.split(' ')
234 dateparts
[0] = self
._adjustDate
(dateparts
[0], fixcentury
=True)
235 value
= string
.join(dateparts
, ' ')
236 ## dbg('adjusted value: "%s"' % value)
237 value
= self
._Paste
(value
, raise_on_invalid
=True, just_return_value
=True)
241 self
._SetValue
(value
)
242 #### dbg('queuing insertion after .SetValue', self._masklength)
243 wx
.CallAfter(self
._SetInsertionPoint
, self
._masklength
)
244 wx
.CallAfter(self
._SetSelection
, self
._masklength
, self
._masklength
)
249 Allow mixin to refresh the base control with this function.
250 REQUIRED by any class derived from MaskedEditMixin.
252 wx
.ComboBox
.Refresh(self
)
256 This function redefines the externally accessible .Refresh() to
257 validate the contents of the masked control as it refreshes.
258 NOTE: this must be done in the class derived from the base wx control.
264 def _IsEditable(self
):
266 Allow mixin to determine if the base control is editable with this function.
267 REQUIRED by any class derived from MaskedEditMixin.
269 return not self
.__readonly
274 This function redefines the externally accessible .Cut to be
275 a smart "erase" of the text in question, so as not to corrupt the
276 masked control. NOTE: this must be done in the class derived
277 from the base wx control.
280 self
._Cut
() # call the mixin's Cut method
282 wx
.ComboBox
.Cut(self
) # else revert to base control behavior
287 This function redefines the externally accessible .Paste to be
288 a smart "paste" of the text in question, so as not to corrupt the
289 masked control. NOTE: this must be done in the class derived
290 from the base wx control.
293 self
._Paste
() # call the mixin's Paste method
295 wx
.ComboBox
.Paste(self
) # else revert to base control behavior
300 This function defines the undo operation for the control. (The default
306 wx
.ComboBox
.Undo() # else revert to base control behavior
309 def Append( self
, choice
, clientData
=None ):
311 This function override is necessary so we can keep track of any additions to the list
312 of choices, because wxComboBox doesn't have an accessor for the choice list.
313 The code here is the same as in the SetParameters() mixin function, but is
314 done for the individual value as appended, so the list can be built incrementally
315 without speed penalty.
318 if type(choice
) not in (types
.StringType
, types
.UnicodeType
):
319 raise TypeError('%s: choices must be a sequence of strings' % str(self
._index
))
320 elif not self
.IsValid(choice
):
321 raise ValueError('%s: "%s" is not a valid value for the control as specified.' % (str(self
._index
), choice
))
323 if not self
._ctrl
_constraints
._choices
:
324 self
._ctrl
_constraints
._compareChoices
= []
325 self
._ctrl
_constraints
._choices
= []
328 compareChoice
= choice
.strip()
330 if self
._ctrl
_constraints
._compareNoCase
:
331 compareChoice
= compareChoice
.lower()
333 if self
._ctrl
_constraints
._alignRight
:
334 choice
= choice
.rjust(self
._masklength
)
336 choice
= choice
.ljust(self
._masklength
)
337 if self
._ctrl
_constraints
._fillChar
!= ' ':
338 choice
= choice
.replace(' ', self
._fillChar
)
339 ## dbg('updated choice:', choice)
342 self
._ctrl
_constraints
._compareChoices
.append(compareChoice
)
343 self
._ctrl
_constraints
._choices
.append(choice
)
344 self
._choices
= self
._ctrl
_constraints
._choices
# (for shorthand)
346 if( not self
.IsValid(choice
) and
347 (not self
._ctrl
_constraints
.IsEmpty(choice
) or
348 (self
._ctrl
_constraints
.IsEmpty(choice
) and self
._ctrl
_constraints
._validRequired
) ) ):
349 raise ValueError('"%s" is not a valid value for the control "%s" as specified.' % (choice
, self
.name
))
351 wx
.ComboBox
.Append(self
, choice
, clientData
)
357 This function override is necessary so we can keep track of any additions to the list
358 of choices, because wxComboBox doesn't have an accessor for the choice list.
362 self
._ctrl
_constraints
._autoCompleteIndex
= -1
363 if self
._ctrl
_constraints
._choices
:
364 self
.SetCtrlParameters(choices
=[])
365 wx
.ComboBox
.Clear(self
)
368 def _OnCtrlParametersChanged(self
):
370 Override mixin's default OnCtrlParametersChanged to detect changes in choice list, so
371 we can update the base control:
373 if self
.controlInitialized
and self
._choices
!= self
._ctrl
_constraints
._choices
:
374 wx
.ComboBox
.Clear(self
)
375 self
._choices
= self
._ctrl
_constraints
._choices
376 for choice
in self
._choices
:
377 wx
.ComboBox
.Append( self
, choice
)
382 This function is a hack to make up for the fact that wxComboBox has no
383 method for returning the selected portion of its edit control. It
384 works, but has the nasty side effect of generating lots of intermediate
387 ## dbg(suspend=1) # turn off debugging around this function
388 ## dbg('MaskedComboBox::GetMark', indent=1)
391 return 0, 0 # no selection possible for editing
392 ## sel_start, sel_to = wxComboBox.GetMark(self) # what I'd *like* to have!
393 sel_start
= sel_to
= self
.GetInsertionPoint()
394 ## dbg("current sel_start:", sel_start)
395 value
= self
.GetValue()
396 ## dbg('value: "%s"' % value)
398 self
._ignoreChange
= True # tell _OnTextChange() to ignore next event (if any)
400 wx
.ComboBox
.Cut(self
)
401 newvalue
= self
.GetValue()
402 ## dbg("value after Cut operation:", newvalue)
404 if newvalue
!= value
: # something was selected; calculate extent
405 ## dbg("something selected")
406 sel_to
= sel_start
+ len(value
) - len(newvalue
)
407 wx
.ComboBox
.SetValue(self
, value
) # restore original value and selection (still ignoring change)
408 wx
.ComboBox
.SetInsertionPoint(self
, sel_start
)
409 wx
.ComboBox
.SetMark(self
, sel_start
, sel_to
)
411 self
._ignoreChange
= False # tell _OnTextChange() to pay attn again
413 ## dbg('computed selection:', sel_start, sel_to, indent=0, suspend=0)
414 return sel_start
, sel_to
417 def SetSelection(self
, index
):
419 Necessary for bookkeeping on choice selection, to keep current value
422 ## dbg('MaskedComboBox::SetSelection(%d)' % index)
424 self
._prevValue
= self
._curValue
425 self
._curValue
= self
._choices
[index
]
426 self
._ctrl
_constraints
._autoCompleteIndex
= index
427 wx
.ComboBox
.SetSelection(self
, index
)
430 def OnKeyDown(self
, event
):
432 This function is necessary because navigation and control key
433 events do not seem to normally be seen by the wxComboBox's
434 EVT_CHAR routine. (Tabs don't seem to be visible no matter
437 if event
.GetKeyCode() in self
._nav
+ self
._control
:
441 event
.Skip() # let mixin default KeyDown behavior occur
444 def OnSelectChoice(self
, event
):
446 This function appears to be necessary, because the processing done
447 on the text of the control somehow interferes with the combobox's
448 selection mechanism for the arrow keys.
450 ## dbg('MaskedComboBox::OnSelectChoice', indent=1)
456 value
= self
.GetValue().strip()
458 if self
._ctrl
_constraints
._compareNoCase
:
459 value
= value
.lower()
461 if event
.GetKeyCode() == wx
.WXK_UP
:
465 match_index
, partial_match
= self
._autoComplete
(
467 self
._ctrl
_constraints
._compareChoices
,
469 self
._ctrl
_constraints
._compareNoCase
,
470 current_index
= self
._ctrl
_constraints
._autoCompleteIndex
)
471 if match_index
is not None:
472 ## dbg('setting selection to', match_index)
473 # issue appropriate event to outside:
474 self
._OnAutoSelect
(self
._ctrl
_constraints
, match_index
=match_index
)
476 keep_processing
= False
478 pos
= self
._adjustPos
(self
._GetInsertionPoint
(), event
.GetKeyCode())
479 field
= self
._FindField
(pos
)
480 if self
.IsEmpty() or not field
._hasList
:
481 ## dbg('selecting 1st value in list')
482 self
._OnAutoSelect
(self
._ctrl
_constraints
, match_index
=0)
484 keep_processing
= False
486 # attempt field-level auto-complete
488 keep_processing
= self
._OnAutoCompleteField
(event
)
489 ## dbg('keep processing?', keep_processing, indent=0)
490 return keep_processing
493 def _OnAutoSelect(self
, field
, match_index
):
495 Override mixin (empty) autocomplete handler, so that autocompletion causes
496 combobox to update appropriately.
498 ## dbg('MaskedComboBox::OnAutoSelect', field._index, indent=1)
499 ## field._autoCompleteIndex = match_index
500 if field
== self
._ctrl
_constraints
:
501 self
.SetSelection(match_index
)
502 ## dbg('issuing combo selection event')
503 self
.GetEventHandler().ProcessEvent(
504 MaskedComboBoxSelectEvent( self
.GetId(), match_index
, self
) )
506 ## dbg('field._autoCompleteIndex:', match_index)
507 ## dbg('self.GetSelection():', self.GetSelection())
511 def _OnReturn(self
, event
):
513 For wxComboBox, it seems that if you hit return when the dropdown is
514 dropped, the event that dismisses the dropdown will also blank the
515 control, because of the implementation of wxComboBox. So here,
516 we look and if the selection is -1, and the value according to
517 (the base control!) is a value in the list, then we schedule a
518 programmatic wxComboBox.SetSelection() call to pick the appropriate
519 item in the list. (and then do the usual OnReturn bit.)
521 ## dbg('MaskedComboBox::OnReturn', indent=1)
522 ## dbg('current value: "%s"' % self.GetValue(), 'current index:', self.GetSelection())
523 if self
.GetSelection() == -1 and self
.GetValue().lower().strip() in self
._ctrl
_constraints
._compareChoices
:
524 wx
.CallAfter(self
.SetSelection
, self
._ctrl
_constraints
._autoCompleteIndex
)
526 event
.m_keyCode
= wx
.WXK_TAB
531 class ComboBox( BaseMaskedComboBox
, MaskedEditAccessorsMixin
):
533 This extra level of inheritance allows us to add the generic set of
534 masked edit parameters only to this class while allowing other
535 classes to derive from the "base" masked combobox control, and provide
536 a smaller set of valid accessor functions.