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