Commit | Line | Data |
---|---|---|
920dae64 AT |
1 | # Based on iwidgets2.2.0/scrolledtext.itk code. |
2 | ||
3 | import Tkinter | |
4 | import Pmw | |
5 | ||
6 | class ScrolledText(Pmw.MegaWidget): | |
7 | def __init__(self, parent = None, **kw): | |
8 | ||
9 | # Define the megawidget options. | |
10 | INITOPT = Pmw.INITOPT | |
11 | optiondefs = ( | |
12 | ('borderframe', 0, INITOPT), | |
13 | ('columnheader', 0, INITOPT), | |
14 | ('hscrollmode', 'dynamic', self._hscrollMode), | |
15 | ('labelmargin', 0, INITOPT), | |
16 | ('labelpos', None, INITOPT), | |
17 | ('rowcolumnheader',0, INITOPT), | |
18 | ('rowheader', 0, INITOPT), | |
19 | ('scrollmargin', 2, INITOPT), | |
20 | ('usehullsize', 0, INITOPT), | |
21 | ('vscrollmode', 'dynamic', self._vscrollMode), | |
22 | ) | |
23 | self.defineoptions(kw, optiondefs) | |
24 | ||
25 | # Initialise the base class (after defining the options). | |
26 | Pmw.MegaWidget.__init__(self, parent) | |
27 | ||
28 | # Create the components. | |
29 | interior = self.interior() | |
30 | ||
31 | if self['usehullsize']: | |
32 | interior.grid_propagate(0) | |
33 | ||
34 | if self['borderframe']: | |
35 | # Create a frame widget to act as the border of the text | |
36 | # widget. Later, pack the text widget so that it fills | |
37 | # the frame. This avoids a problem in Tk, where window | |
38 | # items in a text widget may overlap the border of the | |
39 | # text widget. | |
40 | self._borderframe = self.createcomponent('borderframe', | |
41 | (), None, | |
42 | Tkinter.Frame, (interior,), | |
43 | relief = 'sunken', | |
44 | borderwidth = 2, | |
45 | ) | |
46 | self._borderframe.grid(row = 4, column = 4, sticky = 'news') | |
47 | ||
48 | # Create the text widget. | |
49 | self._textbox = self.createcomponent('text', | |
50 | (), None, | |
51 | Tkinter.Text, (self._borderframe,), | |
52 | highlightthickness = 0, | |
53 | borderwidth = 0, | |
54 | ) | |
55 | self._textbox.pack(fill = 'both', expand = 1) | |
56 | ||
57 | bw = self._borderframe.cget('borderwidth'), | |
58 | ht = self._borderframe.cget('highlightthickness'), | |
59 | else: | |
60 | # Create the text widget. | |
61 | self._textbox = self.createcomponent('text', | |
62 | (), None, | |
63 | Tkinter.Text, (interior,), | |
64 | ) | |
65 | self._textbox.grid(row = 4, column = 4, sticky = 'news') | |
66 | ||
67 | bw = self._textbox.cget('borderwidth'), | |
68 | ht = self._textbox.cget('highlightthickness'), | |
69 | ||
70 | # Create the header text widgets | |
71 | if self['columnheader']: | |
72 | self._columnheader = self.createcomponent('columnheader', | |
73 | (), 'Header', | |
74 | Tkinter.Text, (interior,), | |
75 | height=1, | |
76 | wrap='none', | |
77 | borderwidth = bw, | |
78 | highlightthickness = ht, | |
79 | ) | |
80 | self._columnheader.grid(row = 2, column = 4, sticky = 'ew') | |
81 | self._columnheader.configure( | |
82 | xscrollcommand = self._columnheaderscrolled) | |
83 | ||
84 | if self['rowheader']: | |
85 | self._rowheader = self.createcomponent('rowheader', | |
86 | (), 'Header', | |
87 | Tkinter.Text, (interior,), | |
88 | wrap='none', | |
89 | borderwidth = bw, | |
90 | highlightthickness = ht, | |
91 | ) | |
92 | self._rowheader.grid(row = 4, column = 2, sticky = 'ns') | |
93 | self._rowheader.configure( | |
94 | yscrollcommand = self._rowheaderscrolled) | |
95 | ||
96 | if self['rowcolumnheader']: | |
97 | self._rowcolumnheader = self.createcomponent('rowcolumnheader', | |
98 | (), 'Header', | |
99 | Tkinter.Text, (interior,), | |
100 | height=1, | |
101 | wrap='none', | |
102 | borderwidth = bw, | |
103 | highlightthickness = ht, | |
104 | ) | |
105 | self._rowcolumnheader.grid(row = 2, column = 2, sticky = 'nsew') | |
106 | ||
107 | interior.grid_rowconfigure(4, weight = 1, minsize = 0) | |
108 | interior.grid_columnconfigure(4, weight = 1, minsize = 0) | |
109 | ||
110 | # Create the horizontal scrollbar | |
111 | self._horizScrollbar = self.createcomponent('horizscrollbar', | |
112 | (), 'Scrollbar', | |
113 | Tkinter.Scrollbar, (interior,), | |
114 | orient='horizontal', | |
115 | command=self._textbox.xview | |
116 | ) | |
117 | ||
118 | # Create the vertical scrollbar | |
119 | self._vertScrollbar = self.createcomponent('vertscrollbar', | |
120 | (), 'Scrollbar', | |
121 | Tkinter.Scrollbar, (interior,), | |
122 | orient='vertical', | |
123 | command=self._textbox.yview | |
124 | ) | |
125 | ||
126 | self.createlabel(interior, childCols = 5, childRows = 5) | |
127 | ||
128 | # Initialise instance variables. | |
129 | self._horizScrollbarOn = 0 | |
130 | self._vertScrollbarOn = 0 | |
131 | self.scrollTimer = None | |
132 | self._scrollRecurse = 0 | |
133 | self._horizScrollbarNeeded = 0 | |
134 | self._vertScrollbarNeeded = 0 | |
135 | self._textWidth = None | |
136 | ||
137 | # These four variables avoid an infinite loop caused by the | |
138 | # row or column header's scrollcommand causing the main text | |
139 | # widget's scrollcommand to be called and vice versa. | |
140 | self._textboxLastX = None | |
141 | self._textboxLastY = None | |
142 | self._columnheaderLastX = None | |
143 | self._rowheaderLastY = None | |
144 | ||
145 | # Check keywords and initialise options. | |
146 | self.initialiseoptions() | |
147 | ||
148 | def destroy(self): | |
149 | if self.scrollTimer is not None: | |
150 | self.after_cancel(self.scrollTimer) | |
151 | self.scrollTimer = None | |
152 | Pmw.MegaWidget.destroy(self) | |
153 | ||
154 | # ====================================================================== | |
155 | ||
156 | # Public methods. | |
157 | ||
158 | def clear(self): | |
159 | self.settext('') | |
160 | ||
161 | def importfile(self, fileName, where = 'end'): | |
162 | file = open(fileName, 'r') | |
163 | self._textbox.insert(where, file.read()) | |
164 | file.close() | |
165 | ||
166 | def exportfile(self, fileName): | |
167 | file = open(fileName, 'w') | |
168 | file.write(self._textbox.get('1.0', 'end')) | |
169 | file.close() | |
170 | ||
171 | def settext(self, text): | |
172 | disabled = (str(self._textbox.cget('state')) == 'disabled') | |
173 | if disabled: | |
174 | self._textbox.configure(state='normal') | |
175 | self._textbox.delete('0.0', 'end') | |
176 | self._textbox.insert('end', text) | |
177 | if disabled: | |
178 | self._textbox.configure(state='disabled') | |
179 | ||
180 | # Override Tkinter.Text get method, so that if it is called with | |
181 | # no arguments, return all text (consistent with other widgets). | |
182 | def get(self, first=None, last=None): | |
183 | if first is None: | |
184 | return self._textbox.get('1.0', 'end') | |
185 | else: | |
186 | return self._textbox.get(first, last) | |
187 | ||
188 | def getvalue(self): | |
189 | return self.get() | |
190 | ||
191 | def setvalue(self, text): | |
192 | return self.settext(text) | |
193 | ||
194 | def appendtext(self, text): | |
195 | oldTop, oldBottom = self._textbox.yview() | |
196 | ||
197 | disabled = (str(self._textbox.cget('state')) == 'disabled') | |
198 | if disabled: | |
199 | self._textbox.configure(state='normal') | |
200 | self._textbox.insert('end', text) | |
201 | if disabled: | |
202 | self._textbox.configure(state='disabled') | |
203 | ||
204 | if oldBottom == 1.0: | |
205 | self._textbox.yview('moveto', 1.0) | |
206 | ||
207 | # ====================================================================== | |
208 | ||
209 | # Configuration methods. | |
210 | ||
211 | def _hscrollMode(self): | |
212 | # The horizontal scroll mode has been configured. | |
213 | ||
214 | mode = self['hscrollmode'] | |
215 | ||
216 | if mode == 'static': | |
217 | if not self._horizScrollbarOn: | |
218 | self._toggleHorizScrollbar() | |
219 | elif mode == 'dynamic': | |
220 | if self._horizScrollbarNeeded != self._horizScrollbarOn: | |
221 | self._toggleHorizScrollbar() | |
222 | elif mode == 'none': | |
223 | if self._horizScrollbarOn: | |
224 | self._toggleHorizScrollbar() | |
225 | else: | |
226 | message = 'bad hscrollmode option "%s": should be static, dynamic, or none' % mode | |
227 | raise ValueError, message | |
228 | ||
229 | self._configureScrollCommands() | |
230 | ||
231 | def _vscrollMode(self): | |
232 | # The vertical scroll mode has been configured. | |
233 | ||
234 | mode = self['vscrollmode'] | |
235 | ||
236 | if mode == 'static': | |
237 | if not self._vertScrollbarOn: | |
238 | self._toggleVertScrollbar() | |
239 | elif mode == 'dynamic': | |
240 | if self._vertScrollbarNeeded != self._vertScrollbarOn: | |
241 | self._toggleVertScrollbar() | |
242 | elif mode == 'none': | |
243 | if self._vertScrollbarOn: | |
244 | self._toggleVertScrollbar() | |
245 | else: | |
246 | message = 'bad vscrollmode option "%s": should be static, dynamic, or none' % mode | |
247 | raise ValueError, message | |
248 | ||
249 | self._configureScrollCommands() | |
250 | ||
251 | # ====================================================================== | |
252 | ||
253 | # Private methods. | |
254 | ||
255 | def _configureScrollCommands(self): | |
256 | # If both scrollmodes are not dynamic we can save a lot of | |
257 | # time by not having to create an idle job to handle the | |
258 | # scroll commands. | |
259 | ||
260 | # Clean up previous scroll commands to prevent memory leak. | |
261 | tclCommandName = str(self._textbox.cget('xscrollcommand')) | |
262 | if tclCommandName != '': | |
263 | self._textbox.deletecommand(tclCommandName) | |
264 | tclCommandName = str(self._textbox.cget('yscrollcommand')) | |
265 | if tclCommandName != '': | |
266 | self._textbox.deletecommand(tclCommandName) | |
267 | ||
268 | if self['hscrollmode'] == self['vscrollmode'] == 'dynamic': | |
269 | self._textbox.configure( | |
270 | xscrollcommand=self._scrollBothLater, | |
271 | yscrollcommand=self._scrollBothLater | |
272 | ) | |
273 | else: | |
274 | self._textbox.configure( | |
275 | xscrollcommand=self._scrollXNow, | |
276 | yscrollcommand=self._scrollYNow | |
277 | ) | |
278 | ||
279 | def _scrollXNow(self, first, last): | |
280 | self._horizScrollbar.set(first, last) | |
281 | self._horizScrollbarNeeded = ((first, last) != ('0', '1')) | |
282 | ||
283 | # This code is the same as in _scrollBothNow. Keep it that way. | |
284 | if self['hscrollmode'] == 'dynamic': | |
285 | currentWidth = self._textbox.winfo_width() | |
286 | if self._horizScrollbarNeeded != self._horizScrollbarOn: | |
287 | if self._horizScrollbarNeeded or \ | |
288 | self._textWidth != currentWidth: | |
289 | self._toggleHorizScrollbar() | |
290 | self._textWidth = currentWidth | |
291 | ||
292 | if self['columnheader']: | |
293 | if self._columnheaderLastX != first: | |
294 | self._columnheaderLastX = first | |
295 | self._columnheader.xview('moveto', first) | |
296 | ||
297 | def _scrollYNow(self, first, last): | |
298 | if first == '0' and last == '0': | |
299 | return | |
300 | self._vertScrollbar.set(first, last) | |
301 | self._vertScrollbarNeeded = ((first, last) != ('0', '1')) | |
302 | ||
303 | if self['vscrollmode'] == 'dynamic': | |
304 | if self._vertScrollbarNeeded != self._vertScrollbarOn: | |
305 | self._toggleVertScrollbar() | |
306 | ||
307 | if self['rowheader']: | |
308 | if self._rowheaderLastY != first: | |
309 | self._rowheaderLastY = first | |
310 | self._rowheader.yview('moveto', first) | |
311 | ||
312 | def _scrollBothLater(self, first, last): | |
313 | # Called by the text widget to set the horizontal or vertical | |
314 | # scrollbar when it has scrolled or changed size or contents. | |
315 | ||
316 | if self.scrollTimer is None: | |
317 | self.scrollTimer = self.after_idle(self._scrollBothNow) | |
318 | ||
319 | def _scrollBothNow(self): | |
320 | # This performs the function of _scrollXNow and _scrollYNow. | |
321 | # If one is changed, the other should be updated to match. | |
322 | self.scrollTimer = None | |
323 | ||
324 | # Call update_idletasks to make sure that the containing frame | |
325 | # has been resized before we attempt to set the scrollbars. | |
326 | # Otherwise the scrollbars may be mapped/unmapped continuously. | |
327 | self._scrollRecurse = self._scrollRecurse + 1 | |
328 | self.update_idletasks() | |
329 | self._scrollRecurse = self._scrollRecurse - 1 | |
330 | if self._scrollRecurse != 0: | |
331 | return | |
332 | ||
333 | xview = self._textbox.xview() | |
334 | yview = self._textbox.yview() | |
335 | ||
336 | # The text widget returns a yview of (0.0, 0.0) just after it | |
337 | # has been created. Ignore this. | |
338 | if yview == (0.0, 0.0): | |
339 | return | |
340 | ||
341 | if self['columnheader']: | |
342 | if self._columnheaderLastX != xview[0]: | |
343 | self._columnheaderLastX = xview[0] | |
344 | self._columnheader.xview('moveto', xview[0]) | |
345 | if self['rowheader']: | |
346 | if self._rowheaderLastY != yview[0]: | |
347 | self._rowheaderLastY = yview[0] | |
348 | self._rowheader.yview('moveto', yview[0]) | |
349 | ||
350 | self._horizScrollbar.set(xview[0], xview[1]) | |
351 | self._vertScrollbar.set(yview[0], yview[1]) | |
352 | ||
353 | self._horizScrollbarNeeded = (xview != (0.0, 1.0)) | |
354 | self._vertScrollbarNeeded = (yview != (0.0, 1.0)) | |
355 | ||
356 | # If both horizontal and vertical scrollmodes are dynamic and | |
357 | # currently only one scrollbar is mapped and both should be | |
358 | # toggled, then unmap the mapped scrollbar. This prevents a | |
359 | # continuous mapping and unmapping of the scrollbars. | |
360 | if (self['hscrollmode'] == self['vscrollmode'] == 'dynamic' and | |
361 | self._horizScrollbarNeeded != self._horizScrollbarOn and | |
362 | self._vertScrollbarNeeded != self._vertScrollbarOn and | |
363 | self._vertScrollbarOn != self._horizScrollbarOn): | |
364 | if self._horizScrollbarOn: | |
365 | self._toggleHorizScrollbar() | |
366 | else: | |
367 | self._toggleVertScrollbar() | |
368 | return | |
369 | ||
370 | if self['hscrollmode'] == 'dynamic': | |
371 | ||
372 | # The following test is done to prevent continuous | |
373 | # mapping and unmapping of the horizontal scrollbar. | |
374 | # This may occur when some event (scrolling, resizing | |
375 | # or text changes) modifies the displayed text such | |
376 | # that the bottom line in the window is the longest | |
377 | # line displayed. If this causes the horizontal | |
378 | # scrollbar to be mapped, the scrollbar may "cover up" | |
379 | # the bottom line, which would mean that the scrollbar | |
380 | # is no longer required. If the scrollbar is then | |
381 | # unmapped, the bottom line will then become visible | |
382 | # again, which would cause the scrollbar to be mapped | |
383 | # again, and so on... | |
384 | # | |
385 | # The idea is that, if the width of the text widget | |
386 | # has not changed and the scrollbar is currently | |
387 | # mapped, then do not unmap the scrollbar even if it | |
388 | # is no longer required. This means that, during | |
389 | # normal scrolling of the text, once the horizontal | |
390 | # scrollbar has been mapped it will not be unmapped | |
391 | # (until the width of the text widget changes). | |
392 | ||
393 | currentWidth = self._textbox.winfo_width() | |
394 | if self._horizScrollbarNeeded != self._horizScrollbarOn: | |
395 | if self._horizScrollbarNeeded or \ | |
396 | self._textWidth != currentWidth: | |
397 | self._toggleHorizScrollbar() | |
398 | self._textWidth = currentWidth | |
399 | ||
400 | if self['vscrollmode'] == 'dynamic': | |
401 | if self._vertScrollbarNeeded != self._vertScrollbarOn: | |
402 | self._toggleVertScrollbar() | |
403 | ||
404 | def _columnheaderscrolled(self, first, last): | |
405 | if self._textboxLastX != first: | |
406 | self._textboxLastX = first | |
407 | self._textbox.xview('moveto', first) | |
408 | ||
409 | def _rowheaderscrolled(self, first, last): | |
410 | if self._textboxLastY != first: | |
411 | self._textboxLastY = first | |
412 | self._textbox.yview('moveto', first) | |
413 | ||
414 | def _toggleHorizScrollbar(self): | |
415 | ||
416 | self._horizScrollbarOn = not self._horizScrollbarOn | |
417 | ||
418 | interior = self.interior() | |
419 | if self._horizScrollbarOn: | |
420 | self._horizScrollbar.grid(row = 6, column = 4, sticky = 'news') | |
421 | interior.grid_rowconfigure(5, minsize = self['scrollmargin']) | |
422 | else: | |
423 | self._horizScrollbar.grid_forget() | |
424 | interior.grid_rowconfigure(5, minsize = 0) | |
425 | ||
426 | def _toggleVertScrollbar(self): | |
427 | ||
428 | self._vertScrollbarOn = not self._vertScrollbarOn | |
429 | ||
430 | interior = self.interior() | |
431 | if self._vertScrollbarOn: | |
432 | self._vertScrollbar.grid(row = 4, column = 6, sticky = 'news') | |
433 | interior.grid_columnconfigure(5, minsize = self['scrollmargin']) | |
434 | else: | |
435 | self._vertScrollbar.grid_forget() | |
436 | interior.grid_columnconfigure(5, minsize = 0) | |
437 | ||
438 | # Need to explicitly forward this to override the stupid | |
439 | # (grid_)bbox method inherited from Tkinter.Frame.Grid. | |
440 | def bbox(self, index): | |
441 | return self._textbox.bbox(index) | |
442 | ||
443 | Pmw.forwardmethods(ScrolledText, Tkinter.Text, '_textbox') |