Use GetMark if available
[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 # Not all wx platform implementations have .GetMark, so we make the following test,
447 # and fall back to our old hack if they don't...
448 #
449 if not hasattr(wx.ComboBox, 'GetMark'):
450 def GetMark(self):
451 """
452 This function is a hack to make up for the fact that wx.ComboBox has no
453 method for returning the selected portion of its edit control. It
454 works, but has the nasty side effect of generating lots of intermediate
455 events.
456 """
457 ## dbg(suspend=1) # turn off debugging around this function
458 ## dbg('MaskedComboBox::GetMark', indent=1)
459 if self.__readonly:
460 ## dbg(indent=0)
461 return 0, 0 # no selection possible for editing
462 ## sel_start, sel_to = wxComboBox.GetMark(self) # what I'd *like* to have!
463 sel_start = sel_to = self.GetInsertionPoint()
464 ## dbg("current sel_start:", sel_start)
465 value = self.GetValue()
466 ## dbg('value: "%s"' % value)
467
468 self._ignoreChange = True # tell _OnTextChange() to ignore next event (if any)
469
470 wx.ComboBox.Cut(self)
471 newvalue = self.GetValue()
472 ## dbg("value after Cut operation:", newvalue)
473
474 if newvalue != value: # something was selected; calculate extent
475 ## dbg("something selected")
476 sel_to = sel_start + len(value) - len(newvalue)
477 wx.ComboBox.SetValue(self, value) # restore original value and selection (still ignoring change)
478 wx.ComboBox.SetInsertionPoint(self, sel_start)
479 wx.ComboBox.SetMark(self, sel_start, sel_to)
480
481 self._ignoreChange = False # tell _OnTextChange() to pay attn again
482
483 ## dbg('computed selection:', sel_start, sel_to, indent=0, suspend=0)
484 return sel_start, sel_to
485
486
487 def SetSelection(self, index):
488 """
489 Necessary override for bookkeeping on choice selection, to keep current value
490 current.
491 """
492 ## dbg('MaskedComboBox::SetSelection(%d)' % index)
493 if self._mask:
494 self._prevValue = self._curValue
495 self._curValue = self._choices[index]
496 self._ctrl_constraints._autoCompleteIndex = index
497 wx.ComboBox.SetSelection(self, index)
498
499
500 def _OnKeyDownInComboBox(self, event):
501 """
502 This function is necessary because navigation and control key
503 events do not seem to normally be seen by the wxComboBox's
504 EVT_CHAR routine. (Tabs don't seem to be visible no matter
505 what... {:-( )
506 """
507 if event.GetKeyCode() in self._nav + self._control:
508 self._OnChar(event)
509 return
510 else:
511 event.Skip() # let mixin default KeyDown behavior occur
512
513
514 def _OnSelectChoice(self, event):
515 """
516 This function appears to be necessary, because the processing done
517 on the text of the control somehow interferes with the combobox's
518 selection mechanism for the arrow keys.
519 """
520 ## dbg('MaskedComboBox::OnSelectChoice', indent=1)
521
522 if not self._mask:
523 event.Skip()
524 return
525
526 value = self.GetValue().strip()
527
528 if self._ctrl_constraints._compareNoCase:
529 value = value.lower()
530
531 if event.GetKeyCode() == wx.WXK_UP:
532 direction = -1
533 else:
534 direction = 1
535 match_index, partial_match = self._autoComplete(
536 direction,
537 self._ctrl_constraints._compareChoices,
538 value,
539 self._ctrl_constraints._compareNoCase,
540 current_index = self._ctrl_constraints._autoCompleteIndex)
541 if match_index is not None:
542 ## dbg('setting selection to', match_index)
543 # issue appropriate event to outside:
544 self._OnAutoSelect(self._ctrl_constraints, match_index=match_index)
545 self._CheckValid()
546 keep_processing = False
547 else:
548 pos = self._adjustPos(self._GetInsertionPoint(), event.GetKeyCode())
549 field = self._FindField(pos)
550 if self.IsEmpty() or not field._hasList:
551 ## dbg('selecting 1st value in list')
552 self._OnAutoSelect(self._ctrl_constraints, match_index=0)
553 self._CheckValid()
554 keep_processing = False
555 else:
556 # attempt field-level auto-complete
557 ## dbg(indent=0)
558 keep_processing = self._OnAutoCompleteField(event)
559 ## dbg('keep processing?', keep_processing, indent=0)
560 return keep_processing
561
562
563 def _OnAutoSelect(self, field, match_index):
564 """
565 Override mixin (empty) autocomplete handler, so that autocompletion causes
566 combobox to update appropriately.
567 """
568 ## dbg('MaskedComboBox::OnAutoSelect', field._index, indent=1)
569 ## field._autoCompleteIndex = match_index
570 if field == self._ctrl_constraints:
571 self.SetSelection(match_index)
572 ## dbg('issuing combo selection event')
573 self.GetEventHandler().ProcessEvent(
574 MaskedComboBoxSelectEvent( self.GetId(), match_index, self ) )
575 self._CheckValid()
576 ## dbg('field._autoCompleteIndex:', match_index)
577 ## dbg('self.GetSelection():', self.GetSelection())
578 end = self._goEnd(getPosOnly=True)
579 ## dbg('scheduling set of end position to:', end)
580 # work around bug in wx 2.5
581 wx.CallAfter(self.SetInsertionPoint, 0)
582 wx.CallAfter(self.SetInsertionPoint, end)
583 ## dbg(indent=0)
584
585
586 def _OnReturn(self, event):
587 """
588 For wx.ComboBox, it seems that if you hit return when the dropdown is
589 dropped, the event that dismisses the dropdown will also blank the
590 control, because of the implementation of wxComboBox. So this function
591 examines the selection and if it is -1, and the value according to
592 (the base control!) is a value in the list, then it schedules a
593 programmatic wxComboBox.SetSelection() call to pick the appropriate
594 item in the list. (and then does the usual OnReturn bit.)
595 """
596 ## dbg('MaskedComboBox::OnReturn', indent=1)
597 ## dbg('current value: "%s"' % self.GetValue(), 'current index:', self.GetSelection())
598 if self.GetSelection() == -1 and self.GetValue().lower().strip() in self._ctrl_constraints._compareChoices:
599 wx.CallAfter(self.SetSelection, self._ctrl_constraints._autoCompleteIndex)
600
601 event.m_keyCode = wx.WXK_TAB
602 event.Skip()
603 ## dbg(indent=0)
604
605
606 class ComboBox( BaseMaskedComboBox, MaskedEditAccessorsMixin ):
607 """
608 The "user-visible" masked combobox control, this class is
609 identical to the BaseMaskedComboBox class it's derived from.
610 (This extra level of inheritance allows us to add the generic
611 set of masked edit parameters only to this class while allowing
612 other classes to derive from the "base" masked combobox control,
613 and provide a smaller set of valid accessor functions.)
614 See BaseMaskedComboBox for available methods.
615 """
616 pass
617
618
619 class PreMaskedComboBox( BaseMaskedComboBox, MaskedEditAccessorsMixin ):
620 """
621 This class exists to support the use of XRC subclassing.
622 """
623 # This should really be wx.EVT_WINDOW_CREATE but it is not
624 # currently delivered for native controls on all platforms, so
625 # we'll use EVT_SIZE instead. It should happen shortly after the
626 # control is created as the control is set to its "best" size.
627 _firstEventType = wx.EVT_SIZE
628
629 def __init__(self):
630 pre = wx.PreComboBox()
631 self.PostCreate(pre)
632 self.Bind(self._firstEventType, self.OnCreate)
633
634
635 def OnCreate(self, evt):
636 self.Unbind(self._firstEventType)
637 self._PostInit()
638
639 __i = 0
640 ## CHANGELOG:
641 ## ====================
642 ## Version 1.3
643 ## 1. Made definition of "hack" GetMark conditional on base class not
644 ## implementing it properly, to allow for migration in wx code base
645 ## while taking advantage of improvements therein for some platforms.
646 ##
647 ## Version 1.2
648 ## 1. Converted docstrings to reST format, added doc for ePyDoc.
649 ## 2. Renamed helper functions, vars etc. not intended to be visible in public
650 ## interface to code.
651 ##
652 ## Version 1.1
653 ## 1. Added .SetFont() method that properly resizes control
654 ## 2. Modified control to support construction via XRC mechanism.
655 ## 3. Added AppendItems() to conform with latest combobox.