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