Patches 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 import wx
17 from wx.lib.masked import *
18
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
22 dbg = Logger()
23 ##dbg(enable=0)
24
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)
31
32 self.__selection = selection
33 self.SetEventObject(object)
34
35 def GetSelection(self):
36 """Retrieve the value of the control at the time
37 this event was generated."""
38 return self.__selection
39
40
41 class BaseMaskedComboBox( wx.ComboBox, MaskedEditMixin ):
42 """
43 This masked edit control adds the ability to use a masked input
44 on a combobox, and do auto-complete of such values.
45 """
46 def __init__( self, parent, id=-1, value = '',
47 pos = wx.DefaultPosition,
48 size = wx.DefaultSize,
49 choices = [],
50 style = wx.CB_DROPDOWN,
51 validator = wx.DefaultValidator,
52 name = "maskedComboBox",
53 setupEventHandling = True, ## setup event handling by default):
54 **kwargs):
55
56
57 kwargs['choices'] = choices ## set up maskededit to work with choice list too
58
59 ## Since combobox completion is case-insensitive, always validate same way
60 if not kwargs.has_key('compareNoCase'):
61 kwargs['compareNoCase'] = True
62
63 MaskedEditMixin.__init__( self, name, **kwargs )
64
65 self._choices = self._ctrl_constraints._choices
66 ## dbg('self._choices:', self._choices)
67
68 if self._ctrl_constraints._alignRight:
69 choices = [choice.rjust(self._masklength) for choice in choices]
70 else:
71 choices = [choice.ljust(self._masklength) for choice in choices]
72
73 wx.ComboBox.__init__(self, parent, id, value='',
74 pos=pos, size = size,
75 choices=choices, style=style|wx.WANTS_CHARS,
76 validator=validator,
77 name=name)
78 self.controlInitialized = True
79
80 self._PostInit(style=style, setupEventHandling=setupEventHandling,
81 name=name, value=value, **kwargs)
82
83
84 def _PostInit(self, style=wx.CB_DROPDOWN,
85 setupEventHandling = True, ## setup event handling by default):
86 name = "maskedComboBox", value='', **kwargs):
87
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
92
93 if not hasattr(self, 'controlInitialized'):
94
95 self.controlInitialized = True ## must have been called via XRC, therefore base class is constructed
96 if not kwargs.has_key('choices'):
97 choices=[]
98 kwargs['choices'] = choices ## set up maskededit to work with choice list too
99 self._choices = []
100
101 ## Since combobox completion is case-insensitive, always validate same way
102 if not kwargs.has_key('compareNoCase'):
103 kwargs['compareNoCase'] = True
104
105 MaskedEditMixin.__init__( self, name, **kwargs )
106
107 self._choices = self._ctrl_constraints._choices
108 ## dbg('self._choices:', self._choices)
109
110 if self._ctrl_constraints._alignRight:
111 choices = [choice.rjust(self._masklength) for choice in choices]
112 else:
113 choices = [choice.ljust(self._masklength) for choice in choices]
114 wx.ComboBox.Clear(self)
115 wx.ComboBox.AppendItems(self, choices)
116
117
118 # Set control font - fixed width by default
119 self._setFont()
120
121 if self._autofit:
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))
127
128
129 if value:
130 # ensure value is width of the mask of the control:
131 if self._ctrl_constraints._alignRight:
132 value = value.rjust(self._masklength)
133 else:
134 value = value.ljust(self._masklength)
135
136 if self.__readonly:
137 self.SetStringSelection(value)
138 else:
139 self._SetInitialValue(value)
140
141
142 self._SetKeycodeHandler(wx.WXK_UP, self.OnSelectChoice)
143 self._SetKeycodeHandler(wx.WXK_DOWN, self.OnSelectChoice)
144
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
157
158
159
160 def __repr__(self):
161 return "<MaskedComboBox: %s>" % self.GetValue()
162
163
164 def _CalcSize(self, size=None):
165 """
166 Calculate automatic size if allowed; augment base mixin function
167 to account for the selector button.
168 """
169 size = self._calcSize(size)
170 return (size[0]+20, size[1])
171
172
173 def SetFont(self, *args, **kwargs):
174 """ Set the font, then recalculate control size, if appropriate. """
175 wx.ComboBox.SetFont(self, *args, **kwargs)
176 if self._autofit:
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))
184
185
186 def _GetSelection(self):
187 """
188 Allow mixin to get the text selection of this control.
189 REQUIRED by any class derived from MaskedEditMixin.
190 """
191 return self.GetMark()
192
193 def _SetSelection(self, sel_start, sel_to):
194 """
195 Allow mixin to set the text selection of this control.
196 REQUIRED by any class derived from MaskedEditMixin.
197 """
198 return self.SetMark( sel_start, sel_to )
199
200
201 def _GetInsertionPoint(self):
202 return self.GetInsertionPoint()
203
204 def _SetInsertionPoint(self, pos):
205 self.SetInsertionPoint(pos)
206
207
208 def _GetValue(self):
209 """
210 Allow mixin to get the raw value of the control with this function.
211 REQUIRED by any class derived from MaskedEditMixin.
212 """
213 return self.GetValue()
214
215 def _SetValue(self, value):
216 """
217 Allow mixin to set the raw value of the control with this function.
218 REQUIRED by any class derived from MaskedEditMixin.
219 """
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
223 # to match the mask:
224 if self._ctrl_constraints._alignRight:
225 value = value.rjust(self._masklength)
226 else:
227 value = value.ljust(self._masklength)
228
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:
235 self._CheckValid()
236
237 def SetValue(self, value):
238 """
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.
243 """
244 if not self._mask:
245 wx.ComboBox.SetValue(value) # revert to base control behavior
246 return
247 # else...
248 # empty previous contents, replacing entire value:
249 self._SetInsertionPoint(0)
250 self._SetSelection(0, self._masklength)
251
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)
258
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
262 # to match the mask:
263 elif self._ctrl_constraints._alignRight:
264 value = value.rjust(self._masklength)
265 else:
266 value = value.ljust(self._masklength)
267
268
269 # make SetValue behave the same as if you had typed the value in:
270 try:
271 value, replace_to = self._Paste(value, raise_on_invalid=True, just_return_value=True)
272 if self._isFloat:
273 self._isNeg = False # (clear current assumptions)
274 value = self._adjustFloat(value)
275 elif self._isInt:
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)
280 except ValueError:
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)
288 else:
289 raise
290
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)
295
296
297 def _Refresh(self):
298 """
299 Allow mixin to refresh the base control with this function.
300 REQUIRED by any class derived from MaskedEditMixin.
301 """
302 wx.ComboBox.Refresh(self)
303
304 def Refresh(self):
305 """
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.
309 """
310 self._CheckValid()
311 self._Refresh()
312
313
314 def _IsEditable(self):
315 """
316 Allow mixin to determine if the base control is editable with this function.
317 REQUIRED by any class derived from MaskedEditMixin.
318 """
319 return not self.__readonly
320
321
322 def Cut(self):
323 """
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.
328 """
329 if self._mask:
330 self._Cut() # call the mixin's Cut method
331 else:
332 wx.ComboBox.Cut(self) # else revert to base control behavior
333
334
335 def Paste(self):
336 """
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.
341 """
342 if self._mask:
343 self._Paste() # call the mixin's Paste method
344 else:
345 wx.ComboBox.Paste(self) # else revert to base control behavior
346
347
348 def Undo(self):
349 """
350 This function defines the undo operation for the control. (The default
351 undo is 1-deep.)
352 """
353 if self._mask:
354 self._Undo()
355 else:
356 wx.ComboBox.Undo() # else revert to base control behavior
357
358 def Append( self, choice, clientData=None ):
359 """
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.
365 """
366 if self._mask:
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))
371
372 if not self._ctrl_constraints._choices:
373 self._ctrl_constraints._compareChoices = []
374 self._ctrl_constraints._choices = []
375 self._hasList = True
376
377 compareChoice = choice.strip()
378
379 if self._ctrl_constraints._compareNoCase:
380 compareChoice = compareChoice.lower()
381
382 if self._ctrl_constraints._alignRight:
383 choice = choice.rjust(self._masklength)
384 else:
385 choice = choice.ljust(self._masklength)
386 if self._ctrl_constraints._fillChar != ' ':
387 choice = choice.replace(' ', self._fillChar)
388 ## dbg('updated choice:', choice)
389
390
391 self._ctrl_constraints._compareChoices.append(compareChoice)
392 self._ctrl_constraints._choices.append(choice)
393 self._choices = self._ctrl_constraints._choices # (for shorthand)
394
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))
399
400 wx.ComboBox.Append(self, choice, clientData)
401
402
403 def AppendItems( self, choices ):
404 """
405 AppendItems() is handled in terms of Append, to avoid code replication.
406 """
407 for choice in choices:
408 self.Append(choice)
409
410
411 def Clear( self ):
412 """
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.
415 """
416 if self._mask:
417 self._choices = []
418 self._ctrl_constraints._autoCompleteIndex = -1
419 if self._ctrl_constraints._choices:
420 self.SetCtrlParameters(choices=[])
421 wx.ComboBox.Clear(self)
422
423
424 def _OnCtrlParametersChanged(self):
425 """
426 Override mixin's default OnCtrlParametersChanged to detect changes in choice list, so
427 we can update the base control:
428 """
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 )
434
435
436 def GetMark(self):
437 """
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
441 events.
442 """
443 ## dbg(suspend=1) # turn off debugging around this function
444 ## dbg('MaskedComboBox::GetMark', indent=1)
445 if self.__readonly:
446 ## dbg(indent=0)
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)
453
454 self._ignoreChange = True # tell _OnTextChange() to ignore next event (if any)
455
456 wx.ComboBox.Cut(self)
457 newvalue = self.GetValue()
458 ## dbg("value after Cut operation:", newvalue)
459
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)
466
467 self._ignoreChange = False # tell _OnTextChange() to pay attn again
468
469 ## dbg('computed selection:', sel_start, sel_to, indent=0, suspend=0)
470 return sel_start, sel_to
471
472
473 def SetSelection(self, index):
474 """
475 Necessary for bookkeeping on choice selection, to keep current value
476 current.
477 """
478 ## dbg('MaskedComboBox::SetSelection(%d)' % index)
479 if self._mask:
480 self._prevValue = self._curValue
481 self._curValue = self._choices[index]
482 self._ctrl_constraints._autoCompleteIndex = index
483 wx.ComboBox.SetSelection(self, index)
484
485
486 def OnKeyDown(self, event):
487 """
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
491 what... {:-( )
492 """
493 if event.GetKeyCode() in self._nav + self._control:
494 self._OnChar(event)
495 return
496 else:
497 event.Skip() # let mixin default KeyDown behavior occur
498
499
500 def OnSelectChoice(self, event):
501 """
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.
505 """
506 ## dbg('MaskedComboBox::OnSelectChoice', indent=1)
507
508 if not self._mask:
509 event.Skip()
510 return
511
512 value = self.GetValue().strip()
513
514 if self._ctrl_constraints._compareNoCase:
515 value = value.lower()
516
517 if event.GetKeyCode() == wx.WXK_UP:
518 direction = -1
519 else:
520 direction = 1
521 match_index, partial_match = self._autoComplete(
522 direction,
523 self._ctrl_constraints._compareChoices,
524 value,
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)
531 self._CheckValid()
532 keep_processing = False
533 else:
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)
539 self._CheckValid()
540 keep_processing = False
541 else:
542 # attempt field-level auto-complete
543 ## dbg(indent=0)
544 keep_processing = self._OnAutoCompleteField(event)
545 ## dbg('keep processing?', keep_processing, indent=0)
546 return keep_processing
547
548
549 def _OnAutoSelect(self, field, match_index):
550 """
551 Override mixin (empty) autocomplete handler, so that autocompletion causes
552 combobox to update appropriately.
553 """
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 ) )
561 self._CheckValid()
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)
569 ## dbg(indent=0)
570
571
572 def _OnReturn(self, event):
573 """
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.)
581 """
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)
586
587 event.m_keyCode = wx.WXK_TAB
588 event.Skip()
589 ## dbg(indent=0)
590
591
592 class ComboBox( BaseMaskedComboBox, MaskedEditAccessorsMixin ):
593 """
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.
598 """
599 pass
600
601
602 class PreMaskedComboBox( BaseMaskedComboBox, MaskedEditAccessorsMixin ):
603 """
604 This allows us to use XRC subclassing.
605 """
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
611
612 def __init__(self):
613 pre = wx.PreComboBox()
614 self.PostCreate(pre)
615 self.Bind(self._firstEventType, self.OnCreate)
616
617
618 def OnCreate(self, evt):
619 self.Unbind(self._firstEventType)
620 self._PostInit()
621
622 i=0
623 ## CHANGELOG:
624 ## ====================
625 ## Version 1.1
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.