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
)
446 # Not all wx platform implementations have .GetMark, so we make the following test,
447 # and fall back to our old hack if they don't...
449 if not hasattr(wx
.ComboBox
, 'GetMark'):
452 This function is a hack to make up for the fact that wx.ComboBox has no
453 method for returning the selected portion of its edit control. It
454 works, but has the nasty side effect of generating lots of intermediate
457 ## dbg(suspend=1) # turn off debugging around this function
458 ## dbg('MaskedComboBox::GetMark', indent=1)
461 return 0, 0 # no selection possible for editing
462 ## sel_start, sel_to = wxComboBox.GetMark(self) # what I'd *like* to have!
463 sel_start
= sel_to
= self
.GetInsertionPoint()
464 ## dbg("current sel_start:", sel_start)
465 value
= self
.GetValue()
466 ## dbg('value: "%s"' % value)
468 self
._ignoreChange
= True # tell _OnTextChange() to ignore next event (if any)
470 wx
.ComboBox
.Cut(self
)
471 newvalue
= self
.GetValue()
472 ## dbg("value after Cut operation:", newvalue)
474 if newvalue
!= value
: # something was selected; calculate extent
475 ## dbg("something selected")
476 sel_to
= sel_start
+ len(value
) - len(newvalue
)
477 wx
.ComboBox
.SetValue(self
, value
) # restore original value and selection (still ignoring change)
478 wx
.ComboBox
.SetInsertionPoint(self
, sel_start
)
479 wx
.ComboBox
.SetMark(self
, sel_start
, sel_to
)
481 self
._ignoreChange
= False # tell _OnTextChange() to pay attn again
483 ## dbg('computed selection:', sel_start, sel_to, indent=0, suspend=0)
484 return sel_start
, sel_to
487 def SetSelection(self
, index
):
489 Necessary override for bookkeeping on choice selection, to keep current value
492 ## dbg('MaskedComboBox::SetSelection(%d)' % index)
494 self
._prevValue
= self
._curValue
495 self
._curValue
= self
._choices
[index
]
496 self
._ctrl
_constraints
._autoCompleteIndex
= index
497 wx
.ComboBox
.SetSelection(self
, index
)
500 def _OnKeyDownInComboBox(self
, event
):
502 This function is necessary because navigation and control key
503 events do not seem to normally be seen by the wxComboBox's
504 EVT_CHAR routine. (Tabs don't seem to be visible no matter
507 if event
.GetKeyCode() in self
._nav
+ self
._control
:
511 event
.Skip() # let mixin default KeyDown behavior occur
514 def _OnSelectChoice(self
, event
):
516 This function appears to be necessary, because the processing done
517 on the text of the control somehow interferes with the combobox's
518 selection mechanism for the arrow keys.
520 ## dbg('MaskedComboBox::OnSelectChoice', indent=1)
526 value
= self
.GetValue().strip()
528 if self
._ctrl
_constraints
._compareNoCase
:
529 value
= value
.lower()
531 if event
.GetKeyCode() == wx
.WXK_UP
:
535 match_index
, partial_match
= self
._autoComplete
(
537 self
._ctrl
_constraints
._compareChoices
,
539 self
._ctrl
_constraints
._compareNoCase
,
540 current_index
= self
._ctrl
_constraints
._autoCompleteIndex
)
541 if match_index
is not None:
542 ## dbg('setting selection to', match_index)
543 # issue appropriate event to outside:
544 self
._OnAutoSelect
(self
._ctrl
_constraints
, match_index
=match_index
)
546 keep_processing
= False
548 pos
= self
._adjustPos
(self
._GetInsertionPoint
(), event
.GetKeyCode())
549 field
= self
._FindField
(pos
)
550 if self
.IsEmpty() or not field
._hasList
:
551 ## dbg('selecting 1st value in list')
552 self
._OnAutoSelect
(self
._ctrl
_constraints
, match_index
=0)
554 keep_processing
= False
556 # attempt field-level auto-complete
558 keep_processing
= self
._OnAutoCompleteField
(event
)
559 ## dbg('keep processing?', keep_processing, indent=0)
560 return keep_processing
563 def _OnAutoSelect(self
, field
, match_index
):
565 Override mixin (empty) autocomplete handler, so that autocompletion causes
566 combobox to update appropriately.
568 ## dbg('MaskedComboBox::OnAutoSelect', field._index, indent=1)
569 ## field._autoCompleteIndex = match_index
570 if field
== self
._ctrl
_constraints
:
571 self
.SetSelection(match_index
)
572 ## dbg('issuing combo selection event')
573 self
.GetEventHandler().ProcessEvent(
574 MaskedComboBoxSelectEvent( self
.GetId(), match_index
, self
) )
576 ## dbg('field._autoCompleteIndex:', match_index)
577 ## dbg('self.GetSelection():', self.GetSelection())
578 end
= self
._goEnd
(getPosOnly
=True)
579 ## dbg('scheduling set of end position to:', end)
580 # work around bug in wx 2.5
581 wx
.CallAfter(self
.SetInsertionPoint
, 0)
582 wx
.CallAfter(self
.SetInsertionPoint
, end
)
586 def _OnReturn(self
, event
):
588 For wx.ComboBox, it seems that if you hit return when the dropdown is
589 dropped, the event that dismisses the dropdown will also blank the
590 control, because of the implementation of wxComboBox. So this function
591 examines the selection and if it is -1, and the value according to
592 (the base control!) is a value in the list, then it schedules a
593 programmatic wxComboBox.SetSelection() call to pick the appropriate
594 item in the list. (and then does the usual OnReturn bit.)
596 ## dbg('MaskedComboBox::OnReturn', indent=1)
597 ## dbg('current value: "%s"' % self.GetValue(), 'current index:', self.GetSelection())
598 if self
.GetSelection() == -1 and self
.GetValue().lower().strip() in self
._ctrl
_constraints
._compareChoices
:
599 wx
.CallAfter(self
.SetSelection
, self
._ctrl
_constraints
._autoCompleteIndex
)
601 event
.m_keyCode
= wx
.WXK_TAB
606 class ComboBox( BaseMaskedComboBox
, MaskedEditAccessorsMixin
):
608 The "user-visible" masked combobox control, this class is
609 identical to the BaseMaskedComboBox class it's derived from.
610 (This extra level of inheritance allows us to add the generic
611 set of masked edit parameters only to this class while allowing
612 other classes to derive from the "base" masked combobox control,
613 and provide a smaller set of valid accessor functions.)
614 See BaseMaskedComboBox for available methods.
619 class PreMaskedComboBox( BaseMaskedComboBox
, MaskedEditAccessorsMixin
):
621 This class exists to support the use of XRC subclassing.
623 # This should really be wx.EVT_WINDOW_CREATE but it is not
624 # currently delivered for native controls on all platforms, so
625 # we'll use EVT_SIZE instead. It should happen shortly after the
626 # control is created as the control is set to its "best" size.
627 _firstEventType
= wx
.EVT_SIZE
630 pre
= wx
.PreComboBox()
632 self
.Bind(self
._firstEventType
, self
.OnCreate
)
635 def OnCreate(self
, evt
):
636 self
.Unbind(self
._firstEventType
)
641 ## ====================
643 ## 1. Made definition of "hack" GetMark conditional on base class not
644 ## implementing it properly, to allow for migration in wx code base
645 ## while taking advantage of improvements therein for some platforms.
648 ## 1. Converted docstrings to reST format, added doc for ePyDoc.
649 ## 2. Renamed helper functions, vars etc. not intended to be visible in public
650 ## interface to code.
653 ## 1. Added .SetFont() method that properly resizes control
654 ## 2. Modified control to support construction via XRC mechanism.
655 ## 3. Added AppendItems() to conform with latest combobox.