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
.SetBestFittingSize((width
, height
))
129 # ensure value is width of the mask of the control:
130 if self
._ctrl
_constraints
._alignRight
:
131 value
= value
.rjust(self
._masklength
)
133 value
= value
.ljust(self
._masklength
)
136 self
.SetStringSelection(value
)
138 self
._SetInitialValue
(value
)
141 self
._SetKeycodeHandler
(wx
.WXK_UP
, self
.OnSelectChoice
)
142 self
._SetKeycodeHandler
(wx
.WXK_DOWN
, self
.OnSelectChoice
)
144 if setupEventHandling
:
145 ## Setup event handlers
146 self
.Bind(wx
.EVT_SET_FOCUS
, self
._OnFocus
) ## defeat automatic full selection
147 self
.Bind(wx
.EVT_KILL_FOCUS
, self
._OnKillFocus
) ## run internal validator
148 self
.Bind(wx
.EVT_LEFT_DCLICK
, self
._OnDoubleClick
) ## select field under cursor on dclick
149 self
.Bind(wx
.EVT_RIGHT_UP
, self
._OnContextMenu
) ## bring up an appropriate context menu
150 self
.Bind(wx
.EVT_CHAR
, self
._OnChar
) ## handle each keypress
151 self
.Bind(wx
.EVT_KEY_DOWN
, self
.OnKeyDown
) ## for special processing of up/down keys
152 self
.Bind(wx
.EVT_KEY_DOWN
, self
._OnKeyDown
) ## for processing the rest of the control keys
153 ## (next in evt chain)
154 self
.Bind(wx
.EVT_TEXT
, self
._OnTextChange
) ## color control appropriately & keep
155 ## track of previous value for undo
160 return "<MaskedComboBox: %s>" % self
.GetValue()
163 def _CalcSize(self
, size
=None):
165 Calculate automatic size if allowed; augment base mixin function
166 to account for the selector button.
168 size
= self
._calcSize
(size
)
169 return (size
[0]+20, size
[1])
172 def SetFont(self
, *args
, **kwargs
):
173 """ Set the font, then recalculate control size, if appropriate. """
174 wx
.ComboBox
.SetFont(self
, *args
, **kwargs
)
176 dbg('calculated size:', self
._CalcSize
())
177 self
.SetClientSize(self
._CalcSize
())
178 width
= self
.GetSize().width
179 height
= self
.GetBestSize().height
180 dbg('setting client size to:', (width
, height
))
181 self
.SetBestFittingSize((width
, height
))
184 def _GetSelection(self
):
186 Allow mixin to get the text selection of this control.
187 REQUIRED by any class derived from MaskedEditMixin.
189 return self
.GetMark()
191 def _SetSelection(self
, sel_start
, sel_to
):
193 Allow mixin to set the text selection of this control.
194 REQUIRED by any class derived from MaskedEditMixin.
196 return self
.SetMark( sel_start
, sel_to
)
199 def _GetInsertionPoint(self
):
200 return self
.GetInsertionPoint()
202 def _SetInsertionPoint(self
, pos
):
203 self
.SetInsertionPoint(pos
)
208 Allow mixin to get the raw value of the control with this function.
209 REQUIRED by any class derived from MaskedEditMixin.
211 return self
.GetValue()
213 def _SetValue(self
, value
):
215 Allow mixin to set the raw value of the control with this function.
216 REQUIRED by any class derived from MaskedEditMixin.
218 # For wxComboBox, ensure that values are properly padded so that
219 # if varying length choices are supplied, they always show up
220 # in the window properly, and will be the appropriate length
222 if self
._ctrl
_constraints
._alignRight
:
223 value
= value
.rjust(self
._masklength
)
225 value
= value
.ljust(self
._masklength
)
227 # Record current selection and insertion point, for undo
228 self
._prevSelection
= self
._GetSelection
()
229 self
._prevInsertionPoint
= self
._GetInsertionPoint
()
230 wx
.ComboBox
.SetValue(self
, value
)
231 # text change events don't always fire, so we check validity here
232 # to make certain formatting is applied:
235 def SetValue(self
, value
):
237 This function redefines the externally accessible .SetValue to be
238 a smart "paste" of the text in question, so as not to corrupt the
239 masked control. NOTE: this must be done in the class derived
240 from the base wx control.
243 wx
.ComboBox
.SetValue(value
) # revert to base control behavior
246 # empty previous contents, replacing entire value:
247 self
._SetInsertionPoint
(0)
248 self
._SetSelection
(0, self
._masklength
)
250 if( len(value
) < self
._masklength
# value shorter than control
251 and (self
._isFloat
or self
._isInt
) # and it's a numeric control
252 and self
._ctrl
_constraints
._alignRight
): # and it's a right-aligned control
253 # try to intelligently "pad out" the value to the right size:
254 value
= self
._template
[0:self
._masklength
- len(value
)] + value
255 ## dbg('padded value = "%s"' % value)
257 # For wxComboBox, ensure that values are properly padded so that
258 # if varying length choices are supplied, they always show up
259 # in the window properly, and will be the appropriate length
261 elif self
._ctrl
_constraints
._alignRight
:
262 value
= value
.rjust(self
._masklength
)
264 value
= value
.ljust(self
._masklength
)
267 # make SetValue behave the same as if you had typed the value in:
269 value
, replace_to
= self
._Paste
(value
, raise_on_invalid
=True, just_return_value
=True)
271 self
._isNeg
= False # (clear current assumptions)
272 value
= self
._adjustFloat
(value
)
274 self
._isNeg
= False # (clear current assumptions)
275 value
= self
._adjustInt
(value
)
276 elif self
._isDate
and not self
.IsValid(value
) and self
._4digityear
:
277 value
= self
._adjustDate
(value
, fixcentury
=True)
279 # If date, year might be 2 digits vs. 4; try adjusting it:
280 if self
._isDate
and self
._4digityear
:
281 dateparts
= value
.split(' ')
282 dateparts
[0] = self
._adjustDate
(dateparts
[0], fixcentury
=True)
283 value
= string
.join(dateparts
, ' ')
284 ## dbg('adjusted value: "%s"' % value)
285 value
= self
._Paste
(value
, raise_on_invalid
=True, just_return_value
=True)
289 self
._SetValue
(value
)
290 #### dbg('queuing insertion after .SetValue', replace_to)
291 wx
.CallAfter(self
._SetInsertionPoint
, replace_to
)
292 wx
.CallAfter(self
._SetSelection
, replace_to
, replace_to
)
297 Allow mixin to refresh the base control with this function.
298 REQUIRED by any class derived from MaskedEditMixin.
300 wx
.ComboBox
.Refresh(self
)
304 This function redefines the externally accessible .Refresh() to
305 validate the contents of the masked control as it refreshes.
306 NOTE: this must be done in the class derived from the base wx control.
312 def _IsEditable(self
):
314 Allow mixin to determine if the base control is editable with this function.
315 REQUIRED by any class derived from MaskedEditMixin.
317 return not self
.__readonly
322 This function redefines the externally accessible .Cut to be
323 a smart "erase" of the text in question, so as not to corrupt the
324 masked control. NOTE: this must be done in the class derived
325 from the base wx control.
328 self
._Cut
() # call the mixin's Cut method
330 wx
.ComboBox
.Cut(self
) # else revert to base control behavior
335 This function redefines the externally accessible .Paste to be
336 a smart "paste" of the text in question, so as not to corrupt the
337 masked control. NOTE: this must be done in the class derived
338 from the base wx control.
341 self
._Paste
() # call the mixin's Paste method
343 wx
.ComboBox
.Paste(self
) # else revert to base control behavior
348 This function defines the undo operation for the control. (The default
354 wx
.ComboBox
.Undo() # else revert to base control behavior
356 def Append( self
, choice
, clientData
=None ):
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.
360 The code here is the same as in the SetParameters() mixin function, but is
361 done for the individual value as appended, so the list can be built incrementally
362 without speed penalty.
365 if type(choice
) not in (types
.StringType
, types
.UnicodeType
):
366 raise TypeError('%s: choices must be a sequence of strings' % str(self
._index
))
367 elif not self
.IsValid(choice
):
368 raise ValueError('%s: "%s" is not a valid value for the control as specified.' % (str(self
._index
), choice
))
370 if not self
._ctrl
_constraints
._choices
:
371 self
._ctrl
_constraints
._compareChoices
= []
372 self
._ctrl
_constraints
._choices
= []
375 compareChoice
= choice
.strip()
377 if self
._ctrl
_constraints
._compareNoCase
:
378 compareChoice
= compareChoice
.lower()
380 if self
._ctrl
_constraints
._alignRight
:
381 choice
= choice
.rjust(self
._masklength
)
383 choice
= choice
.ljust(self
._masklength
)
384 if self
._ctrl
_constraints
._fillChar
!= ' ':
385 choice
= choice
.replace(' ', self
._fillChar
)
386 ## dbg('updated choice:', choice)
389 self
._ctrl
_constraints
._compareChoices
.append(compareChoice
)
390 self
._ctrl
_constraints
._choices
.append(choice
)
391 self
._choices
= self
._ctrl
_constraints
._choices
# (for shorthand)
393 if( not self
.IsValid(choice
) and
394 (not self
._ctrl
_constraints
.IsEmpty(choice
) or
395 (self
._ctrl
_constraints
.IsEmpty(choice
) and self
._ctrl
_constraints
._validRequired
) ) ):
396 raise ValueError('"%s" is not a valid value for the control "%s" as specified.' % (choice
, self
.name
))
398 wx
.ComboBox
.Append(self
, choice
, clientData
)
401 def AppendItems( self
, choices
):
403 AppendItems() is handled in terms of Append, to avoid code replication.
405 for choice
in choices
:
411 This function override is necessary so we can keep track of any additions to the list
412 of choices, because wxComboBox doesn't have an accessor for the choice list.
416 self
._ctrl
_constraints
._autoCompleteIndex
= -1
417 if self
._ctrl
_constraints
._choices
:
418 self
.SetCtrlParameters(choices
=[])
419 wx
.ComboBox
.Clear(self
)
422 def _OnCtrlParametersChanged(self
):
424 Override mixin's default OnCtrlParametersChanged to detect changes in choice list, so
425 we can update the base control:
427 if self
.controlInitialized
and self
._choices
!= self
._ctrl
_constraints
._choices
:
428 wx
.ComboBox
.Clear(self
)
429 self
._choices
= self
._ctrl
_constraints
._choices
430 for choice
in self
._choices
:
431 wx
.ComboBox
.Append( self
, choice
)
436 This function is a hack to make up for the fact that wxComboBox has no
437 method for returning the selected portion of its edit control. It
438 works, but has the nasty side effect of generating lots of intermediate
441 ## dbg(suspend=1) # turn off debugging around this function
442 ## dbg('MaskedComboBox::GetMark', indent=1)
445 return 0, 0 # no selection possible for editing
446 ## sel_start, sel_to = wxComboBox.GetMark(self) # what I'd *like* to have!
447 sel_start
= sel_to
= self
.GetInsertionPoint()
448 ## dbg("current sel_start:", sel_start)
449 value
= self
.GetValue()
450 ## dbg('value: "%s"' % value)
452 self
._ignoreChange
= True # tell _OnTextChange() to ignore next event (if any)
454 wx
.ComboBox
.Cut(self
)
455 newvalue
= self
.GetValue()
456 ## dbg("value after Cut operation:", newvalue)
458 if newvalue
!= value
: # something was selected; calculate extent
459 ## dbg("something selected")
460 sel_to
= sel_start
+ len(value
) - len(newvalue
)
461 wx
.ComboBox
.SetValue(self
, value
) # restore original value and selection (still ignoring change)
462 wx
.ComboBox
.SetInsertionPoint(self
, sel_start
)
463 wx
.ComboBox
.SetMark(self
, sel_start
, sel_to
)
465 self
._ignoreChange
= False # tell _OnTextChange() to pay attn again
467 ## dbg('computed selection:', sel_start, sel_to, indent=0, suspend=0)
468 return sel_start
, sel_to
471 def SetSelection(self
, index
):
473 Necessary for bookkeeping on choice selection, to keep current value
476 ## dbg('MaskedComboBox::SetSelection(%d)' % index)
478 self
._prevValue
= self
._curValue
479 self
._curValue
= self
._choices
[index
]
480 self
._ctrl
_constraints
._autoCompleteIndex
= index
481 wx
.ComboBox
.SetSelection(self
, index
)
484 def OnKeyDown(self
, event
):
486 This function is necessary because navigation and control key
487 events do not seem to normally be seen by the wxComboBox's
488 EVT_CHAR routine. (Tabs don't seem to be visible no matter
491 if event
.GetKeyCode() in self
._nav
+ self
._control
:
495 event
.Skip() # let mixin default KeyDown behavior occur
498 def OnSelectChoice(self
, event
):
500 This function appears to be necessary, because the processing done
501 on the text of the control somehow interferes with the combobox's
502 selection mechanism for the arrow keys.
504 ## dbg('MaskedComboBox::OnSelectChoice', indent=1)
510 value
= self
.GetValue().strip()
512 if self
._ctrl
_constraints
._compareNoCase
:
513 value
= value
.lower()
515 if event
.GetKeyCode() == wx
.WXK_UP
:
519 match_index
, partial_match
= self
._autoComplete
(
521 self
._ctrl
_constraints
._compareChoices
,
523 self
._ctrl
_constraints
._compareNoCase
,
524 current_index
= self
._ctrl
_constraints
._autoCompleteIndex
)
525 if match_index
is not None:
526 ## dbg('setting selection to', match_index)
527 # issue appropriate event to outside:
528 self
._OnAutoSelect
(self
._ctrl
_constraints
, match_index
=match_index
)
530 keep_processing
= False
532 pos
= self
._adjustPos
(self
._GetInsertionPoint
(), event
.GetKeyCode())
533 field
= self
._FindField
(pos
)
534 if self
.IsEmpty() or not field
._hasList
:
535 ## dbg('selecting 1st value in list')
536 self
._OnAutoSelect
(self
._ctrl
_constraints
, match_index
=0)
538 keep_processing
= False
540 # attempt field-level auto-complete
542 keep_processing
= self
._OnAutoCompleteField
(event
)
543 ## dbg('keep processing?', keep_processing, indent=0)
544 return keep_processing
547 def _OnAutoSelect(self
, field
, match_index
):
549 Override mixin (empty) autocomplete handler, so that autocompletion causes
550 combobox to update appropriately.
552 ## dbg('MaskedComboBox::OnAutoSelect', field._index, indent=1)
553 ## field._autoCompleteIndex = match_index
554 if field
== self
._ctrl
_constraints
:
555 self
.SetSelection(match_index
)
556 ## dbg('issuing combo selection event')
557 self
.GetEventHandler().ProcessEvent(
558 MaskedComboBoxSelectEvent( self
.GetId(), match_index
, self
) )
560 ## dbg('field._autoCompleteIndex:', match_index)
561 ## dbg('self.GetSelection():', self.GetSelection())
562 end
= self
._goEnd
(getPosOnly
=True)
563 ## dbg('scheduling set of end position to:', end)
564 # work around bug in wx 2.5
565 wx
.CallAfter(self
.SetInsertionPoint
, 0)
566 wx
.CallAfter(self
.SetInsertionPoint
, end
)
570 def _OnReturn(self
, event
):
572 For wxComboBox, it seems that if you hit return when the dropdown is
573 dropped, the event that dismisses the dropdown will also blank the
574 control, because of the implementation of wxComboBox. So here,
575 we look and if the selection is -1, and the value according to
576 (the base control!) is a value in the list, then we schedule a
577 programmatic wxComboBox.SetSelection() call to pick the appropriate
578 item in the list. (and then do the usual OnReturn bit.)
580 ## dbg('MaskedComboBox::OnReturn', indent=1)
581 ## dbg('current value: "%s"' % self.GetValue(), 'current index:', self.GetSelection())
582 if self
.GetSelection() == -1 and self
.GetValue().lower().strip() in self
._ctrl
_constraints
._compareChoices
:
583 wx
.CallAfter(self
.SetSelection
, self
._ctrl
_constraints
._autoCompleteIndex
)
585 event
.m_keyCode
= wx
.WXK_TAB
590 class ComboBox( BaseMaskedComboBox
, MaskedEditAccessorsMixin
):
592 This extra level of inheritance allows us to add the generic set of
593 masked edit parameters only to this class while allowing other
594 classes to derive from the "base" masked combobox control, and provide
595 a smaller set of valid accessor functions.
600 class PreMaskedComboBox( BaseMaskedComboBox
, MaskedEditAccessorsMixin
):
602 This allows us to use XRC subclassing.
604 # This should really be wx.EVT_WINDOW_CREATE but it is not
605 # currently delivered for native controls on all platforms, so
606 # we'll use EVT_SIZE instead. It should happen shortly after the
607 # control is created as the control is set to its "best" size.
608 _firstEventType
= wx
.EVT_SIZE
611 pre
= wx
.PreComboBox()
613 self
.Bind(self
._firstEventType
, self
.OnCreate
)
616 def OnCreate(self
, evt
):
617 self
.Unbind(self
._firstEventType
)
622 ## ====================
624 ## 1. Added .SetFont() method that properly resizes control
625 ## 2. Modified control to support construction via XRC mechanism.
626 ## 3. Added AppendItems() to conform with latest combobox.