1# Based on iwidgets2.2.0/scrolledlistbox.itk code.
2
3import types
4import Tkinter
5import Pmw
6
7class ScrolledListBox(Pmw.MegaWidget):
8    _classBindingsDefinedFor = 0
9
10    def __init__(self, parent = None, **kw):
11
12	# Define the megawidget options.
13	INITOPT = Pmw.INITOPT
14	optiondefs = (
15	    ('dblclickcommand',    None,            None),
16	    ('hscrollmode',        'dynamic',       self._hscrollMode),
17	    ('items',              (),              INITOPT),
18	    ('labelmargin',        0,               INITOPT),
19	    ('labelpos',           None,            INITOPT),
20	    ('scrollmargin',       2,               INITOPT),
21	    ('selectioncommand',   None,            None),
22	    ('usehullsize',        0,               INITOPT),
23	    ('vscrollmode',        'dynamic',       self._vscrollMode),
24	)
25	self.defineoptions(kw, optiondefs)
26
27	# Initialise the base class (after defining the options).
28	Pmw.MegaWidget.__init__(self, parent)
29
30	# Create the components.
31	interior = self.interior()
32
33	if self['usehullsize']:
34	    interior.grid_propagate(0)
35
36	# Create the listbox widget.
37	self._listbox = self.createcomponent('listbox',
38		(), None,
39		Tkinter.Listbox, (interior,))
40	self._listbox.grid(row = 2, column = 2, sticky = 'news')
41	interior.grid_rowconfigure(2, weight = 1, minsize = 0)
42	interior.grid_columnconfigure(2, weight = 1, minsize = 0)
43
44	# Create the horizontal scrollbar
45	self._horizScrollbar = self.createcomponent('horizscrollbar',
46		(), 'Scrollbar',
47		Tkinter.Scrollbar, (interior,),
48	        orient='horizontal',
49		command=self._listbox.xview
50	)
51
52	# Create the vertical scrollbar
53	self._vertScrollbar = self.createcomponent('vertscrollbar',
54		(), 'Scrollbar',
55		Tkinter.Scrollbar, (interior,),
56		orient='vertical',
57		command=self._listbox.yview
58	)
59
60	self.createlabel(interior, childCols = 3, childRows = 3)
61
62	# Add the items specified by the initialisation option.
63	items = self['items']
64	if type(items) != types.TupleType:
65	    items = tuple(items)
66	if len(items) > 0:
67	    apply(self._listbox.insert, ('end',) + items)
68
69	_registerScrolledList(self._listbox, self)
70
71        # Establish the special class bindings if not already done.
72        # Also create bindings if the Tkinter default interpreter has
73        # changed.  Use Tkinter._default_root to create class
74        # bindings, so that a reference to root is created by
75        # bind_class rather than a reference to self, which would
76        # prevent object cleanup.
77        theTag = 'ScrolledListBoxTag'
78        if ScrolledListBox._classBindingsDefinedFor != Tkinter._default_root:
79            root  = Tkinter._default_root
80
81            def doubleEvent(event):
82                _handleEvent(event, 'double')
83            def keyEvent(event):
84                _handleEvent(event, 'key')
85            def releaseEvent(event):
86                _handleEvent(event, 'release')
87
88            # Bind space and return keys and button 1 to the selectioncommand.
89            root.bind_class(theTag, '<Key-space>', keyEvent)
90            root.bind_class(theTag, '<Key-Return>', keyEvent)
91            root.bind_class(theTag, '<ButtonRelease-1>', releaseEvent)
92
93            # Bind double button 1 click to the dblclickcommand.
94            root.bind_class(theTag, '<Double-ButtonRelease-1>', doubleEvent)
95
96	    ScrolledListBox._classBindingsDefinedFor = root
97
98	bindtags = self._listbox.bindtags()
99	self._listbox.bindtags(bindtags + (theTag,))
100
101	# Initialise instance variables.
102	self._horizScrollbarOn = 0
103	self._vertScrollbarOn = 0
104	self.scrollTimer = None
105        self._scrollRecurse = 0
106	self._horizScrollbarNeeded = 0
107	self._vertScrollbarNeeded = 0
108
109	# Check keywords and initialise options.
110	self.initialiseoptions()
111
112    def destroy(self):
113	if self.scrollTimer is not None:
114	    self.after_cancel(self.scrollTimer)
115	    self.scrollTimer = None
116	_deregisterScrolledList(self._listbox)
117	Pmw.MegaWidget.destroy(self)
118
119    # ======================================================================
120
121    # Public methods.
122
123    def clear(self):
124	self.setlist(())
125
126    def getcurselection(self):
127	rtn = []
128	for sel in self.curselection():
129	    rtn.append(self._listbox.get(sel))
130	return tuple(rtn)
131
132    def getvalue(self):
133        return self.getcurselection()
134
135    def setvalue(self, textOrList):
136        self._listbox.selection_clear(0, 'end')
137        listitems = list(self._listbox.get(0, 'end'))
138        if type(textOrList) == types.StringType:
139            if textOrList in listitems:
140                self._listbox.selection_set(listitems.index(textOrList))
141            else:
142                raise ValueError, 'no such item "%s"' % textOrList
143        else:
144            for item in textOrList:
145                if item in listitems:
146                    self._listbox.selection_set(listitems.index(item))
147                else:
148                    raise ValueError, 'no such item "%s"' % item
149
150    def setlist(self, items):
151        self._listbox.delete(0, 'end')
152	if len(items) > 0:
153	    if type(items) != types.TupleType:
154		items = tuple(items)
155	    apply(self._listbox.insert, (0,) + items)
156
157    # Override Tkinter.Listbox get method, so that if it is called with
158    # no arguments, return all list elements (consistent with other widgets).
159    def get(self, first=None, last=None):
160	if first is None:
161	    return self._listbox.get(0, 'end')
162	else:
163	    return self._listbox.get(first, last)
164
165    # ======================================================================
166
167    # Configuration methods.
168
169    def _hscrollMode(self):
170	# The horizontal scroll mode has been configured.
171
172	mode = self['hscrollmode']
173
174	if mode == 'static':
175	    if not self._horizScrollbarOn:
176		self._toggleHorizScrollbar()
177	elif mode == 'dynamic':
178	    if self._horizScrollbarNeeded != self._horizScrollbarOn:
179		self._toggleHorizScrollbar()
180	elif mode == 'none':
181	    if self._horizScrollbarOn:
182		self._toggleHorizScrollbar()
183	else:
184	    message = 'bad hscrollmode option "%s": should be static, dynamic, or none' % mode
185	    raise ValueError, message
186
187        self._configureScrollCommands()
188
189    def _vscrollMode(self):
190	# The vertical scroll mode has been configured.
191
192	mode = self['vscrollmode']
193
194	if mode == 'static':
195	    if not self._vertScrollbarOn:
196		self._toggleVertScrollbar()
197	elif mode == 'dynamic':
198	    if self._vertScrollbarNeeded != self._vertScrollbarOn:
199		self._toggleVertScrollbar()
200	elif mode == 'none':
201	    if self._vertScrollbarOn:
202		self._toggleVertScrollbar()
203	else:
204	    message = 'bad vscrollmode option "%s": should be static, dynamic, or none' % mode
205	    raise ValueError, message
206
207        self._configureScrollCommands()
208
209    # ======================================================================
210
211    # Private methods.
212
213    def _configureScrollCommands(self):
214        # If both scrollmodes are not dynamic we can save a lot of
215        # time by not having to create an idle job to handle the
216        # scroll commands.
217
218        # Clean up previous scroll commands to prevent memory leak.
219        tclCommandName = str(self._listbox.cget('xscrollcommand'))
220        if tclCommandName != '':
221            self._listbox.deletecommand(tclCommandName)
222        tclCommandName = str(self._listbox.cget('yscrollcommand'))
223        if tclCommandName != '':
224            self._listbox.deletecommand(tclCommandName)
225
226	if self['hscrollmode'] == self['vscrollmode'] == 'dynamic':
227            self._listbox.configure(
228                    xscrollcommand=self._scrollBothLater,
229                    yscrollcommand=self._scrollBothLater
230            )
231        else:
232            self._listbox.configure(
233                    xscrollcommand=self._scrollXNow,
234                    yscrollcommand=self._scrollYNow
235            )
236
237    def _scrollXNow(self, first, last):
238        self._horizScrollbar.set(first, last)
239        self._horizScrollbarNeeded = ((first, last) != ('0', '1'))
240
241	if self['hscrollmode'] == 'dynamic':
242	    if self._horizScrollbarNeeded != self._horizScrollbarOn:
243		self._toggleHorizScrollbar()
244
245    def _scrollYNow(self, first, last):
246        self._vertScrollbar.set(first, last)
247        self._vertScrollbarNeeded = ((first, last) != ('0', '1'))
248
249        if self['vscrollmode'] == 'dynamic':
250            if self._vertScrollbarNeeded != self._vertScrollbarOn:
251                self._toggleVertScrollbar()
252
253    def _scrollBothLater(self, first, last):
254	# Called by the listbox to set the horizontal or vertical
255	# scrollbar when it has scrolled or changed size or contents.
256
257	if self.scrollTimer is None:
258	    self.scrollTimer = self.after_idle(self._scrollBothNow)
259
260    def _scrollBothNow(self):
261        # This performs the function of _scrollXNow and _scrollYNow.
262        # If one is changed, the other should be updated to match.
263	self.scrollTimer = None
264
265        # Call update_idletasks to make sure that the containing frame
266        # has been resized before we attempt to set the scrollbars.
267        # Otherwise the scrollbars may be mapped/unmapped continuously.
268        self._scrollRecurse = self._scrollRecurse + 1
269        self.update_idletasks()
270        self._scrollRecurse = self._scrollRecurse - 1
271        if self._scrollRecurse != 0:
272            return
273
274	xview = self._listbox.xview()
275	yview = self._listbox.yview()
276	self._horizScrollbar.set(xview[0], xview[1])
277	self._vertScrollbar.set(yview[0], yview[1])
278
279	self._horizScrollbarNeeded = (xview != (0.0, 1.0))
280	self._vertScrollbarNeeded = (yview != (0.0, 1.0))
281
282	# If both horizontal and vertical scrollmodes are dynamic and
283	# currently only one scrollbar is mapped and both should be
284	# toggled, then unmap the mapped scrollbar.  This prevents a
285	# continuous mapping and unmapping of the scrollbars.
286	if (self['hscrollmode'] == self['vscrollmode'] == 'dynamic' and
287		self._horizScrollbarNeeded != self._horizScrollbarOn and
288		self._vertScrollbarNeeded != self._vertScrollbarOn and
289		self._vertScrollbarOn != self._horizScrollbarOn):
290	    if self._horizScrollbarOn:
291		self._toggleHorizScrollbar()
292	    else:
293		self._toggleVertScrollbar()
294	    return
295
296	if self['hscrollmode'] == 'dynamic':
297	    if self._horizScrollbarNeeded != self._horizScrollbarOn:
298		self._toggleHorizScrollbar()
299
300	if self['vscrollmode'] == 'dynamic':
301	    if self._vertScrollbarNeeded != self._vertScrollbarOn:
302		self._toggleVertScrollbar()
303
304    def _toggleHorizScrollbar(self):
305
306	self._horizScrollbarOn = not self._horizScrollbarOn
307
308	interior = self.interior()
309	if self._horizScrollbarOn:
310	    self._horizScrollbar.grid(row = 4, column = 2, sticky = 'news')
311	    interior.grid_rowconfigure(3, minsize = self['scrollmargin'])
312	else:
313	    self._horizScrollbar.grid_forget()
314	    interior.grid_rowconfigure(3, minsize = 0)
315
316    def _toggleVertScrollbar(self):
317
318	self._vertScrollbarOn = not self._vertScrollbarOn
319
320	interior = self.interior()
321	if self._vertScrollbarOn:
322	    self._vertScrollbar.grid(row = 2, column = 4, sticky = 'news')
323	    interior.grid_columnconfigure(3, minsize = self['scrollmargin'])
324	else:
325	    self._vertScrollbar.grid_forget()
326	    interior.grid_columnconfigure(3, minsize = 0)
327
328    def _handleEvent(self, event, eventType):
329        if eventType == 'double':
330            command = self['dblclickcommand']
331        elif eventType == 'key':
332            command = self['selectioncommand']
333        else: #eventType == 'release'
334            # Do not execute the command if the mouse was released
335            # outside the listbox.
336            if (event.x < 0 or self._listbox.winfo_width() <= event.x or
337                    event.y < 0 or self._listbox.winfo_height() <= event.y):
338                return
339
340            command = self['selectioncommand']
341
342        if callable(command):
343            command()
344
345    # Need to explicitly forward this to override the stupid
346    # (grid_)size method inherited from Tkinter.Frame.Grid.
347    def size(self):
348	return self._listbox.size()
349
350    # Need to explicitly forward this to override the stupid
351    # (grid_)bbox method inherited from Tkinter.Frame.Grid.
352    def bbox(self, index):
353	return self._listbox.bbox(index)
354
355Pmw.forwardmethods(ScrolledListBox, Tkinter.Listbox, '_listbox')
356
357# ======================================================================
358
359_listboxCache = {}
360
361def _registerScrolledList(listbox, scrolledList):
362    # Register an ScrolledList widget for a Listbox widget
363
364    _listboxCache[listbox] = scrolledList
365
366def _deregisterScrolledList(listbox):
367    # Deregister a Listbox widget
368    del _listboxCache[listbox]
369
370def _handleEvent(event, eventType):
371    # Forward events for a Listbox to it's ScrolledListBox
372
373    # A binding earlier in the bindtags list may have destroyed the
374    # megawidget, so need to check.
375    if _listboxCache.has_key(event.widget):
376        _listboxCache[event.widget]._handleEvent(event, eventType)
377