Added new MaskedEditControl code from Will Sadkin. The modules are
[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 # This is necessary, because wxComboBox currently provides no
58 # method for determining later if this was specified in the
59 # constructor for the control...
60 self.__readonly = style & wx.CB_READONLY == wx.CB_READONLY
61
62 kwargs['choices'] = choices ## set up maskededit to work with choice list too
63
64 ## Since combobox completion is case-insensitive, always validate same way
65 if not kwargs.has_key('compareNoCase'):
66 kwargs['compareNoCase'] = True
67
68 MaskedEditMixin.__init__( self, name, **kwargs )
69
70 self._choices = self._ctrl_constraints._choices
71 ## dbg('self._choices:', self._choices)
72
73 if self._ctrl_constraints._alignRight:
74 choices = [choice.rjust(self._masklength) for choice in choices]
75 else:
76 choices = [choice.ljust(self._masklength) for choice in choices]
77
78 wx.ComboBox.__init__(self, parent, id, value='',
79 pos=pos, size = size,
80 choices=choices, style=style|wx.WANTS_CHARS,
81 validator=validator,
82 name=name)
83
84 self.controlInitialized = True
85
86 # Set control font - fixed width by default
87 self._setFont()
88
89 if self._autofit:
90 self.SetClientSize(self._CalcSize())
91
92 if value:
93 # ensure value is width of the mask of the control:
94 if self._ctrl_constraints._alignRight:
95 value = value.rjust(self._masklength)
96 else:
97 value = value.ljust(self._masklength)
98
99 if self.__readonly:
100 self.SetStringSelection(value)
101 else:
102 self._SetInitialValue(value)
103
104
105 self._SetKeycodeHandler(wx.WXK_UP, self.OnSelectChoice)
106 self._SetKeycodeHandler(wx.WXK_DOWN, self.OnSelectChoice)
107
108 if setupEventHandling:
109 ## Setup event handlers
110 self.Bind(wx.EVT_SET_FOCUS, self._OnFocus ) ## defeat automatic full selection
111 self.Bind(wx.EVT_KILL_FOCUS, self._OnKillFocus ) ## run internal validator
112 self.Bind(wx.EVT_LEFT_DCLICK, self._OnDoubleClick) ## select field under cursor on dclick
113 self.Bind(wx.EVT_RIGHT_UP, self._OnContextMenu ) ## bring up an appropriate context menu
114 self.Bind(wx.EVT_CHAR, self._OnChar ) ## handle each keypress
115 self.Bind(wx.EVT_KEY_DOWN, self.OnKeyDown ) ## for special processing of up/down keys
116 self.Bind(wx.EVT_KEY_DOWN, self._OnKeyDown ) ## for processing the rest of the control keys
117 ## (next in evt chain)
118 self.Bind(wx.EVT_TEXT, self._OnTextChange ) ## color control appropriately & keep
119 ## track of previous value for undo
120
121
122
123 def __repr__(self):
124 return "<MaskedComboBox: %s>" % self.GetValue()
125
126
127 def _CalcSize(self, size=None):
128 """
129 Calculate automatic size if allowed; augment base mixin function
130 to account for the selector button.
131 """
132 size = self._calcSize(size)
133 return (size[0]+20, size[1])
134
135
136 def _GetSelection(self):
137 """
138 Allow mixin to get the text selection of this control.
139 REQUIRED by any class derived from MaskedEditMixin.
140 """
141 return self.GetMark()
142
143 def _SetSelection(self, sel_start, sel_to):
144 """
145 Allow mixin to set the text selection of this control.
146 REQUIRED by any class derived from MaskedEditMixin.
147 """
148 return self.SetMark( sel_start, sel_to )
149
150
151 def _GetInsertionPoint(self):
152 return self.GetInsertionPoint()
153
154 def _SetInsertionPoint(self, pos):
155 self.SetInsertionPoint(pos)
156
157
158 def _GetValue(self):
159 """
160 Allow mixin to get the raw value of the control with this function.
161 REQUIRED by any class derived from MaskedEditMixin.
162 """
163 return self.GetValue()
164
165 def _SetValue(self, value):
166 """
167 Allow mixin to set the raw value of the control with this function.
168 REQUIRED by any class derived from MaskedEditMixin.
169 """
170 # For wxComboBox, ensure that values are properly padded so that
171 # if varying length choices are supplied, they always show up
172 # in the window properly, and will be the appropriate length
173 # to match the mask:
174 if self._ctrl_constraints._alignRight:
175 value = value.rjust(self._masklength)
176 else:
177 value = value.ljust(self._masklength)
178
179 # Record current selection and insertion point, for undo
180 self._prevSelection = self._GetSelection()
181 self._prevInsertionPoint = self._GetInsertionPoint()
182 wx.ComboBox.SetValue(self, value)
183 # text change events don't always fire, so we check validity here
184 # to make certain formatting is applied:
185 self._CheckValid()
186
187 def SetValue(self, value):
188 """
189 This function redefines the externally accessible .SetValue to be
190 a smart "paste" of the text in question, so as not to corrupt the
191 masked control. NOTE: this must be done in the class derived
192 from the base wx control.
193 """
194 if not self._mask:
195 wx.ComboBox.SetValue(value) # revert to base control behavior
196 return
197 # else...
198 # empty previous contents, replacing entire value:
199 self._SetInsertionPoint(0)
200 self._SetSelection(0, self._masklength)
201
202 if( len(value) < self._masklength # value shorter than control
203 and (self._isFloat or self._isInt) # and it's a numeric control
204 and self._ctrl_constraints._alignRight ): # and it's a right-aligned control
205 # try to intelligently "pad out" the value to the right size:
206 value = self._template[0:self._masklength - len(value)] + value
207 ## dbg('padded value = "%s"' % value)
208
209 # For wxComboBox, ensure that values are properly padded so that
210 # if varying length choices are supplied, they always show up
211 # in the window properly, and will be the appropriate length
212 # to match the mask:
213 elif self._ctrl_constraints._alignRight:
214 value = value.rjust(self._masklength)
215 else:
216 value = value.ljust(self._masklength)
217
218
219 # make SetValue behave the same as if you had typed the value in:
220 try:
221 value = self._Paste(value, raise_on_invalid=True, just_return_value=True)
222 if self._isFloat:
223 self._isNeg = False # (clear current assumptions)
224 value = self._adjustFloat(value)
225 elif self._isInt:
226 self._isNeg = False # (clear current assumptions)
227 value = self._adjustInt(value)
228 elif self._isDate and not self.IsValid(value) and self._4digityear:
229 value = self._adjustDate(value, fixcentury=True)
230 except ValueError:
231 # If date, year might be 2 digits vs. 4; try adjusting it:
232 if self._isDate and self._4digityear:
233 dateparts = value.split(' ')
234 dateparts[0] = self._adjustDate(dateparts[0], fixcentury=True)
235 value = string.join(dateparts, ' ')
236 ## dbg('adjusted value: "%s"' % value)
237 value = self._Paste(value, raise_on_invalid=True, just_return_value=True)
238 else:
239 raise
240
241 self._SetValue(value)
242 #### dbg('queuing insertion after .SetValue', self._masklength)
243 wx.CallAfter(self._SetInsertionPoint, self._masklength)
244 wx.CallAfter(self._SetSelection, self._masklength, self._masklength)
245
246
247 def _Refresh(self):
248 """
249 Allow mixin to refresh the base control with this function.
250 REQUIRED by any class derived from MaskedEditMixin.
251 """
252 wx.ComboBox.Refresh(self)
253
254 def Refresh(self):
255 """
256 This function redefines the externally accessible .Refresh() to
257 validate the contents of the masked control as it refreshes.
258 NOTE: this must be done in the class derived from the base wx control.
259 """
260 self._CheckValid()
261 self._Refresh()
262
263
264 def _IsEditable(self):
265 """
266 Allow mixin to determine if the base control is editable with this function.
267 REQUIRED by any class derived from MaskedEditMixin.
268 """
269 return not self.__readonly
270
271
272 def Cut(self):
273 """
274 This function redefines the externally accessible .Cut to be
275 a smart "erase" of the text in question, so as not to corrupt the
276 masked control. NOTE: this must be done in the class derived
277 from the base wx control.
278 """
279 if self._mask:
280 self._Cut() # call the mixin's Cut method
281 else:
282 wx.ComboBox.Cut(self) # else revert to base control behavior
283
284
285 def Paste(self):
286 """
287 This function redefines the externally accessible .Paste to be
288 a smart "paste" of the text in question, so as not to corrupt the
289 masked control. NOTE: this must be done in the class derived
290 from the base wx control.
291 """
292 if self._mask:
293 self._Paste() # call the mixin's Paste method
294 else:
295 wx.ComboBox.Paste(self) # else revert to base control behavior
296
297
298 def Undo(self):
299 """
300 This function defines the undo operation for the control. (The default
301 undo is 1-deep.)
302 """
303 if self._mask:
304 self._Undo()
305 else:
306 wx.ComboBox.Undo() # else revert to base control behavior
307
308
309 def Append( self, choice, clientData=None ):
310 """
311 This function override is necessary so we can keep track of any additions to the list
312 of choices, because wxComboBox doesn't have an accessor for the choice list.
313 The code here is the same as in the SetParameters() mixin function, but is
314 done for the individual value as appended, so the list can be built incrementally
315 without speed penalty.
316 """
317 if self._mask:
318 if type(choice) not in (types.StringType, types.UnicodeType):
319 raise TypeError('%s: choices must be a sequence of strings' % str(self._index))
320 elif not self.IsValid(choice):
321 raise ValueError('%s: "%s" is not a valid value for the control as specified.' % (str(self._index), choice))
322
323 if not self._ctrl_constraints._choices:
324 self._ctrl_constraints._compareChoices = []
325 self._ctrl_constraints._choices = []
326 self._hasList = True
327
328 compareChoice = choice.strip()
329
330 if self._ctrl_constraints._compareNoCase:
331 compareChoice = compareChoice.lower()
332
333 if self._ctrl_constraints._alignRight:
334 choice = choice.rjust(self._masklength)
335 else:
336 choice = choice.ljust(self._masklength)
337 if self._ctrl_constraints._fillChar != ' ':
338 choice = choice.replace(' ', self._fillChar)
339 ## dbg('updated choice:', choice)
340
341
342 self._ctrl_constraints._compareChoices.append(compareChoice)
343 self._ctrl_constraints._choices.append(choice)
344 self._choices = self._ctrl_constraints._choices # (for shorthand)
345
346 if( not self.IsValid(choice) and
347 (not self._ctrl_constraints.IsEmpty(choice) or
348 (self._ctrl_constraints.IsEmpty(choice) and self._ctrl_constraints._validRequired) ) ):
349 raise ValueError('"%s" is not a valid value for the control "%s" as specified.' % (choice, self.name))
350
351 wx.ComboBox.Append(self, choice, clientData)
352
353
354
355 def Clear( self ):
356 """
357 This function override is necessary so we can keep track of any additions to the list
358 of choices, because wxComboBox doesn't have an accessor for the choice list.
359 """
360 if self._mask:
361 self._choices = []
362 self._ctrl_constraints._autoCompleteIndex = -1
363 if self._ctrl_constraints._choices:
364 self.SetCtrlParameters(choices=[])
365 wx.ComboBox.Clear(self)
366
367
368 def _OnCtrlParametersChanged(self):
369 """
370 Override mixin's default OnCtrlParametersChanged to detect changes in choice list, so
371 we can update the base control:
372 """
373 if self.controlInitialized and self._choices != self._ctrl_constraints._choices:
374 wx.ComboBox.Clear(self)
375 self._choices = self._ctrl_constraints._choices
376 for choice in self._choices:
377 wx.ComboBox.Append( self, choice )
378
379
380 def GetMark(self):
381 """
382 This function is a hack to make up for the fact that wxComboBox has no
383 method for returning the selected portion of its edit control. It
384 works, but has the nasty side effect of generating lots of intermediate
385 events.
386 """
387 ## dbg(suspend=1) # turn off debugging around this function
388 ## dbg('MaskedComboBox::GetMark', indent=1)
389 if self.__readonly:
390 ## dbg(indent=0)
391 return 0, 0 # no selection possible for editing
392 ## sel_start, sel_to = wxComboBox.GetMark(self) # what I'd *like* to have!
393 sel_start = sel_to = self.GetInsertionPoint()
394 ## dbg("current sel_start:", sel_start)
395 value = self.GetValue()
396 ## dbg('value: "%s"' % value)
397
398 self._ignoreChange = True # tell _OnTextChange() to ignore next event (if any)
399
400 wx.ComboBox.Cut(self)
401 newvalue = self.GetValue()
402 ## dbg("value after Cut operation:", newvalue)
403
404 if newvalue != value: # something was selected; calculate extent
405 ## dbg("something selected")
406 sel_to = sel_start + len(value) - len(newvalue)
407 wx.ComboBox.SetValue(self, value) # restore original value and selection (still ignoring change)
408 wx.ComboBox.SetInsertionPoint(self, sel_start)
409 wx.ComboBox.SetMark(self, sel_start, sel_to)
410
411 self._ignoreChange = False # tell _OnTextChange() to pay attn again
412
413 ## dbg('computed selection:', sel_start, sel_to, indent=0, suspend=0)
414 return sel_start, sel_to
415
416
417 def SetSelection(self, index):
418 """
419 Necessary for bookkeeping on choice selection, to keep current value
420 current.
421 """
422 ## dbg('MaskedComboBox::SetSelection(%d)' % index)
423 if self._mask:
424 self._prevValue = self._curValue
425 self._curValue = self._choices[index]
426 self._ctrl_constraints._autoCompleteIndex = index
427 wx.ComboBox.SetSelection(self, index)
428
429
430 def OnKeyDown(self, event):
431 """
432 This function is necessary because navigation and control key
433 events do not seem to normally be seen by the wxComboBox's
434 EVT_CHAR routine. (Tabs don't seem to be visible no matter
435 what... {:-( )
436 """
437 if event.GetKeyCode() in self._nav + self._control:
438 self._OnChar(event)
439 return
440 else:
441 event.Skip() # let mixin default KeyDown behavior occur
442
443
444 def OnSelectChoice(self, event):
445 """
446 This function appears to be necessary, because the processing done
447 on the text of the control somehow interferes with the combobox's
448 selection mechanism for the arrow keys.
449 """
450 ## dbg('MaskedComboBox::OnSelectChoice', indent=1)
451
452 if not self._mask:
453 event.Skip()
454 return
455
456 value = self.GetValue().strip()
457
458 if self._ctrl_constraints._compareNoCase:
459 value = value.lower()
460
461 if event.GetKeyCode() == wx.WXK_UP:
462 direction = -1
463 else:
464 direction = 1
465 match_index, partial_match = self._autoComplete(
466 direction,
467 self._ctrl_constraints._compareChoices,
468 value,
469 self._ctrl_constraints._compareNoCase,
470 current_index = self._ctrl_constraints._autoCompleteIndex)
471 if match_index is not None:
472 ## dbg('setting selection to', match_index)
473 # issue appropriate event to outside:
474 self._OnAutoSelect(self._ctrl_constraints, match_index=match_index)
475 self._CheckValid()
476 keep_processing = False
477 else:
478 pos = self._adjustPos(self._GetInsertionPoint(), event.GetKeyCode())
479 field = self._FindField(pos)
480 if self.IsEmpty() or not field._hasList:
481 ## dbg('selecting 1st value in list')
482 self._OnAutoSelect(self._ctrl_constraints, match_index=0)
483 self._CheckValid()
484 keep_processing = False
485 else:
486 # attempt field-level auto-complete
487 ## dbg(indent=0)
488 keep_processing = self._OnAutoCompleteField(event)
489 ## dbg('keep processing?', keep_processing, indent=0)
490 return keep_processing
491
492
493 def _OnAutoSelect(self, field, match_index):
494 """
495 Override mixin (empty) autocomplete handler, so that autocompletion causes
496 combobox to update appropriately.
497 """
498 ## dbg('MaskedComboBox::OnAutoSelect', field._index, indent=1)
499 ## field._autoCompleteIndex = match_index
500 if field == self._ctrl_constraints:
501 self.SetSelection(match_index)
502 ## dbg('issuing combo selection event')
503 self.GetEventHandler().ProcessEvent(
504 MaskedComboBoxSelectEvent( self.GetId(), match_index, self ) )
505 self._CheckValid()
506 ## dbg('field._autoCompleteIndex:', match_index)
507 ## dbg('self.GetSelection():', self.GetSelection())
508 ## dbg(indent=0)
509
510
511 def _OnReturn(self, event):
512 """
513 For wxComboBox, it seems that if you hit return when the dropdown is
514 dropped, the event that dismisses the dropdown will also blank the
515 control, because of the implementation of wxComboBox. So here,
516 we look and if the selection is -1, and the value according to
517 (the base control!) is a value in the list, then we schedule a
518 programmatic wxComboBox.SetSelection() call to pick the appropriate
519 item in the list. (and then do the usual OnReturn bit.)
520 """
521 ## dbg('MaskedComboBox::OnReturn', indent=1)
522 ## dbg('current value: "%s"' % self.GetValue(), 'current index:', self.GetSelection())
523 if self.GetSelection() == -1 and self.GetValue().lower().strip() in self._ctrl_constraints._compareChoices:
524 wx.CallAfter(self.SetSelection, self._ctrl_constraints._autoCompleteIndex)
525
526 event.m_keyCode = wx.WXK_TAB
527 event.Skip()
528 ## dbg(indent=0)
529
530
531 class ComboBox( BaseMaskedComboBox, MaskedEditAccessorsMixin ):
532 """
533 This extra level of inheritance allows us to add the generic set of
534 masked edit parameters only to this class while allowing other
535 classes to derive from the "base" masked combobox control, and provide
536 a smaller set of valid accessor functions.
537 """
538 pass
539
540