Commit | Line | Data |
---|---|---|
920dae64 AT |
1 | # Based on iwidgets2.2.0/scrolledlistbox.itk code. |
2 | ||
3 | import types | |
4 | import Tkinter | |
5 | import Pmw | |
6 | ||
7 | class 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 | ||
355 | Pmw.forwardmethods(ScrolledListBox, Tkinter.Listbox, '_listbox') | |
356 | ||
357 | # ====================================================================== | |
358 | ||
359 | _listboxCache = {} | |
360 | ||
361 | def _registerScrolledList(listbox, scrolledList): | |
362 | # Register an ScrolledList widget for a Listbox widget | |
363 | ||
364 | _listboxCache[listbox] = scrolledList | |
365 | ||
366 | def _deregisterScrolledList(listbox): | |
367 | # Deregister a Listbox widget | |
368 | del _listboxCache[listbox] | |
369 | ||
370 | def _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) |