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
())
91 self
.SetSizeHints(self
.GetSize())
94 # ensure value is width of the mask of the control:
95 if self
._ctrl
_constraints
._alignRight
:
96 value
= value
.rjust(self
._masklength
)
98 value
= value
.ljust(self
._masklength
)
101 self
.SetStringSelection(value
)
103 self
._SetInitialValue
(value
)
106 self
._SetKeycodeHandler
(wx
.WXK_UP
, self
.OnSelectChoice
)
107 self
._SetKeycodeHandler
(wx
.WXK_DOWN
, self
.OnSelectChoice
)
109 if setupEventHandling
:
110 ## Setup event handlers
111 self
.Bind(wx
.EVT_SET_FOCUS
, self
._OnFocus
) ## defeat automatic full selection
112 self
.Bind(wx
.EVT_KILL_FOCUS
, self
._OnKillFocus
) ## run internal validator
113 self
.Bind(wx
.EVT_LEFT_DCLICK
, self
._OnDoubleClick
) ## select field under cursor on dclick
114 self
.Bind(wx
.EVT_RIGHT_UP
, self
._OnContextMenu
) ## bring up an appropriate context menu
115 self
.Bind(wx
.EVT_CHAR
, self
._OnChar
) ## handle each keypress
116 self
.Bind(wx
.EVT_KEY_DOWN
, self
.OnKeyDown
) ## for special processing of up/down keys
117 self
.Bind(wx
.EVT_KEY_DOWN
, self
._OnKeyDown
) ## for processing the rest of the control keys
118 ## (next in evt chain)
119 self
.Bind(wx
.EVT_TEXT
, self
._OnTextChange
) ## color control appropriately & keep
120 ## track of previous value for undo
125 return "<MaskedComboBox: %s>" % self
.GetValue()
128 def _CalcSize(self
, size
=None):
130 Calculate automatic size if allowed; augment base mixin function
131 to account for the selector button.
133 size
= self
._calcSize
(size
)
134 return (size
[0]+20, size
[1])
137 def _GetSelection(self
):
139 Allow mixin to get the text selection of this control.
140 REQUIRED by any class derived from MaskedEditMixin.
142 return self
.GetMark()
144 def _SetSelection(self
, sel_start
, sel_to
):
146 Allow mixin to set the text selection of this control.
147 REQUIRED by any class derived from MaskedEditMixin.
149 return self
.SetMark( sel_start
, sel_to
)
152 def _GetInsertionPoint(self
):
153 return self
.GetInsertionPoint()
155 def _SetInsertionPoint(self
, pos
):
156 self
.SetInsertionPoint(pos
)
161 Allow mixin to get the raw value of the control with this function.
162 REQUIRED by any class derived from MaskedEditMixin.
164 return self
.GetValue()
166 def _SetValue(self
, value
):
168 Allow mixin to set the raw value of the control with this function.
169 REQUIRED by any class derived from MaskedEditMixin.
171 # For wxComboBox, ensure that values are properly padded so that
172 # if varying length choices are supplied, they always show up
173 # in the window properly, and will be the appropriate length
175 if self
._ctrl
_constraints
._alignRight
:
176 value
= value
.rjust(self
._masklength
)
178 value
= value
.ljust(self
._masklength
)
180 # Record current selection and insertion point, for undo
181 self
._prevSelection
= self
._GetSelection
()
182 self
._prevInsertionPoint
= self
._GetInsertionPoint
()
183 wx
.ComboBox
.SetValue(self
, value
)
184 # text change events don't always fire, so we check validity here
185 # to make certain formatting is applied:
188 def SetValue(self
, value
):
190 This function redefines the externally accessible .SetValue to be
191 a smart "paste" of the text in question, so as not to corrupt the
192 masked control. NOTE: this must be done in the class derived
193 from the base wx control.
196 wx
.ComboBox
.SetValue(value
) # revert to base control behavior
199 # empty previous contents, replacing entire value:
200 self
._SetInsertionPoint
(0)
201 self
._SetSelection
(0, self
._masklength
)
203 if( len(value
) < self
._masklength
# value shorter than control
204 and (self
._isFloat
or self
._isInt
) # and it's a numeric control
205 and self
._ctrl
_constraints
._alignRight
): # and it's a right-aligned control
206 # try to intelligently "pad out" the value to the right size:
207 value
= self
._template
[0:self
._masklength
- len(value
)] + value
208 ## dbg('padded value = "%s"' % value)
210 # For wxComboBox, ensure that values are properly padded so that
211 # if varying length choices are supplied, they always show up
212 # in the window properly, and will be the appropriate length
214 elif self
._ctrl
_constraints
._alignRight
:
215 value
= value
.rjust(self
._masklength
)
217 value
= value
.ljust(self
._masklength
)
220 # make SetValue behave the same as if you had typed the value in:
222 value
, replace_to
= self
._Paste
(value
, raise_on_invalid
=True, just_return_value
=True)
224 self
._isNeg
= False # (clear current assumptions)
225 value
= self
._adjustFloat
(value
)
227 self
._isNeg
= False # (clear current assumptions)
228 value
= self
._adjustInt
(value
)
229 elif self
._isDate
and not self
.IsValid(value
) and self
._4digityear
:
230 value
= self
._adjustDate
(value
, fixcentury
=True)
232 # If date, year might be 2 digits vs. 4; try adjusting it:
233 if self
._isDate
and self
._4digityear
:
234 dateparts
= value
.split(' ')
235 dateparts
[0] = self
._adjustDate
(dateparts
[0], fixcentury
=True)
236 value
= string
.join(dateparts
, ' ')
237 ## dbg('adjusted value: "%s"' % value)
238 value
= self
._Paste
(value
, raise_on_invalid
=True, just_return_value
=True)
242 self
._SetValue
(value
)
243 #### dbg('queuing insertion after .SetValue', replace_to)
244 wx
.CallAfter(self
._SetInsertionPoint
, replace_to
)
245 wx
.CallAfter(self
._SetSelection
, replace_to
, replace_to
)
250 Allow mixin to refresh the base control with this function.
251 REQUIRED by any class derived from MaskedEditMixin.
253 wx
.ComboBox
.Refresh(self
)
257 This function redefines the externally accessible .Refresh() to
258 validate the contents of the masked control as it refreshes.
259 NOTE: this must be done in the class derived from the base wx control.
265 def _IsEditable(self
):
267 Allow mixin to determine if the base control is editable with this function.
268 REQUIRED by any class derived from MaskedEditMixin.
270 return not self
.__readonly
275 This function redefines the externally accessible .Cut to be
276 a smart "erase" of the text in question, so as not to corrupt the
277 masked control. NOTE: this must be done in the class derived
278 from the base wx control.
281 self
._Cut
() # call the mixin's Cut method
283 wx
.ComboBox
.Cut(self
) # else revert to base control behavior
288 This function redefines the externally accessible .Paste to be
289 a smart "paste" of the text in question, so as not to corrupt the
290 masked control. NOTE: this must be done in the class derived
291 from the base wx control.
294 self
._Paste
() # call the mixin's Paste method
296 wx
.ComboBox
.Paste(self
) # else revert to base control behavior
301 This function defines the undo operation for the control. (The default
307 wx
.ComboBox
.Undo() # else revert to base control behavior
310 def Append( self
, choice
, clientData
=None ):
312 This function override is necessary so we can keep track of any additions to the list
313 of choices, because wxComboBox doesn't have an accessor for the choice list.
314 The code here is the same as in the SetParameters() mixin function, but is
315 done for the individual value as appended, so the list can be built incrementally
316 without speed penalty.
319 if type(choice
) not in (types
.StringType
, types
.UnicodeType
):
320 raise TypeError('%s: choices must be a sequence of strings' % str(self
._index
))
321 elif not self
.IsValid(choice
):
322 raise ValueError('%s: "%s" is not a valid value for the control as specified.' % (str(self
._index
), choice
))
324 if not self
._ctrl
_constraints
._choices
:
325 self
._ctrl
_constraints
._compareChoices
= []
326 self
._ctrl
_constraints
._choices
= []
329 compareChoice
= choice
.strip()
331 if self
._ctrl
_constraints
._compareNoCase
:
332 compareChoice
= compareChoice
.lower()
334 if self
._ctrl
_constraints
._alignRight
:
335 choice
= choice
.rjust(self
._masklength
)
337 choice
= choice
.ljust(self
._masklength
)
338 if self
._ctrl
_constraints
._fillChar
!= ' ':
339 choice
= choice
.replace(' ', self
._fillChar
)
340 ## dbg('updated choice:', choice)
343 self
._ctrl
_constraints
._compareChoices
.append(compareChoice
)
344 self
._ctrl
_constraints
._choices
.append(choice
)
345 self
._choices
= self
._ctrl
_constraints
._choices
# (for shorthand)
347 if( not self
.IsValid(choice
) and
348 (not self
._ctrl
_constraints
.IsEmpty(choice
) or
349 (self
._ctrl
_constraints
.IsEmpty(choice
) and self
._ctrl
_constraints
._validRequired
) ) ):
350 raise ValueError('"%s" is not a valid value for the control "%s" as specified.' % (choice
, self
.name
))
352 wx
.ComboBox
.Append(self
, choice
, clientData
)
358 This function override is necessary so we can keep track of any additions to the list
359 of choices, because wxComboBox doesn't have an accessor for the choice list.
363 self
._ctrl
_constraints
._autoCompleteIndex
= -1
364 if self
._ctrl
_constraints
._choices
:
365 self
.SetCtrlParameters(choices
=[])
366 wx
.ComboBox
.Clear(self
)
369 def _OnCtrlParametersChanged(self
):
371 Override mixin's default OnCtrlParametersChanged to detect changes in choice list, so
372 we can update the base control:
374 if self
.controlInitialized
and self
._choices
!= self
._ctrl
_constraints
._choices
:
375 wx
.ComboBox
.Clear(self
)
376 self
._choices
= self
._ctrl
_constraints
._choices
377 for choice
in self
._choices
:
378 wx
.ComboBox
.Append( self
, choice
)
383 This function is a hack to make up for the fact that wxComboBox has no
384 method for returning the selected portion of its edit control. It
385 works, but has the nasty side effect of generating lots of intermediate
388 ## dbg(suspend=1) # turn off debugging around this function
389 ## dbg('MaskedComboBox::GetMark', indent=1)
392 return 0, 0 # no selection possible for editing
393 ## sel_start, sel_to = wxComboBox.GetMark(self) # what I'd *like* to have!
394 sel_start
= sel_to
= self
.GetInsertionPoint()
395 ## dbg("current sel_start:", sel_start)
396 value
= self
.GetValue()
397 ## dbg('value: "%s"' % value)
399 self
._ignoreChange
= True # tell _OnTextChange() to ignore next event (if any)
401 wx
.ComboBox
.Cut(self
)
402 newvalue
= self
.GetValue()
403 ## dbg("value after Cut operation:", newvalue)
405 if newvalue
!= value
: # something was selected; calculate extent
406 ## dbg("something selected")
407 sel_to
= sel_start
+ len(value
) - len(newvalue
)
408 wx
.ComboBox
.SetValue(self
, value
) # restore original value and selection (still ignoring change)
409 wx
.ComboBox
.SetInsertionPoint(self
, sel_start
)
410 wx
.ComboBox
.SetMark(self
, sel_start
, sel_to
)
412 self
._ignoreChange
= False # tell _OnTextChange() to pay attn again
414 ## dbg('computed selection:', sel_start, sel_to, indent=0, suspend=0)
415 return sel_start
, sel_to
418 def SetSelection(self
, index
):
420 Necessary for bookkeeping on choice selection, to keep current value
423 ## dbg('MaskedComboBox::SetSelection(%d)' % index)
425 self
._prevValue
= self
._curValue
426 self
._curValue
= self
._choices
[index
]
427 self
._ctrl
_constraints
._autoCompleteIndex
= index
428 wx
.ComboBox
.SetSelection(self
, index
)
431 def OnKeyDown(self
, event
):
433 This function is necessary because navigation and control key
434 events do not seem to normally be seen by the wxComboBox's
435 EVT_CHAR routine. (Tabs don't seem to be visible no matter
438 if event
.GetKeyCode() in self
._nav
+ self
._control
:
442 event
.Skip() # let mixin default KeyDown behavior occur
445 def OnSelectChoice(self
, event
):
447 This function appears to be necessary, because the processing done
448 on the text of the control somehow interferes with the combobox's
449 selection mechanism for the arrow keys.
451 ## dbg('MaskedComboBox::OnSelectChoice', indent=1)
457 value
= self
.GetValue().strip()
459 if self
._ctrl
_constraints
._compareNoCase
:
460 value
= value
.lower()
462 if event
.GetKeyCode() == wx
.WXK_UP
:
466 match_index
, partial_match
= self
._autoComplete
(
468 self
._ctrl
_constraints
._compareChoices
,
470 self
._ctrl
_constraints
._compareNoCase
,
471 current_index
= self
._ctrl
_constraints
._autoCompleteIndex
)
472 if match_index
is not None:
473 ## dbg('setting selection to', match_index)
474 # issue appropriate event to outside:
475 self
._OnAutoSelect
(self
._ctrl
_constraints
, match_index
=match_index
)
477 keep_processing
= False
479 pos
= self
._adjustPos
(self
._GetInsertionPoint
(), event
.GetKeyCode())
480 field
= self
._FindField
(pos
)
481 if self
.IsEmpty() or not field
._hasList
:
482 ## dbg('selecting 1st value in list')
483 self
._OnAutoSelect
(self
._ctrl
_constraints
, match_index
=0)
485 keep_processing
= False
487 # attempt field-level auto-complete
489 keep_processing
= self
._OnAutoCompleteField
(event
)
490 ## dbg('keep processing?', keep_processing, indent=0)
491 return keep_processing
494 def _OnAutoSelect(self
, field
, match_index
):
496 Override mixin (empty) autocomplete handler, so that autocompletion causes
497 combobox to update appropriately.
499 ## dbg('MaskedComboBox::OnAutoSelect', field._index, indent=1)
500 ## field._autoCompleteIndex = match_index
501 if field
== self
._ctrl
_constraints
:
502 self
.SetSelection(match_index
)
503 ## dbg('issuing combo selection event')
504 self
.GetEventHandler().ProcessEvent(
505 MaskedComboBoxSelectEvent( self
.GetId(), match_index
, self
) )
507 ## dbg('field._autoCompleteIndex:', match_index)
508 ## dbg('self.GetSelection():', self.GetSelection())
509 end
= self
._goEnd
(getPosOnly
=True)
510 ## dbg('scheduling set of end position to:', end)
511 # work around bug in wx 2.5
512 wx
.CallAfter(self
.SetInsertionPoint
, 0)
513 wx
.CallAfter(self
.SetInsertionPoint
, end
)
517 def _OnReturn(self
, event
):
519 For wxComboBox, it seems that if you hit return when the dropdown is
520 dropped, the event that dismisses the dropdown will also blank the
521 control, because of the implementation of wxComboBox. So here,
522 we look and if the selection is -1, and the value according to
523 (the base control!) is a value in the list, then we schedule a
524 programmatic wxComboBox.SetSelection() call to pick the appropriate
525 item in the list. (and then do the usual OnReturn bit.)
527 ## dbg('MaskedComboBox::OnReturn', indent=1)
528 ## dbg('current value: "%s"' % self.GetValue(), 'current index:', self.GetSelection())
529 if self
.GetSelection() == -1 and self
.GetValue().lower().strip() in self
._ctrl
_constraints
._compareChoices
:
530 wx
.CallAfter(self
.SetSelection
, self
._ctrl
_constraints
._autoCompleteIndex
)
532 event
.m_keyCode
= wx
.WXK_TAB
537 class ComboBox( BaseMaskedComboBox
, MaskedEditAccessorsMixin
):
539 This extra level of inheritance allows us to add the generic set of
540 masked edit parameters only to this class while allowing other
541 classes to derive from the "base" masked combobox control, and provide
542 a smaller set of valid accessor functions.