Commit | Line | Data |
---|---|---|
920dae64 AT |
1 | import string |
2 | import types | |
3 | import Tkinter | |
4 | import Pmw | |
5 | ||
6 | class ScrolledFrame(Pmw.MegaWidget): | |
7 | def __init__(self, parent = None, **kw): | |
8 | ||
9 | # Define the megawidget options. | |
10 | INITOPT = Pmw.INITOPT | |
11 | optiondefs = ( | |
12 | ('borderframe', 1, INITOPT), | |
13 | ('horizflex', 'fixed', self._horizflex), | |
14 | ('horizfraction', 0.05, INITOPT), | |
15 | ('hscrollmode', 'dynamic', self._hscrollMode), | |
16 | ('labelmargin', 0, INITOPT), | |
17 | ('labelpos', None, INITOPT), | |
18 | ('scrollmargin', 2, INITOPT), | |
19 | ('usehullsize', 0, INITOPT), | |
20 | ('vertflex', 'fixed', self._vertflex), | |
21 | ('vertfraction', 0.05, INITOPT), | |
22 | ('vscrollmode', 'dynamic', self._vscrollMode), | |
23 | ) | |
24 | self.defineoptions(kw, optiondefs) | |
25 | ||
26 | # Initialise the base class (after defining the options). | |
27 | Pmw.MegaWidget.__init__(self, parent) | |
28 | ||
29 | # Create the components. | |
30 | self.origInterior = Pmw.MegaWidget.interior(self) | |
31 | ||
32 | if self['usehullsize']: | |
33 | self.origInterior.grid_propagate(0) | |
34 | ||
35 | if self['borderframe']: | |
36 | # Create a frame widget to act as the border of the clipper. | |
37 | self._borderframe = self.createcomponent('borderframe', | |
38 | (), None, | |
39 | Tkinter.Frame, (self.origInterior,), | |
40 | relief = 'sunken', | |
41 | borderwidth = 2, | |
42 | ) | |
43 | self._borderframe.grid(row = 2, column = 2, sticky = 'news') | |
44 | ||
45 | # Create the clipping window. | |
46 | self._clipper = self.createcomponent('clipper', | |
47 | (), None, | |
48 | Tkinter.Frame, (self._borderframe,), | |
49 | width = 400, | |
50 | height = 300, | |
51 | highlightthickness = 0, | |
52 | borderwidth = 0, | |
53 | ) | |
54 | self._clipper.pack(fill = 'both', expand = 1) | |
55 | else: | |
56 | # Create the clipping window. | |
57 | self._clipper = self.createcomponent('clipper', | |
58 | (), None, | |
59 | Tkinter.Frame, (self.origInterior,), | |
60 | width = 400, | |
61 | height = 300, | |
62 | relief = 'sunken', | |
63 | borderwidth = 2, | |
64 | ) | |
65 | self._clipper.grid(row = 2, column = 2, sticky = 'news') | |
66 | ||
67 | self.origInterior.grid_rowconfigure(2, weight = 1, minsize = 0) | |
68 | self.origInterior.grid_columnconfigure(2, weight = 1, minsize = 0) | |
69 | ||
70 | # Create the horizontal scrollbar | |
71 | self._horizScrollbar = self.createcomponent('horizscrollbar', | |
72 | (), 'Scrollbar', | |
73 | Tkinter.Scrollbar, (self.origInterior,), | |
74 | orient='horizontal', | |
75 | command=self.xview | |
76 | ) | |
77 | ||
78 | # Create the vertical scrollbar | |
79 | self._vertScrollbar = self.createcomponent('vertscrollbar', | |
80 | (), 'Scrollbar', | |
81 | Tkinter.Scrollbar, (self.origInterior,), | |
82 | orient='vertical', | |
83 | command=self.yview | |
84 | ) | |
85 | ||
86 | self.createlabel(self.origInterior, childCols = 3, childRows = 3) | |
87 | ||
88 | # Initialise instance variables. | |
89 | self._horizScrollbarOn = 0 | |
90 | self._vertScrollbarOn = 0 | |
91 | self.scrollTimer = None | |
92 | self._scrollRecurse = 0 | |
93 | self._horizScrollbarNeeded = 0 | |
94 | self._vertScrollbarNeeded = 0 | |
95 | self.startX = 0 | |
96 | self.startY = 0 | |
97 | self._flexoptions = ('fixed', 'expand', 'shrink', 'elastic') | |
98 | ||
99 | # Create a frame in the clipper to contain the widgets to be | |
100 | # scrolled. | |
101 | self._frame = self.createcomponent('frame', | |
102 | (), None, | |
103 | Tkinter.Frame, (self._clipper,) | |
104 | ) | |
105 | ||
106 | # Whenever the clipping window or scrolled frame change size, | |
107 | # update the scrollbars. | |
108 | self._frame.bind('<Configure>', self._reposition) | |
109 | self._clipper.bind('<Configure>', self._reposition) | |
110 | ||
111 | # Work around a bug in Tk where the value returned by the | |
112 | # scrollbar get() method is (0.0, 0.0, 0.0, 0.0) rather than | |
113 | # the expected 2-tuple. This occurs if xview() is called soon | |
114 | # after the Pmw.ScrolledFrame has been created. | |
115 | self._horizScrollbar.set(0.0, 1.0) | |
116 | self._vertScrollbar.set(0.0, 1.0) | |
117 | ||
118 | # Check keywords and initialise options. | |
119 | self.initialiseoptions() | |
120 | ||
121 | def destroy(self): | |
122 | if self.scrollTimer is not None: | |
123 | self.after_cancel(self.scrollTimer) | |
124 | self.scrollTimer = None | |
125 | Pmw.MegaWidget.destroy(self) | |
126 | ||
127 | # ====================================================================== | |
128 | ||
129 | # Public methods. | |
130 | ||
131 | def interior(self): | |
132 | return self._frame | |
133 | ||
134 | # Set timer to call real reposition method, so that it is not | |
135 | # called multiple times when many things are reconfigured at the | |
136 | # same time. | |
137 | def reposition(self): | |
138 | if self.scrollTimer is None: | |
139 | self.scrollTimer = self.after_idle(self._scrollBothNow) | |
140 | ||
141 | # Called when the user clicks in the horizontal scrollbar. | |
142 | # Calculates new position of frame then calls reposition() to | |
143 | # update the frame and the scrollbar. | |
144 | def xview(self, mode = None, value = None, units = None): | |
145 | ||
146 | if type(value) == types.StringType: | |
147 | value = string.atof(value) | |
148 | if mode is None: | |
149 | return self._horizScrollbar.get() | |
150 | elif mode == 'moveto': | |
151 | frameWidth = self._frame.winfo_reqwidth() | |
152 | self.startX = value * float(frameWidth) | |
153 | else: # mode == 'scroll' | |
154 | clipperWidth = self._clipper.winfo_width() | |
155 | if units == 'units': | |
156 | jump = int(clipperWidth * self['horizfraction']) | |
157 | else: | |
158 | jump = clipperWidth | |
159 | self.startX = self.startX + value * jump | |
160 | ||
161 | self.reposition() | |
162 | ||
163 | # Called when the user clicks in the vertical scrollbar. | |
164 | # Calculates new position of frame then calls reposition() to | |
165 | # update the frame and the scrollbar. | |
166 | def yview(self, mode = None, value = None, units = None): | |
167 | ||
168 | if type(value) == types.StringType: | |
169 | value = string.atof(value) | |
170 | if mode is None: | |
171 | return self._vertScrollbar.get() | |
172 | elif mode == 'moveto': | |
173 | frameHeight = self._frame.winfo_reqheight() | |
174 | self.startY = value * float(frameHeight) | |
175 | else: # mode == 'scroll' | |
176 | clipperHeight = self._clipper.winfo_height() | |
177 | if units == 'units': | |
178 | jump = int(clipperHeight * self['vertfraction']) | |
179 | else: | |
180 | jump = clipperHeight | |
181 | self.startY = self.startY + value * jump | |
182 | ||
183 | self.reposition() | |
184 | ||
185 | # ====================================================================== | |
186 | ||
187 | # Configuration methods. | |
188 | ||
189 | def _hscrollMode(self): | |
190 | # The horizontal scroll mode has been configured. | |
191 | ||
192 | mode = self['hscrollmode'] | |
193 | ||
194 | if mode == 'static': | |
195 | if not self._horizScrollbarOn: | |
196 | self._toggleHorizScrollbar() | |
197 | elif mode == 'dynamic': | |
198 | if self._horizScrollbarNeeded != self._horizScrollbarOn: | |
199 | self._toggleHorizScrollbar() | |
200 | elif mode == 'none': | |
201 | if self._horizScrollbarOn: | |
202 | self._toggleHorizScrollbar() | |
203 | else: | |
204 | message = 'bad hscrollmode option "%s": should be static, dynamic, or none' % mode | |
205 | raise ValueError, message | |
206 | ||
207 | def _vscrollMode(self): | |
208 | # The vertical scroll mode has been configured. | |
209 | ||
210 | mode = self['vscrollmode'] | |
211 | ||
212 | if mode == 'static': | |
213 | if not self._vertScrollbarOn: | |
214 | self._toggleVertScrollbar() | |
215 | elif mode == 'dynamic': | |
216 | if self._vertScrollbarNeeded != self._vertScrollbarOn: | |
217 | self._toggleVertScrollbar() | |
218 | elif mode == 'none': | |
219 | if self._vertScrollbarOn: | |
220 | self._toggleVertScrollbar() | |
221 | else: | |
222 | message = 'bad vscrollmode option "%s": should be static, dynamic, or none' % mode | |
223 | raise ValueError, message | |
224 | ||
225 | def _horizflex(self): | |
226 | # The horizontal flex mode has been configured. | |
227 | ||
228 | flex = self['horizflex'] | |
229 | ||
230 | if flex not in self._flexoptions: | |
231 | message = 'bad horizflex option "%s": should be one of %s' % \ | |
232 | (flex, str(self._flexoptions)) | |
233 | raise ValueError, message | |
234 | ||
235 | self.reposition() | |
236 | ||
237 | def _vertflex(self): | |
238 | # The vertical flex mode has been configured. | |
239 | ||
240 | flex = self['vertflex'] | |
241 | ||
242 | if flex not in self._flexoptions: | |
243 | message = 'bad vertflex option "%s": should be one of %s' % \ | |
244 | (flex, str(self._flexoptions)) | |
245 | raise ValueError, message | |
246 | ||
247 | self.reposition() | |
248 | ||
249 | # ====================================================================== | |
250 | ||
251 | # Private methods. | |
252 | ||
253 | def _reposition(self, event): | |
254 | self.reposition() | |
255 | ||
256 | def _getxview(self): | |
257 | ||
258 | # Horizontal dimension. | |
259 | clipperWidth = self._clipper.winfo_width() | |
260 | frameWidth = self._frame.winfo_reqwidth() | |
261 | if frameWidth <= clipperWidth: | |
262 | # The scrolled frame is smaller than the clipping window. | |
263 | ||
264 | self.startX = 0 | |
265 | endScrollX = 1.0 | |
266 | ||
267 | if self['horizflex'] in ('expand', 'elastic'): | |
268 | relwidth = 1 | |
269 | else: | |
270 | relwidth = '' | |
271 | else: | |
272 | # The scrolled frame is larger than the clipping window. | |
273 | ||
274 | if self['horizflex'] in ('shrink', 'elastic'): | |
275 | self.startX = 0 | |
276 | endScrollX = 1.0 | |
277 | relwidth = 1 | |
278 | else: | |
279 | if self.startX + clipperWidth > frameWidth: | |
280 | self.startX = frameWidth - clipperWidth | |
281 | endScrollX = 1.0 | |
282 | else: | |
283 | if self.startX < 0: | |
284 | self.startX = 0 | |
285 | endScrollX = (self.startX + clipperWidth) / float(frameWidth) | |
286 | relwidth = '' | |
287 | ||
288 | # Position frame relative to clipper. | |
289 | self._frame.place(x = -self.startX, relwidth = relwidth) | |
290 | return (self.startX / float(frameWidth), endScrollX) | |
291 | ||
292 | def _getyview(self): | |
293 | ||
294 | # Vertical dimension. | |
295 | clipperHeight = self._clipper.winfo_height() | |
296 | frameHeight = self._frame.winfo_reqheight() | |
297 | if frameHeight <= clipperHeight: | |
298 | # The scrolled frame is smaller than the clipping window. | |
299 | ||
300 | self.startY = 0 | |
301 | endScrollY = 1.0 | |
302 | ||
303 | if self['vertflex'] in ('expand', 'elastic'): | |
304 | relheight = 1 | |
305 | else: | |
306 | relheight = '' | |
307 | else: | |
308 | # The scrolled frame is larger than the clipping window. | |
309 | ||
310 | if self['vertflex'] in ('shrink', 'elastic'): | |
311 | self.startY = 0 | |
312 | endScrollY = 1.0 | |
313 | relheight = 1 | |
314 | else: | |
315 | if self.startY + clipperHeight > frameHeight: | |
316 | self.startY = frameHeight - clipperHeight | |
317 | endScrollY = 1.0 | |
318 | else: | |
319 | if self.startY < 0: | |
320 | self.startY = 0 | |
321 | endScrollY = (self.startY + clipperHeight) / float(frameHeight) | |
322 | relheight = '' | |
323 | ||
324 | # Position frame relative to clipper. | |
325 | self._frame.place(y = -self.startY, relheight = relheight) | |
326 | return (self.startY / float(frameHeight), endScrollY) | |
327 | ||
328 | # According to the relative geometries of the frame and the | |
329 | # clipper, reposition the frame within the clipper and reset the | |
330 | # scrollbars. | |
331 | def _scrollBothNow(self): | |
332 | self.scrollTimer = None | |
333 | ||
334 | # Call update_idletasks to make sure that the containing frame | |
335 | # has been resized before we attempt to set the scrollbars. | |
336 | # Otherwise the scrollbars may be mapped/unmapped continuously. | |
337 | self._scrollRecurse = self._scrollRecurse + 1 | |
338 | self.update_idletasks() | |
339 | self._scrollRecurse = self._scrollRecurse - 1 | |
340 | if self._scrollRecurse != 0: | |
341 | return | |
342 | ||
343 | xview = self._getxview() | |
344 | yview = self._getyview() | |
345 | self._horizScrollbar.set(xview[0], xview[1]) | |
346 | self._vertScrollbar.set(yview[0], yview[1]) | |
347 | ||
348 | self._horizScrollbarNeeded = (xview != (0.0, 1.0)) | |
349 | self._vertScrollbarNeeded = (yview != (0.0, 1.0)) | |
350 | ||
351 | # If both horizontal and vertical scrollmodes are dynamic and | |
352 | # currently only one scrollbar is mapped and both should be | |
353 | # toggled, then unmap the mapped scrollbar. This prevents a | |
354 | # continuous mapping and unmapping of the scrollbars. | |
355 | if (self['hscrollmode'] == self['vscrollmode'] == 'dynamic' and | |
356 | self._horizScrollbarNeeded != self._horizScrollbarOn and | |
357 | self._vertScrollbarNeeded != self._vertScrollbarOn and | |
358 | self._vertScrollbarOn != self._horizScrollbarOn): | |
359 | if self._horizScrollbarOn: | |
360 | self._toggleHorizScrollbar() | |
361 | else: | |
362 | self._toggleVertScrollbar() | |
363 | return | |
364 | ||
365 | if self['hscrollmode'] == 'dynamic': | |
366 | if self._horizScrollbarNeeded != self._horizScrollbarOn: | |
367 | self._toggleHorizScrollbar() | |
368 | ||
369 | if self['vscrollmode'] == 'dynamic': | |
370 | if self._vertScrollbarNeeded != self._vertScrollbarOn: | |
371 | self._toggleVertScrollbar() | |
372 | ||
373 | def _toggleHorizScrollbar(self): | |
374 | ||
375 | self._horizScrollbarOn = not self._horizScrollbarOn | |
376 | ||
377 | interior = self.origInterior | |
378 | if self._horizScrollbarOn: | |
379 | self._horizScrollbar.grid(row = 4, column = 2, sticky = 'news') | |
380 | interior.grid_rowconfigure(3, minsize = self['scrollmargin']) | |
381 | else: | |
382 | self._horizScrollbar.grid_forget() | |
383 | interior.grid_rowconfigure(3, minsize = 0) | |
384 | ||
385 | def _toggleVertScrollbar(self): | |
386 | ||
387 | self._vertScrollbarOn = not self._vertScrollbarOn | |
388 | ||
389 | interior = self.origInterior | |
390 | if self._vertScrollbarOn: | |
391 | self._vertScrollbar.grid(row = 2, column = 4, sticky = 'news') | |
392 | interior.grid_columnconfigure(3, minsize = self['scrollmargin']) | |
393 | else: | |
394 | self._vertScrollbar.grid_forget() | |
395 | interior.grid_columnconfigure(3, minsize = 0) |