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 kwargs
['choices'] = choices
## set up maskededit to work with choice list too
59 ## Since combobox completion is case-insensitive, always validate same way
60 if not kwargs
.has_key('compareNoCase'):
61 kwargs
['compareNoCase'] = True
63 MaskedEditMixin
.__init
__( self
, name
, **kwargs
)
65 self
._choices
= self
._ctrl
_constraints
._choices
66 ## dbg('self._choices:', self._choices)
68 if self
._ctrl
_constraints
._alignRight
:
69 choices
= [choice
.rjust(self
._masklength
) for choice
in choices
]
71 choices
= [choice
.ljust(self
._masklength
) for choice
in choices
]
73 wx
.ComboBox
.__init
__(self
, parent
, id, value
='',
75 choices
=choices
, style
=style|wx
.WANTS_CHARS
,
78 self
.controlInitialized
= True
80 self
._PostInit
(style
=style
, setupEventHandling
=setupEventHandling
,
81 name
=name
, value
=value
, **kwargs
)
84 def _PostInit(self
, style
=wx
.CB_DROPDOWN
,
85 setupEventHandling
= True, ## setup event handling by default):
86 name
= "maskedComboBox", value
='', **kwargs
):
88 # This is necessary, because wxComboBox currently provides no
89 # method for determining later if this was specified in the
90 # constructor for the control...
91 self
.__readonly
= style
& wx
.CB_READONLY
== wx
.CB_READONLY
93 if not hasattr(self
, 'controlInitialized'):
95 self
.controlInitialized
= True ## must have been called via XRC, therefore base class is constructed
96 if not kwargs
.has_key('choices'):
98 kwargs
['choices'] = choices
## set up maskededit to work with choice list too
101 ## Since combobox completion is case-insensitive, always validate same way
102 if not kwargs
.has_key('compareNoCase'):
103 kwargs
['compareNoCase'] = True
105 MaskedEditMixin
.__init
__( self
, name
, **kwargs
)
107 self
._choices
= self
._ctrl
_constraints
._choices
108 ## dbg('self._choices:', self._choices)
110 if self
._ctrl
_constraints
._alignRight
:
111 choices
= [choice
.rjust(self
._masklength
) for choice
in choices
]
113 choices
= [choice
.ljust(self
._masklength
) for choice
in choices
]
114 wx
.ComboBox
.Clear(self
)
115 wx
.ComboBox
.AppendItems(self
, choices
)
118 # Set control font - fixed width by default
122 self
.SetClientSize(self
._CalcSize
())
123 width
= self
.GetSize().width
124 height
= self
.GetBestSize().height
125 self
.SetSize((width
, height
))
126 self
.SetSizeHints((width
, height
))
130 # ensure value is width of the mask of the control:
131 if self
._ctrl
_constraints
._alignRight
:
132 value
= value
.rjust(self
._masklength
)
134 value
= value
.ljust(self
._masklength
)
137 self
.SetStringSelection(value
)
139 self
._SetInitialValue
(value
)
142 self
._SetKeycodeHandler
(wx
.WXK_UP
, self
.OnSelectChoice
)
143 self
._SetKeycodeHandler
(wx
.WXK_DOWN
, self
.OnSelectChoice
)
145 if setupEventHandling
:
146 ## Setup event handlers
147 self
.Bind(wx
.EVT_SET_FOCUS
, self
._OnFocus
) ## defeat automatic full selection
148 self
.Bind(wx
.EVT_KILL_FOCUS
, self
._OnKillFocus
) ## run internal validator
149 self
.Bind(wx
.EVT_LEFT_DCLICK
, self
._OnDoubleClick
) ## select field under cursor on dclick
150 self
.Bind(wx
.EVT_RIGHT_UP
, self
._OnContextMenu
) ## bring up an appropriate context menu
151 self
.Bind(wx
.EVT_CHAR
, self
._OnChar
) ## handle each keypress
152 self
.Bind(wx
.EVT_KEY_DOWN
, self
.OnKeyDown
) ## for special processing of up/down keys
153 self
.Bind(wx
.EVT_KEY_DOWN
, self
._OnKeyDown
) ## for processing the rest of the control keys
154 ## (next in evt chain)
155 self
.Bind(wx
.EVT_TEXT
, self
._OnTextChange
) ## color control appropriately & keep
156 ## track of previous value for undo
161 return "<MaskedComboBox: %s>" % self
.GetValue()
164 def _CalcSize(self
, size
=None):
166 Calculate automatic size if allowed; augment base mixin function
167 to account for the selector button.
169 size
= self
._calcSize
(size
)
170 return (size
[0]+20, size
[1])
173 def SetFont(self
, *args
, **kwargs
):
174 """ Set the font, then recalculate control size, if appropriate. """
175 wx
.ComboBox
.SetFont(self
, *args
, **kwargs
)
177 dbg('calculated size:', self
._CalcSize
())
178 self
.SetClientSize(self
._CalcSize
())
179 width
= self
.GetSize().width
180 height
= self
.GetBestSize().height
181 dbg('setting client size to:', (width
, height
))
182 self
.SetSize((width
, height
))
183 self
.SetSizeHints((width
, height
))
186 def _GetSelection(self
):
188 Allow mixin to get the text selection of this control.
189 REQUIRED by any class derived from MaskedEditMixin.
191 return self
.GetMark()
193 def _SetSelection(self
, sel_start
, sel_to
):
195 Allow mixin to set the text selection of this control.
196 REQUIRED by any class derived from MaskedEditMixin.
198 return self
.SetMark( sel_start
, sel_to
)
201 def _GetInsertionPoint(self
):
202 return self
.GetInsertionPoint()
204 def _SetInsertionPoint(self
, pos
):
205 self
.SetInsertionPoint(pos
)
210 Allow mixin to get the raw value of the control with this function.
211 REQUIRED by any class derived from MaskedEditMixin.
213 return self
.GetValue()
215 def _SetValue(self
, value
):
217 Allow mixin to set the raw value of the control with this function.
218 REQUIRED by any class derived from MaskedEditMixin.
220 # For wxComboBox, ensure that values are properly padded so that
221 # if varying length choices are supplied, they always show up
222 # in the window properly, and will be the appropriate length
224 if self
._ctrl
_constraints
._alignRight
:
225 value
= value
.rjust(self
._masklength
)
227 value
= value
.ljust(self
._masklength
)
229 # Record current selection and insertion point, for undo
230 self
._prevSelection
= self
._GetSelection
()
231 self
._prevInsertionPoint
= self
._GetInsertionPoint
()
232 wx
.ComboBox
.SetValue(self
, value
)
233 # text change events don't always fire, so we check validity here
234 # to make certain formatting is applied:
237 def SetValue(self
, value
):
239 This function redefines the externally accessible .SetValue to be
240 a smart "paste" of the text in question, so as not to corrupt the
241 masked control. NOTE: this must be done in the class derived
242 from the base wx control.
245 wx
.ComboBox
.SetValue(value
) # revert to base control behavior
248 # empty previous contents, replacing entire value:
249 self
._SetInsertionPoint
(0)
250 self
._SetSelection
(0, self
._masklength
)
252 if( len(value
) < self
._masklength
# value shorter than control
253 and (self
._isFloat
or self
._isInt
) # and it's a numeric control
254 and self
._ctrl
_constraints
._alignRight
): # and it's a right-aligned control
255 # try to intelligently "pad out" the value to the right size:
256 value
= self
._template
[0:self
._masklength
- len(value
)] + value
257 ## dbg('padded value = "%s"' % value)
259 # For wxComboBox, ensure that values are properly padded so that
260 # if varying length choices are supplied, they always show up
261 # in the window properly, and will be the appropriate length
263 elif self
._ctrl
_constraints
._alignRight
:
264 value
= value
.rjust(self
._masklength
)
266 value
= value
.ljust(self
._masklength
)
269 # make SetValue behave the same as if you had typed the value in:
271 value
, replace_to
= self
._Paste
(value
, raise_on_invalid
=True, just_return_value
=True)
273 self
._isNeg
= False # (clear current assumptions)
274 value
= self
._adjustFloat
(value
)
276 self
._isNeg
= False # (clear current assumptions)
277 value
= self
._adjustInt
(value
)
278 elif self
._isDate
and not self
.IsValid(value
) and self
._4digityear
:
279 value
= self
._adjustDate
(value
, fixcentury
=True)
281 # If date, year might be 2 digits vs. 4; try adjusting it:
282 if self
._isDate
and self
._4digityear
:
283 dateparts
= value
.split(' ')
284 dateparts
[0] = self
._adjustDate
(dateparts
[0], fixcentury
=True)
285 value
= string
.join(dateparts
, ' ')
286 ## dbg('adjusted value: "%s"' % value)
287 value
= self
._Paste
(value
, raise_on_invalid
=True, just_return_value
=True)
291 self
._SetValue
(value
)
292 #### dbg('queuing insertion after .SetValue', replace_to)
293 wx
.CallAfter(self
._SetInsertionPoint
, replace_to
)
294 wx
.CallAfter(self
._SetSelection
, replace_to
, replace_to
)
299 Allow mixin to refresh the base control with this function.
300 REQUIRED by any class derived from MaskedEditMixin.
302 wx
.ComboBox
.Refresh(self
)
306 This function redefines the externally accessible .Refresh() to
307 validate the contents of the masked control as it refreshes.
308 NOTE: this must be done in the class derived from the base wx control.
314 def _IsEditable(self
):
316 Allow mixin to determine if the base control is editable with this function.
317 REQUIRED by any class derived from MaskedEditMixin.
319 return not self
.__readonly
324 This function redefines the externally accessible .Cut to be
325 a smart "erase" of the text in question, so as not to corrupt the
326 masked control. NOTE: this must be done in the class derived
327 from the base wx control.
330 self
._Cut
() # call the mixin's Cut method
332 wx
.ComboBox
.Cut(self
) # else revert to base control behavior
337 This function redefines the externally accessible .Paste to be
338 a smart "paste" of the text in question, so as not to corrupt the
339 masked control. NOTE: this must be done in the class derived
340 from the base wx control.
343 self
._Paste
() # call the mixin's Paste method
345 wx
.ComboBox
.Paste(self
) # else revert to base control behavior
350 This function defines the undo operation for the control. (The default
356 wx
.ComboBox
.Undo() # else revert to base control behavior
358 def Append( self
, choice
, clientData
=None ):
360 This function override is necessary so we can keep track of any additions to the list
361 of choices, because wxComboBox doesn't have an accessor for the choice list.
362 The code here is the same as in the SetParameters() mixin function, but is
363 done for the individual value as appended, so the list can be built incrementally
364 without speed penalty.
367 if type(choice
) not in (types
.StringType
, types
.UnicodeType
):
368 raise TypeError('%s: choices must be a sequence of strings' % str(self
._index
))
369 elif not self
.IsValid(choice
):
370 raise ValueError('%s: "%s" is not a valid value for the control as specified.' % (str(self
._index
), choice
))
372 if not self
._ctrl
_constraints
._choices
:
373 self
._ctrl
_constraints
._compareChoices
= []
374 self
._ctrl
_constraints
._choices
= []
377 compareChoice
= choice
.strip()
379 if self
._ctrl
_constraints
._compareNoCase
:
380 compareChoice
= compareChoice
.lower()
382 if self
._ctrl
_constraints
._alignRight
:
383 choice
= choice
.rjust(self
._masklength
)
385 choice
= choice
.ljust(self
._masklength
)
386 if self
._ctrl
_constraints
._fillChar
!= ' ':
387 choice
= choice
.replace(' ', self
._fillChar
)
388 ## dbg('updated choice:', choice)
391 self
._ctrl
_constraints
._compareChoices
.append(compareChoice
)
392 self
._ctrl
_constraints
._choices
.append(choice
)
393 self
._choices
= self
._ctrl
_constraints
._choices
# (for shorthand)
395 if( not self
.IsValid(choice
) and
396 (not self
._ctrl
_constraints
.IsEmpty(choice
) or
397 (self
._ctrl
_constraints
.IsEmpty(choice
) and self
._ctrl
_constraints
._validRequired
) ) ):
398 raise ValueError('"%s" is not a valid value for the control "%s" as specified.' % (choice
, self
.name
))
400 wx
.ComboBox
.Append(self
, choice
, clientData
)
403 def AppendItems( self
, choices
):
405 AppendItems() is handled in terms of Append, to avoid code replication.
407 for choice
in choices
:
413 This function override is necessary so we can keep track of any additions to the list
414 of choices, because wxComboBox doesn't have an accessor for the choice list.
418 self
._ctrl
_constraints
._autoCompleteIndex
= -1
419 if self
._ctrl
_constraints
._choices
:
420 self
.SetCtrlParameters(choices
=[])
421 wx
.ComboBox
.Clear(self
)
424 def _OnCtrlParametersChanged(self
):
426 Override mixin's default OnCtrlParametersChanged to detect changes in choice list, so
427 we can update the base control:
429 if self
.controlInitialized
and self
._choices
!= self
._ctrl
_constraints
._choices
:
430 wx
.ComboBox
.Clear(self
)
431 self
._choices
= self
._ctrl
_constraints
._choices
432 for choice
in self
._choices
:
433 wx
.ComboBox
.Append( self
, choice
)
438 This function is a hack to make up for the fact that wxComboBox has no
439 method for returning the selected portion of its edit control. It
440 works, but has the nasty side effect of generating lots of intermediate
443 ## dbg(suspend=1) # turn off debugging around this function
444 ## dbg('MaskedComboBox::GetMark', indent=1)
447 return 0, 0 # no selection possible for editing
448 ## sel_start, sel_to = wxComboBox.GetMark(self) # what I'd *like* to have!
449 sel_start
= sel_to
= self
.GetInsertionPoint()
450 ## dbg("current sel_start:", sel_start)
451 value
= self
.GetValue()
452 ## dbg('value: "%s"' % value)
454 self
._ignoreChange
= True # tell _OnTextChange() to ignore next event (if any)
456 wx
.ComboBox
.Cut(self
)
457 newvalue
= self
.GetValue()
458 ## dbg("value after Cut operation:", newvalue)
460 if newvalue
!= value
: # something was selected; calculate extent
461 ## dbg("something selected")
462 sel_to
= sel_start
+ len(value
) - len(newvalue
)
463 wx
.ComboBox
.SetValue(self
, value
) # restore original value and selection (still ignoring change)
464 wx
.ComboBox
.SetInsertionPoint(self
, sel_start
)
465 wx
.ComboBox
.SetMark(self
, sel_start
, sel_to
)
467 self
._ignoreChange
= False # tell _OnTextChange() to pay attn again
469 ## dbg('computed selection:', sel_start, sel_to, indent=0, suspend=0)
470 return sel_start
, sel_to
473 def SetSelection(self
, index
):
475 Necessary for bookkeeping on choice selection, to keep current value
478 ## dbg('MaskedComboBox::SetSelection(%d)' % index)
480 self
._prevValue
= self
._curValue
481 self
._curValue
= self
._choices
[index
]
482 self
._ctrl
_constraints
._autoCompleteIndex
= index
483 wx
.ComboBox
.SetSelection(self
, index
)
486 def OnKeyDown(self
, event
):
488 This function is necessary because navigation and control key
489 events do not seem to normally be seen by the wxComboBox's
490 EVT_CHAR routine. (Tabs don't seem to be visible no matter
493 if event
.GetKeyCode() in self
._nav
+ self
._control
:
497 event
.Skip() # let mixin default KeyDown behavior occur
500 def OnSelectChoice(self
, event
):
502 This function appears to be necessary, because the processing done
503 on the text of the control somehow interferes with the combobox's
504 selection mechanism for the arrow keys.
506 ## dbg('MaskedComboBox::OnSelectChoice', indent=1)
512 value
= self
.GetValue().strip()
514 if self
._ctrl
_constraints
._compareNoCase
:
515 value
= value
.lower()
517 if event
.GetKeyCode() == wx
.WXK_UP
:
521 match_index
, partial_match
= self
._autoComplete
(
523 self
._ctrl
_constraints
._compareChoices
,
525 self
._ctrl
_constraints
._compareNoCase
,
526 current_index
= self
._ctrl
_constraints
._autoCompleteIndex
)
527 if match_index
is not None:
528 ## dbg('setting selection to', match_index)
529 # issue appropriate event to outside:
530 self
._OnAutoSelect
(self
._ctrl
_constraints
, match_index
=match_index
)
532 keep_processing
= False
534 pos
= self
._adjustPos
(self
._GetInsertionPoint
(), event
.GetKeyCode())
535 field
= self
._FindField
(pos
)
536 if self
.IsEmpty() or not field
._hasList
:
537 ## dbg('selecting 1st value in list')
538 self
._OnAutoSelect
(self
._ctrl
_constraints
, match_index
=0)
540 keep_processing
= False
542 # attempt field-level auto-complete
544 keep_processing
= self
._OnAutoCompleteField
(event
)
545 ## dbg('keep processing?', keep_processing, indent=0)
546 return keep_processing
549 def _OnAutoSelect(self
, field
, match_index
):
551 Override mixin (empty) autocomplete handler, so that autocompletion causes
552 combobox to update appropriately.
554 ## dbg('MaskedComboBox::OnAutoSelect', field._index, indent=1)
555 ## field._autoCompleteIndex = match_index
556 if field
== self
._ctrl
_constraints
:
557 self
.SetSelection(match_index
)
558 ## dbg('issuing combo selection event')
559 self
.GetEventHandler().ProcessEvent(
560 MaskedComboBoxSelectEvent( self
.GetId(), match_index
, self
) )
562 ## dbg('field._autoCompleteIndex:', match_index)
563 ## dbg('self.GetSelection():', self.GetSelection())
564 end
= self
._goEnd
(getPosOnly
=True)
565 ## dbg('scheduling set of end position to:', end)
566 # work around bug in wx 2.5
567 wx
.CallAfter(self
.SetInsertionPoint
, 0)
568 wx
.CallAfter(self
.SetInsertionPoint
, end
)
572 def _OnReturn(self
, event
):
574 For wxComboBox, it seems that if you hit return when the dropdown is
575 dropped, the event that dismisses the dropdown will also blank the
576 control, because of the implementation of wxComboBox. So here,
577 we look and if the selection is -1, and the value according to
578 (the base control!) is a value in the list, then we schedule a
579 programmatic wxComboBox.SetSelection() call to pick the appropriate
580 item in the list. (and then do the usual OnReturn bit.)
582 ## dbg('MaskedComboBox::OnReturn', indent=1)
583 ## dbg('current value: "%s"' % self.GetValue(), 'current index:', self.GetSelection())
584 if self
.GetSelection() == -1 and self
.GetValue().lower().strip() in self
._ctrl
_constraints
._compareChoices
:
585 wx
.CallAfter(self
.SetSelection
, self
._ctrl
_constraints
._autoCompleteIndex
)
587 event
.m_keyCode
= wx
.WXK_TAB
592 class ComboBox( BaseMaskedComboBox
, MaskedEditAccessorsMixin
):
594 This extra level of inheritance allows us to add the generic set of
595 masked edit parameters only to this class while allowing other
596 classes to derive from the "base" masked combobox control, and provide
597 a smaller set of valid accessor functions.
602 class PreMaskedComboBox( BaseMaskedComboBox
, MaskedEditAccessorsMixin
):
604 This allows us to use XRC subclassing.
606 # This should really be wx.EVT_WINDOW_CREATE but it is not
607 # currently delivered for native controls on all platforms, so
608 # we'll use EVT_SIZE instead. It should happen shortly after the
609 # control is created as the control is set to its "best" size.
610 _firstEventType
= wx
.EVT_SIZE
613 pre
= wx
.PreComboBox()
615 self
.Bind(self
._firstEventType
, self
.OnCreate
)
618 def OnCreate(self
, evt
):
619 self
.Unbind(self
._firstEventType
)
624 ## ====================
626 ## 1. Added .SetFont() method that properly resizes control
627 ## 2. Modified control to support construction via XRC mechanism.
628 ## 3. Added AppendItems() to conform with latest combobox.