]> git.saurik.com Git - wxWidgets.git/blob - wxPython/wx/lib/masked/combobox.py
MaskedEdit updates from Will Sadkin:
[wxWidgets.git] / wxPython / wx / lib / masked / combobox.py
1 #----------------------------------------------------------------------------
2 # Name: masked.combobox.py
3 # Authors: Will Sadkin
4 # Email: wsadkin@nameconnector.com
5 # Created: 02/11/2003
6 # Copyright: (c) 2003 by Will Sadkin, 2003
7 # RCS-ID: $Id$
8 # License: wxWidgets license
9 #----------------------------------------------------------------------------
10 #
11 # This masked edit class allows for the semantics of masked controls
12 # to be applied to combo boxes.
13 #
14 #----------------------------------------------------------------------------
15
16 """
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.
20 """
21
22 import wx, types, string
23 from wx.lib.masked import *
24
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
28 ##dbg = Logger()
29 ##dbg(enable=0)
30
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):
35 """
36 Because calling SetSelection programmatically does not fire EVT_COMBOBOX
37 events, the derived control has to do it itself when it auto-completes.
38 """
39 def __init__(self, id, selection = 0, object=None):
40 wx.PyCommandEvent.__init__(self, wx.wxEVT_COMMAND_COMBOBOX_SELECTED, id)
41
42 self.__selection = selection
43 self.SetEventObject(object)
44
45 def GetSelection(self):
46 """Retrieve the value of the control at the time
47 this event was generated."""
48 return self.__selection
49
50
51 class BaseMaskedComboBox( wx.ComboBox, MaskedEditMixin ):
52 """
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.
56 """
57 def __init__( self, parent, id=-1, value = '',
58 pos = wx.DefaultPosition,
59 size = wx.DefaultSize,
60 choices = [],
61 style = wx.CB_DROPDOWN,
62 validator = wx.DefaultValidator,
63 name = "maskedComboBox",
64 setupEventHandling = True, ## setup event handling by default):
65 **kwargs):
66
67
68 kwargs['choices'] = choices ## set up maskededit to work with choice list too
69
70 ## Since combobox completion is case-insensitive, always validate same way
71 if not kwargs.has_key('compareNoCase'):
72 kwargs['compareNoCase'] = True
73
74 MaskedEditMixin.__init__( self, name, **kwargs )
75
76 self._choices = self._ctrl_constraints._choices
77 ## dbg('self._choices:', self._choices)
78
79 if self._ctrl_constraints._alignRight:
80 choices = [choice.rjust(self._masklength) for choice in choices]
81 else:
82 choices = [choice.ljust(self._masklength) for choice in choices]
83
84 wx.ComboBox.__init__(self, parent, id, value='',
85 pos=pos, size = size,
86 choices=choices, style=style|wx.WANTS_CHARS,
87 validator=validator,
88 name=name)
89 self.controlInitialized = True
90
91 self._PostInit(style=style, setupEventHandling=setupEventHandling,
92 name=name, value=value, **kwargs)
93
94
95 def _PostInit(self, style=wx.CB_DROPDOWN,
96 setupEventHandling = True, ## setup event handling by default):
97 name = "maskedComboBox", value='', **kwargs):
98
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
103
104 if not hasattr(self, 'controlInitialized'):
105
106 self.controlInitialized = True ## must have been called via XRC, therefore base class is constructed
107 if not kwargs.has_key('choices'):
108 choices=[]
109 kwargs['choices'] = choices ## set up maskededit to work with choice list too
110 self._choices = []
111
112 ## Since combobox completion is case-insensitive, always validate same way
113 if not kwargs.has_key('compareNoCase'):
114 kwargs['compareNoCase'] = True
115
116 MaskedEditMixin.__init__( self, name, **kwargs )
117
118 self._choices = self._ctrl_constraints._choices
119 ## dbg('self._choices:', self._choices)
120
121 if self._ctrl_constraints._alignRight:
122 choices = [choice.rjust(self._masklength) for choice in choices]
123 else:
124 choices = [choice.ljust(self._masklength) for choice in choices]
125 wx.ComboBox.Clear(self)
126 wx.ComboBox.AppendItems(self, choices)
127
128
129 # Set control font - fixed width by default
130 self._setFont()
131
132 if self._autofit:
133 self.SetClientSize(self._CalcSize())
134 width = self.GetSize().width
135 height = self.GetBestSize().height
136 self.SetBestFittingSize((width, height))
137
138
139 if value:
140 # ensure value is width of the mask of the control:
141 if self._ctrl_constraints._alignRight:
142 value = value.rjust(self._masklength)
143 else:
144 value = value.ljust(self._masklength)
145
146 if self.__readonly:
147 self.SetStringSelection(value)
148 else:
149 self._SetInitialValue(value)
150
151
152 self._SetKeycodeHandler(wx.WXK_UP, self._OnSelectChoice)
153 self._SetKeycodeHandler(wx.WXK_DOWN, self._OnSelectChoice)
154
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
167
168
169
170 def __repr__(self):
171 return "<MaskedComboBox: %s>" % self.GetValue()
172
173
174 def _CalcSize(self, size=None):
175 """
176 Calculate automatic size if allowed; augment base mixin function
177 to account for the selector button.
178 """
179 size = self._calcSize(size)
180 return (size[0]+20, size[1])
181
182
183 def SetFont(self, *args, **kwargs):
184 """ Set the font, then recalculate control size, if appropriate. """
185 wx.ComboBox.SetFont(self, *args, **kwargs)
186 if self._autofit:
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))
193
194
195 def _GetSelection(self):
196 """
197 Allow mixin to get the text selection of this control.
198 REQUIRED by any class derived from MaskedEditMixin.
199 """
200 return self.GetMark()
201
202 def _SetSelection(self, sel_start, sel_to):
203 """
204 Allow mixin to set the text selection of this control.
205 REQUIRED by any class derived from MaskedEditMixin.
206 """
207 return self.SetMark( sel_start, sel_to )
208
209
210 def _GetInsertionPoint(self):
211 return self.GetInsertionPoint()
212
213 def _SetInsertionPoint(self, pos):
214 self.SetInsertionPoint(pos)
215
216
217 def _GetValue(self):
218 """
219 Allow mixin to get the raw value of the control with this function.
220 REQUIRED by any class derived from MaskedEditMixin.
221 """
222 return self.GetValue()
223
224 def _SetValue(self, value):
225 """
226 Allow mixin to set the raw value of the control with this function.
227 REQUIRED by any class derived from MaskedEditMixin.
228 """
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
232 # to match the mask:
233 if self._ctrl_constraints._alignRight:
234 value = value.rjust(self._masklength)
235 else:
236 value = value.ljust(self._masklength)
237
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:
244 self._CheckValid()
245
246 def SetValue(self, value):
247 """
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.
252 """
253 if not self._mask:
254 wx.ComboBox.SetValue(value) # revert to base control behavior
255 return
256 # else...
257 # empty previous contents, replacing entire value:
258 self._SetInsertionPoint(0)
259 self._SetSelection(0, self._masklength)
260
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)
267
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
271 # to match the mask:
272 elif self._ctrl_constraints._alignRight:
273 value = value.rjust(self._masklength)
274 else:
275 value = value.ljust(self._masklength)
276
277
278 # make SetValue behave the same as if you had typed the value in:
279 try:
280 value, replace_to = self._Paste(value, raise_on_invalid=True, just_return_value=True)
281 if self._isFloat:
282 self._isNeg = False # (clear current assumptions)
283 value = self._adjustFloat(value)
284 elif self._isInt:
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)
289 except ValueError:
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)
297 else:
298 raise
299
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)
304
305
306 def _Refresh(self):
307 """
308 Allow mixin to refresh the base control with this function.
309 REQUIRED by any class derived from MaskedEditMixin.
310 """
311 wx.ComboBox.Refresh(self)
312
313 def Refresh(self):
314 """
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.
318 """
319 self._CheckValid()
320 self._Refresh()
321
322
323 def _IsEditable(self):
324 """
325 Allow mixin to determine if the base control is editable with this function.
326 REQUIRED by any class derived from MaskedEditMixin.
327 """
328 return not self.__readonly
329
330
331 def Cut(self):
332 """
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.
337 """
338 if self._mask:
339 self._Cut() # call the mixin's Cut method
340 else:
341 wx.ComboBox.Cut(self) # else revert to base control behavior
342
343
344 def Paste(self):
345 """
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.
350 """
351 if self._mask:
352 self._Paste() # call the mixin's Paste method
353 else:
354 wx.ComboBox.Paste(self) # else revert to base control behavior
355
356
357 def Undo(self):
358 """
359 This function defines the undo operation for the control. (The default
360 undo is 1-deep.)
361 """
362 if self._mask:
363 self._Undo()
364 else:
365 wx.ComboBox.Undo() # else revert to base control behavior
366
367 def Append( self, choice, clientData=None ):
368 """
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.
374 """
375 if self._mask:
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))
380
381 if not self._ctrl_constraints._choices:
382 self._ctrl_constraints._compareChoices = []
383 self._ctrl_constraints._choices = []
384 self._hasList = True
385
386 compareChoice = choice.strip()
387
388 if self._ctrl_constraints._compareNoCase:
389 compareChoice = compareChoice.lower()
390
391 if self._ctrl_constraints._alignRight:
392 choice = choice.rjust(self._masklength)
393 else:
394 choice = choice.ljust(self._masklength)
395 if self._ctrl_constraints._fillChar != ' ':
396 choice = choice.replace(' ', self._fillChar)
397 ## dbg('updated choice:', choice)
398
399
400 self._ctrl_constraints._compareChoices.append(compareChoice)
401 self._ctrl_constraints._choices.append(choice)
402 self._choices = self._ctrl_constraints._choices # (for shorthand)
403
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))
408
409 wx.ComboBox.Append(self, choice, clientData)
410
411
412 def AppendItems( self, choices ):
413 """
414 AppendItems() is handled in terms of Append, to avoid code replication.
415 """
416 for choice in choices:
417 self.Append(choice)
418
419
420 def Clear( self ):
421 """
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.
425 """
426 if self._mask:
427 self._choices = []
428 self._ctrl_constraints._autoCompleteIndex = -1
429 if self._ctrl_constraints._choices:
430 self.SetCtrlParameters(choices=[])
431 wx.ComboBox.Clear(self)
432
433
434 def _OnCtrlParametersChanged(self):
435 """
436 This overrides the mixin's default OnCtrlParametersChanged to detect
437 changes in choice list, so masked.Combobox can update the base control:
438 """
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 )
444
445
446 def GetMark(self):
447 """
448 This function is a hack to make up for the fact that wx.ComboBox has no
449 method for returning the selected portion of its edit control. It
450 works, but has the nasty side effect of generating lots of intermediate
451 events.
452 """
453 ## dbg(suspend=1) # turn off debugging around this function
454 ## dbg('MaskedComboBox::GetMark', indent=1)
455 if self.__readonly:
456 ## dbg(indent=0)
457 return 0, 0 # no selection possible for editing
458 ## sel_start, sel_to = wxComboBox.GetMark(self) # what I'd *like* to have!
459 sel_start = sel_to = self.GetInsertionPoint()
460 ## dbg("current sel_start:", sel_start)
461 value = self.GetValue()
462 ## dbg('value: "%s"' % value)
463
464 self._ignoreChange = True # tell _OnTextChange() to ignore next event (if any)
465
466 wx.ComboBox.Cut(self)
467 newvalue = self.GetValue()
468 ## dbg("value after Cut operation:", newvalue)
469
470 if newvalue != value: # something was selected; calculate extent
471 ## dbg("something selected")
472 sel_to = sel_start + len(value) - len(newvalue)
473 wx.ComboBox.SetValue(self, value) # restore original value and selection (still ignoring change)
474 wx.ComboBox.SetInsertionPoint(self, sel_start)
475 wx.ComboBox.SetMark(self, sel_start, sel_to)
476
477 self._ignoreChange = False # tell _OnTextChange() to pay attn again
478
479 ## dbg('computed selection:', sel_start, sel_to, indent=0, suspend=0)
480 return sel_start, sel_to
481
482
483 def SetSelection(self, index):
484 """
485 Necessary override for bookkeeping on choice selection, to keep current value
486 current.
487 """
488 ## dbg('MaskedComboBox::SetSelection(%d)' % index)
489 if self._mask:
490 self._prevValue = self._curValue
491 self._curValue = self._choices[index]
492 self._ctrl_constraints._autoCompleteIndex = index
493 wx.ComboBox.SetSelection(self, index)
494
495
496 def _OnKeyDownInComboBox(self, event):
497 """
498 This function is necessary because navigation and control key
499 events do not seem to normally be seen by the wxComboBox's
500 EVT_CHAR routine. (Tabs don't seem to be visible no matter
501 what... {:-( )
502 """
503 if event.GetKeyCode() in self._nav + self._control:
504 self._OnChar(event)
505 return
506 else:
507 event.Skip() # let mixin default KeyDown behavior occur
508
509
510 def _OnSelectChoice(self, event):
511 """
512 This function appears to be necessary, because the processing done
513 on the text of the control somehow interferes with the combobox's
514 selection mechanism for the arrow keys.
515 """
516 ## dbg('MaskedComboBox::OnSelectChoice', indent=1)
517
518 if not self._mask:
519 event.Skip()
520 return
521
522 value = self.GetValue().strip()
523
524 if self._ctrl_constraints._compareNoCase:
525 value = value.lower()
526
527 if event.GetKeyCode() == wx.WXK_UP:
528 direction = -1
529 else:
530 direction = 1
531 match_index, partial_match = self._autoComplete(
532 direction,
533 self._ctrl_constraints._compareChoices,
534 value,
535 self._ctrl_constraints._compareNoCase,
536 current_index = self._ctrl_constraints._autoCompleteIndex)
537 if match_index is not None:
538 ## dbg('setting selection to', match_index)
539 # issue appropriate event to outside:
540 self._OnAutoSelect(self._ctrl_constraints, match_index=match_index)
541 self._CheckValid()
542 keep_processing = False
543 else:
544 pos = self._adjustPos(self._GetInsertionPoint(), event.GetKeyCode())
545 field = self._FindField(pos)
546 if self.IsEmpty() or not field._hasList:
547 ## dbg('selecting 1st value in list')
548 self._OnAutoSelect(self._ctrl_constraints, match_index=0)
549 self._CheckValid()
550 keep_processing = False
551 else:
552 # attempt field-level auto-complete
553 ## dbg(indent=0)
554 keep_processing = self._OnAutoCompleteField(event)
555 ## dbg('keep processing?', keep_processing, indent=0)
556 return keep_processing
557
558
559 def _OnAutoSelect(self, field, match_index):
560 """
561 Override mixin (empty) autocomplete handler, so that autocompletion causes
562 combobox to update appropriately.
563 """
564 ## dbg('MaskedComboBox::OnAutoSelect', field._index, indent=1)
565 ## field._autoCompleteIndex = match_index
566 if field == self._ctrl_constraints:
567 self.SetSelection(match_index)
568 ## dbg('issuing combo selection event')
569 self.GetEventHandler().ProcessEvent(
570 MaskedComboBoxSelectEvent( self.GetId(), match_index, self ) )
571 self._CheckValid()
572 ## dbg('field._autoCompleteIndex:', match_index)
573 ## dbg('self.GetSelection():', self.GetSelection())
574 end = self._goEnd(getPosOnly=True)
575 ## dbg('scheduling set of end position to:', end)
576 # work around bug in wx 2.5
577 wx.CallAfter(self.SetInsertionPoint, 0)
578 wx.CallAfter(self.SetInsertionPoint, end)
579 ## dbg(indent=0)
580
581
582 def _OnReturn(self, event):
583 """
584 For wx.ComboBox, it seems that if you hit return when the dropdown is
585 dropped, the event that dismisses the dropdown will also blank the
586 control, because of the implementation of wxComboBox. So this function
587 examines the selection and if it is -1, and the value according to
588 (the base control!) is a value in the list, then it schedules a
589 programmatic wxComboBox.SetSelection() call to pick the appropriate
590 item in the list. (and then does the usual OnReturn bit.)
591 """
592 ## dbg('MaskedComboBox::OnReturn', indent=1)
593 ## dbg('current value: "%s"' % self.GetValue(), 'current index:', self.GetSelection())
594 if self.GetSelection() == -1 and self.GetValue().lower().strip() in self._ctrl_constraints._compareChoices:
595 wx.CallAfter(self.SetSelection, self._ctrl_constraints._autoCompleteIndex)
596
597 event.m_keyCode = wx.WXK_TAB
598 event.Skip()
599 ## dbg(indent=0)
600
601
602 class ComboBox( BaseMaskedComboBox, MaskedEditAccessorsMixin ):
603 """
604 The "user-visible" masked combobox control, this class is
605 identical to the BaseMaskedComboBox class it's derived from.
606 (This extra level of inheritance allows us to add the generic
607 set of masked edit parameters only to this class while allowing
608 other classes to derive from the "base" masked combobox control,
609 and provide a smaller set of valid accessor functions.)
610 See BaseMaskedComboBox for available methods.
611 """
612 pass
613
614
615 class PreMaskedComboBox( BaseMaskedComboBox, MaskedEditAccessorsMixin ):
616 """
617 This class exists to support the use of XRC subclassing.
618 """
619 # This should really be wx.EVT_WINDOW_CREATE but it is not
620 # currently delivered for native controls on all platforms, so
621 # we'll use EVT_SIZE instead. It should happen shortly after the
622 # control is created as the control is set to its "best" size.
623 _firstEventType = wx.EVT_SIZE
624
625 def __init__(self):
626 pre = wx.PreComboBox()
627 self.PostCreate(pre)
628 self.Bind(self._firstEventType, self.OnCreate)
629
630
631 def OnCreate(self, evt):
632 self.Unbind(self._firstEventType)
633 self._PostInit()
634
635 __i = 0
636 ## CHANGELOG:
637 ## ====================
638 ## Version 1.2
639 ## 1. Converted docstrings to reST format, added doc for ePyDoc.
640 ## 2. Renamed helper functions, vars etc. not intended to be visible in public
641 ## interface to code.
642 ##
643 ## Version 1.1
644 ## 1. Added .SetFont() method that properly resizes control
645 ## 2. Modified control to support construction via XRC mechanism.
646 ## 3. Added AppendItems() to conform with latest combobox.