Introduction
This recipe extends the MozillaStyleListBox recipe.
wx.ListBox provides a truly annoying default type-selection behavior: Whatever key you type, it selects the first item that starts with that key. So if, for example, you have a large number of items beginning with w (such as in a list of wxPython constants!), you still have to use the arrow keys (a lot!) or the mouse.
This recipe offers a wx.ListBox subclass that will jump directly to the item that starts with the phrase you type in (as opposed to selecting the next item that starts with the character you just typed). It will reset the phrase (to allow the type-selection to begin again from the start) after a definable interval of inactivity.
What Objects are Involved
wx.ListBox
string.printable
Process Overview
When the user presses any printable key, that key is appended to a selection prefix. The TypeSelectListBox then deselects any items already selected and goes to the first item that begins with the selection phrase. The process is case insensitive.
The selection prefix is automatically cleared after a configurable interval, to allow the user to pause, then type a new prefix. This behavior is very common in GUI applications. This timeout behavior can be suppressed by passing timeout=0 to the constructor.
The user may type Backspace to remove the trailing character from the prefix, or Delete to clear the prefix and start over.
The selection prefix reset timer is restarted when the user types any character, so the user has the specified interval to locate and press the next key. The timer is restarted even if the user types a non-printable character other than backspace or delete, to allow the user more time to find the key he or she meant to hit.
Special Concerns
If you plan to use any method other than Set to add items to the wx.ListBox (such as Append), you need to provide a method for it that sets self.choices to the items that make up the wx.ListBox.
Code Sample
1 import wx
2 from string import printable as _printableChars
3
4 class TypeSelectListBox( wx.ListBox ):
5 def __init__( self, parent, id, pos=wx.DefaultPosition,
6 size=wx.DefaultSize, choices=[], timeout=1.6, style=0,
7 validator=wx.DefaultValidator, name="listBox" ):
8 """
9 @param timeout: Type-selection timeout in seconds.
10 To suppress timeout behavior, pass timeout=0.
11 Default value of 1.6 seconds is empirical.
12 """
13
14 super(TypeSelectListBox,self).__init__( parent, id,
15 pos, size, choices, style, validator, name )
16
17 self._choices = choices
18 self._prefix = ''
19 self._timeoutInterval = timeout
20 if self._timeoutInterval:
21 # Convert timeout interval to milliseconds
22 self._timeoutInterval = int( 1000 * self._timeoutInterval )
23 # Timeout was specified, so initialize my timer.
24 self._timerId = wx.NewId()
25 self._timer = wx.Timer( self, self._timerId )
26 # Process timeouts
27 self.Bind( wx.EVT_TIMER, self._onTimer )
28
29 # Intercept character key events
30 self.Bind( wx.EVT_CHAR, self._onEvtChar )
31
32 def _onTimer( self, evt ):
33 """
34 Clear prefix, but don't clear current selection.
35 When user resumes typing, we'll start with a new prefix.
36
37 Do not restart timer. It will be restarted when (if) user
38 resumes typing.
39 """
40 self._prefix = ''
41
42 def _startTimer( self ):
43 # (Re)start the timeout timer
44 if self._timeoutInterval:
45 self._timer.Start( self._timeoutInterval, wx.TIMER_ONE_SHOT )
46
47 def _onEvtChar( self, evt ):
48 # Suspend timer
49 self._timer.Stop()
50
51 keycode = evt.GetKeyCode()
52 if keycode == wx.WXK_BACK:
53 # Backspace removes last letter
54 self._prefix = self._prefix[:-1]
55 elif keycode == wx.WXK_DELETE:
56 # Delete erases prefix
57 self._prefix = ''
58 elif keycode>127 or chr(keycode) not in _printableChars:
59 # Do the default action for nonascii keycodes
60 evt.Skip()
61 # Restart the timer
62 self._startTimer()
63 return
64 else:
65 # It's a printable character
66 self._prefix += chr(keycode).lower()
67
68 # De-select currently-selected item
69 for item in self.GetSelections():
70 self.Deselect(item)
71
72 if self._prefix != '':
73 # Locate first item with this prefix
74 for choice in self._choices:
75 if choice.lower().startswith( self._prefix ):
76 self.SetStringSelection( choice )
77 break
78 # Restart the timer
79 self._startTimer()
80
81 def Set( self, choices ):
82 self._choices = choices
83 self._prefix = ''
84 wx.ListBox.Set(self, choices)
85 return 0
Comments
Please place any questions or comments here. I've subscribed to the page, so I should see any changes.