Commit | Line | Data |
---|---|---|
920dae64 AT |
1 | import Tkinter |
2 | import Pmw | |
3 | ||
4 | class ScrolledCanvas(Pmw.MegaWidget): | |
5 | def __init__(self, parent = None, **kw): | |
6 | ||
7 | # Define the megawidget options. | |
8 | INITOPT = Pmw.INITOPT | |
9 | optiondefs = ( | |
10 | ('borderframe', 0, INITOPT), | |
11 | ('canvasmargin', 0, INITOPT), | |
12 | ('hscrollmode', 'dynamic', self._hscrollMode), | |
13 | ('labelmargin', 0, INITOPT), | |
14 | ('labelpos', None, INITOPT), | |
15 | ('scrollmargin', 2, INITOPT), | |
16 | ('usehullsize', 0, INITOPT), | |
17 | ('vscrollmode', 'dynamic', self._vscrollMode), | |
18 | ) | |
19 | self.defineoptions(kw, optiondefs) | |
20 | ||
21 | # Initialise the base class (after defining the options). | |
22 | Pmw.MegaWidget.__init__(self, parent) | |
23 | ||
24 | # Create the components. | |
25 | self.origInterior = Pmw.MegaWidget.interior(self) | |
26 | ||
27 | if self['usehullsize']: | |
28 | self.origInterior.grid_propagate(0) | |
29 | ||
30 | if self['borderframe']: | |
31 | # Create a frame widget to act as the border of the canvas. | |
32 | self._borderframe = self.createcomponent('borderframe', | |
33 | (), None, | |
34 | Tkinter.Frame, (self.origInterior,), | |
35 | relief = 'sunken', | |
36 | borderwidth = 2, | |
37 | ) | |
38 | self._borderframe.grid(row = 2, column = 2, sticky = 'news') | |
39 | ||
40 | # Create the canvas widget. | |
41 | self._canvas = self.createcomponent('canvas', | |
42 | (), None, | |
43 | Tkinter.Canvas, (self._borderframe,), | |
44 | highlightthickness = 0, | |
45 | borderwidth = 0, | |
46 | ) | |
47 | self._canvas.pack(fill = 'both', expand = 1) | |
48 | else: | |
49 | # Create the canvas widget. | |
50 | self._canvas = self.createcomponent('canvas', | |
51 | (), None, | |
52 | Tkinter.Canvas, (self.origInterior,), | |
53 | relief = 'sunken', | |
54 | borderwidth = 2, | |
55 | ) | |
56 | self._canvas.grid(row = 2, column = 2, sticky = 'news') | |
57 | ||
58 | self.origInterior.grid_rowconfigure(2, weight = 1, minsize = 0) | |
59 | self.origInterior.grid_columnconfigure(2, weight = 1, minsize = 0) | |
60 | ||
61 | # Create the horizontal scrollbar | |
62 | self._horizScrollbar = self.createcomponent('horizscrollbar', | |
63 | (), 'Scrollbar', | |
64 | Tkinter.Scrollbar, (self.origInterior,), | |
65 | orient='horizontal', | |
66 | command=self._canvas.xview | |
67 | ) | |
68 | ||
69 | # Create the vertical scrollbar | |
70 | self._vertScrollbar = self.createcomponent('vertscrollbar', | |
71 | (), 'Scrollbar', | |
72 | Tkinter.Scrollbar, (self.origInterior,), | |
73 | orient='vertical', | |
74 | command=self._canvas.yview | |
75 | ) | |
76 | ||
77 | self.createlabel(self.origInterior, childCols = 3, childRows = 3) | |
78 | ||
79 | # Initialise instance variables. | |
80 | self._horizScrollbarOn = 0 | |
81 | self._vertScrollbarOn = 0 | |
82 | self.scrollTimer = None | |
83 | self._scrollRecurse = 0 | |
84 | self._horizScrollbarNeeded = 0 | |
85 | self._vertScrollbarNeeded = 0 | |
86 | self.setregionTimer = None | |
87 | ||
88 | # Check keywords and initialise options. | |
89 | self.initialiseoptions() | |
90 | ||
91 | def destroy(self): | |
92 | if self.scrollTimer is not None: | |
93 | self.after_cancel(self.scrollTimer) | |
94 | self.scrollTimer = None | |
95 | if self.setregionTimer is not None: | |
96 | self.after_cancel(self.setregionTimer) | |
97 | self.setregionTimer = None | |
98 | Pmw.MegaWidget.destroy(self) | |
99 | ||
100 | # ====================================================================== | |
101 | ||
102 | # Public methods. | |
103 | ||
104 | def interior(self): | |
105 | return self._canvas | |
106 | ||
107 | def resizescrollregion(self): | |
108 | if self.setregionTimer is None: | |
109 | self.setregionTimer = self.after_idle(self._setRegion) | |
110 | ||
111 | # ====================================================================== | |
112 | ||
113 | # Configuration methods. | |
114 | ||
115 | def _hscrollMode(self): | |
116 | # The horizontal scroll mode has been configured. | |
117 | ||
118 | mode = self['hscrollmode'] | |
119 | ||
120 | if mode == 'static': | |
121 | if not self._horizScrollbarOn: | |
122 | self._toggleHorizScrollbar() | |
123 | elif mode == 'dynamic': | |
124 | if self._horizScrollbarNeeded != self._horizScrollbarOn: | |
125 | self._toggleHorizScrollbar() | |
126 | elif mode == 'none': | |
127 | if self._horizScrollbarOn: | |
128 | self._toggleHorizScrollbar() | |
129 | else: | |
130 | message = 'bad hscrollmode option "%s": should be static, dynamic, or none' % mode | |
131 | raise ValueError, message | |
132 | ||
133 | self._configureScrollCommands() | |
134 | ||
135 | def _vscrollMode(self): | |
136 | # The vertical scroll mode has been configured. | |
137 | ||
138 | mode = self['vscrollmode'] | |
139 | ||
140 | if mode == 'static': | |
141 | if not self._vertScrollbarOn: | |
142 | self._toggleVertScrollbar() | |
143 | elif mode == 'dynamic': | |
144 | if self._vertScrollbarNeeded != self._vertScrollbarOn: | |
145 | self._toggleVertScrollbar() | |
146 | elif mode == 'none': | |
147 | if self._vertScrollbarOn: | |
148 | self._toggleVertScrollbar() | |
149 | else: | |
150 | message = 'bad vscrollmode option "%s": should be static, dynamic, or none' % mode | |
151 | raise ValueError, message | |
152 | ||
153 | self._configureScrollCommands() | |
154 | ||
155 | # ====================================================================== | |
156 | ||
157 | # Private methods. | |
158 | ||
159 | def _configureScrollCommands(self): | |
160 | # If both scrollmodes are not dynamic we can save a lot of | |
161 | # time by not having to create an idle job to handle the | |
162 | # scroll commands. | |
163 | ||
164 | # Clean up previous scroll commands to prevent memory leak. | |
165 | tclCommandName = str(self._canvas.cget('xscrollcommand')) | |
166 | if tclCommandName != '': | |
167 | self._canvas.deletecommand(tclCommandName) | |
168 | tclCommandName = str(self._canvas.cget('yscrollcommand')) | |
169 | if tclCommandName != '': | |
170 | self._canvas.deletecommand(tclCommandName) | |
171 | ||
172 | if self['hscrollmode'] == self['vscrollmode'] == 'dynamic': | |
173 | self._canvas.configure( | |
174 | xscrollcommand=self._scrollBothLater, | |
175 | yscrollcommand=self._scrollBothLater | |
176 | ) | |
177 | else: | |
178 | self._canvas.configure( | |
179 | xscrollcommand=self._scrollXNow, | |
180 | yscrollcommand=self._scrollYNow | |
181 | ) | |
182 | ||
183 | def _scrollXNow(self, first, last): | |
184 | self._horizScrollbar.set(first, last) | |
185 | self._horizScrollbarNeeded = ((first, last) != ('0', '1')) | |
186 | ||
187 | if self['hscrollmode'] == 'dynamic': | |
188 | if self._horizScrollbarNeeded != self._horizScrollbarOn: | |
189 | self._toggleHorizScrollbar() | |
190 | ||
191 | def _scrollYNow(self, first, last): | |
192 | self._vertScrollbar.set(first, last) | |
193 | self._vertScrollbarNeeded = ((first, last) != ('0', '1')) | |
194 | ||
195 | if self['vscrollmode'] == 'dynamic': | |
196 | if self._vertScrollbarNeeded != self._vertScrollbarOn: | |
197 | self._toggleVertScrollbar() | |
198 | ||
199 | def _scrollBothLater(self, first, last): | |
200 | # Called by the canvas to set the horizontal or vertical | |
201 | # scrollbar when it has scrolled or changed scrollregion. | |
202 | ||
203 | if self.scrollTimer is None: | |
204 | self.scrollTimer = self.after_idle(self._scrollBothNow) | |
205 | ||
206 | def _scrollBothNow(self): | |
207 | # This performs the function of _scrollXNow and _scrollYNow. | |
208 | # If one is changed, the other should be updated to match. | |
209 | self.scrollTimer = None | |
210 | ||
211 | # Call update_idletasks to make sure that the containing frame | |
212 | # has been resized before we attempt to set the scrollbars. | |
213 | # Otherwise the scrollbars may be mapped/unmapped continuously. | |
214 | self._scrollRecurse = self._scrollRecurse + 1 | |
215 | self.update_idletasks() | |
216 | self._scrollRecurse = self._scrollRecurse - 1 | |
217 | if self._scrollRecurse != 0: | |
218 | return | |
219 | ||
220 | xview = self._canvas.xview() | |
221 | yview = self._canvas.yview() | |
222 | self._horizScrollbar.set(xview[0], xview[1]) | |
223 | self._vertScrollbar.set(yview[0], yview[1]) | |
224 | ||
225 | self._horizScrollbarNeeded = (xview != (0.0, 1.0)) | |
226 | self._vertScrollbarNeeded = (yview != (0.0, 1.0)) | |
227 | ||
228 | # If both horizontal and vertical scrollmodes are dynamic and | |
229 | # currently only one scrollbar is mapped and both should be | |
230 | # toggled, then unmap the mapped scrollbar. This prevents a | |
231 | # continuous mapping and unmapping of the scrollbars. | |
232 | if (self['hscrollmode'] == self['vscrollmode'] == 'dynamic' and | |
233 | self._horizScrollbarNeeded != self._horizScrollbarOn and | |
234 | self._vertScrollbarNeeded != self._vertScrollbarOn and | |
235 | self._vertScrollbarOn != self._horizScrollbarOn): | |
236 | if self._horizScrollbarOn: | |
237 | self._toggleHorizScrollbar() | |
238 | else: | |
239 | self._toggleVertScrollbar() | |
240 | return | |
241 | ||
242 | if self['hscrollmode'] == 'dynamic': | |
243 | if self._horizScrollbarNeeded != self._horizScrollbarOn: | |
244 | self._toggleHorizScrollbar() | |
245 | ||
246 | if self['vscrollmode'] == 'dynamic': | |
247 | if self._vertScrollbarNeeded != self._vertScrollbarOn: | |
248 | self._toggleVertScrollbar() | |
249 | ||
250 | def _toggleHorizScrollbar(self): | |
251 | ||
252 | self._horizScrollbarOn = not self._horizScrollbarOn | |
253 | ||
254 | interior = self.origInterior | |
255 | if self._horizScrollbarOn: | |
256 | self._horizScrollbar.grid(row = 4, column = 2, sticky = 'news') | |
257 | interior.grid_rowconfigure(3, minsize = self['scrollmargin']) | |
258 | else: | |
259 | self._horizScrollbar.grid_forget() | |
260 | interior.grid_rowconfigure(3, minsize = 0) | |
261 | ||
262 | def _toggleVertScrollbar(self): | |
263 | ||
264 | self._vertScrollbarOn = not self._vertScrollbarOn | |
265 | ||
266 | interior = self.origInterior | |
267 | if self._vertScrollbarOn: | |
268 | self._vertScrollbar.grid(row = 2, column = 4, sticky = 'news') | |
269 | interior.grid_columnconfigure(3, minsize = self['scrollmargin']) | |
270 | else: | |
271 | self._vertScrollbar.grid_forget() | |
272 | interior.grid_columnconfigure(3, minsize = 0) | |
273 | ||
274 | def _setRegion(self): | |
275 | self.setregionTimer = None | |
276 | ||
277 | region = self._canvas.bbox('all') | |
278 | if region is not None: | |
279 | canvasmargin = self['canvasmargin'] | |
280 | region = (region[0] - canvasmargin, region[1] - canvasmargin, | |
281 | region[2] + canvasmargin, region[3] + canvasmargin) | |
282 | self._canvas.configure(scrollregion = region) | |
283 | ||
284 | # Need to explicitly forward this to override the stupid | |
285 | # (grid_)bbox method inherited from Tkinter.Frame.Grid. | |
286 | def bbox(self, *args): | |
287 | return apply(self._canvas.bbox, args) | |
288 | ||
289 | Pmw.forwardmethods(ScrolledCanvas, Tkinter.Canvas, '_canvas') |