Commit | Line | Data |
---|---|---|
920dae64 AT |
1 | # PanedWidget |
2 | # a frame which may contain several resizable sub-frames | |
3 | ||
4 | import string | |
5 | import sys | |
6 | import types | |
7 | import Tkinter | |
8 | import Pmw | |
9 | ||
10 | class PanedWidget(Pmw.MegaWidget): | |
11 | ||
12 | def __init__(self, parent = None, **kw): | |
13 | ||
14 | # Define the megawidget options. | |
15 | INITOPT = Pmw.INITOPT | |
16 | optiondefs = ( | |
17 | ('command', None, None), | |
18 | ('orient', 'vertical', INITOPT), | |
19 | ('separatorrelief', 'sunken', INITOPT), | |
20 | ('separatorthickness', 2, INITOPT), | |
21 | ('handlesize', 8, INITOPT), | |
22 | ('hull_width', 400, None), | |
23 | ('hull_height', 400, None), | |
24 | ) | |
25 | self.defineoptions(kw, optiondefs, | |
26 | dynamicGroups = ('Frame', 'Separator', 'Handle')) | |
27 | ||
28 | # Initialise the base class (after defining the options). | |
29 | Pmw.MegaWidget.__init__(self, parent) | |
30 | ||
31 | self.bind('<Configure>', self._handleConfigure) | |
32 | ||
33 | if self['orient'] not in ('horizontal', 'vertical'): | |
34 | raise ValueError, 'bad orient option ' + repr(self['orient']) + \ | |
35 | ': must be either \'horizontal\' or \'vertical\'' | |
36 | ||
37 | self._separatorThickness = self['separatorthickness'] | |
38 | self._handleSize = self['handlesize'] | |
39 | self._paneNames = [] # List of pane names | |
40 | self._paneAttrs = {} # Map from pane name to pane info | |
41 | ||
42 | self._timerId = None | |
43 | self._frame = {} | |
44 | self._separator = [] | |
45 | self._button = [] | |
46 | self._totalSize = 0 | |
47 | self._movePending = 0 | |
48 | self._relsize = {} | |
49 | self._relmin = {} | |
50 | self._relmax = {} | |
51 | self._size = {} | |
52 | self._min = {} | |
53 | self._max = {} | |
54 | self._rootp = None | |
55 | self._curSize = None | |
56 | self._beforeLimit = None | |
57 | self._afterLimit = None | |
58 | self._buttonIsDown = 0 | |
59 | self._majorSize = 100 | |
60 | self._minorSize = 100 | |
61 | ||
62 | # Check keywords and initialise options. | |
63 | self.initialiseoptions() | |
64 | ||
65 | def insert(self, name, before = 0, **kw): | |
66 | # Parse <kw> for options. | |
67 | self._initPaneOptions(name) | |
68 | self._parsePaneOptions(name, kw) | |
69 | ||
70 | insertPos = self._nameToIndex(before) | |
71 | atEnd = (insertPos == len(self._paneNames)) | |
72 | ||
73 | # Add the frame. | |
74 | self._paneNames[insertPos:insertPos] = [name] | |
75 | self._frame[name] = self.createcomponent(name, | |
76 | (), 'Frame', | |
77 | Tkinter.Frame, (self.interior(),)) | |
78 | ||
79 | # Add separator, if necessary. | |
80 | if len(self._paneNames) > 1: | |
81 | self._addSeparator() | |
82 | else: | |
83 | self._separator.append(None) | |
84 | self._button.append(None) | |
85 | ||
86 | # Add the new frame and adjust the PanedWidget | |
87 | if atEnd: | |
88 | size = self._size[name] | |
89 | if size > 0 or self._relsize[name] is not None: | |
90 | if self['orient'] == 'vertical': | |
91 | self._frame[name].place(x=0, relwidth=1, | |
92 | height=size, y=self._totalSize) | |
93 | else: | |
94 | self._frame[name].place(y=0, relheight=1, | |
95 | width=size, x=self._totalSize) | |
96 | else: | |
97 | if self['orient'] == 'vertical': | |
98 | self._frame[name].place(x=0, relwidth=1, | |
99 | y=self._totalSize) | |
100 | else: | |
101 | self._frame[name].place(y=0, relheight=1, | |
102 | x=self._totalSize) | |
103 | else: | |
104 | self._updateSizes() | |
105 | ||
106 | self._totalSize = self._totalSize + self._size[name] | |
107 | return self._frame[name] | |
108 | ||
109 | def add(self, name, **kw): | |
110 | return apply(self.insert, (name, len(self._paneNames)), kw) | |
111 | ||
112 | def delete(self, name): | |
113 | deletePos = self._nameToIndex(name) | |
114 | name = self._paneNames[deletePos] | |
115 | self.destroycomponent(name) | |
116 | del self._paneNames[deletePos] | |
117 | del self._frame[name] | |
118 | del self._size[name] | |
119 | del self._min[name] | |
120 | del self._max[name] | |
121 | del self._relsize[name] | |
122 | del self._relmin[name] | |
123 | del self._relmax[name] | |
124 | ||
125 | last = len(self._paneNames) | |
126 | del self._separator[last] | |
127 | del self._button[last] | |
128 | if last > 0: | |
129 | self.destroycomponent(self._sepName(last)) | |
130 | self.destroycomponent(self._buttonName(last)) | |
131 | ||
132 | self._plotHandles() | |
133 | ||
134 | def setnaturalsize(self): | |
135 | self.update_idletasks() | |
136 | totalWidth = 0 | |
137 | totalHeight = 0 | |
138 | maxWidth = 0 | |
139 | maxHeight = 0 | |
140 | for name in self._paneNames: | |
141 | frame = self._frame[name] | |
142 | w = frame.winfo_reqwidth() | |
143 | h = frame.winfo_reqheight() | |
144 | totalWidth = totalWidth + w | |
145 | totalHeight = totalHeight + h | |
146 | if maxWidth < w: | |
147 | maxWidth = w | |
148 | if maxHeight < h: | |
149 | maxHeight = h | |
150 | ||
151 | # Note that, since the hull is a frame, the width and height | |
152 | # options specify the geometry *outside* the borderwidth and | |
153 | # highlightthickness. | |
154 | bw = string.atoi(str(self.cget('hull_borderwidth'))) | |
155 | hl = string.atoi(str(self.cget('hull_highlightthickness'))) | |
156 | extra = (bw + hl) * 2 | |
157 | if str(self.cget('orient')) == 'horizontal': | |
158 | totalWidth = totalWidth + extra | |
159 | maxHeight = maxHeight + extra | |
160 | self.configure(hull_width = totalWidth, hull_height = maxHeight) | |
161 | else: | |
162 | totalHeight = (totalHeight + extra + | |
163 | (len(self._paneNames) - 1) * self._separatorThickness) | |
164 | maxWidth = maxWidth + extra | |
165 | self.configure(hull_width = maxWidth, hull_height = totalHeight) | |
166 | ||
167 | def move(self, name, newPos, newPosOffset = 0): | |
168 | ||
169 | # see if we can spare ourselves some work | |
170 | numPanes = len(self._paneNames) | |
171 | if numPanes < 2: | |
172 | return | |
173 | ||
174 | newPos = self._nameToIndex(newPos) + newPosOffset | |
175 | if newPos < 0 or newPos >=numPanes: | |
176 | return | |
177 | ||
178 | deletePos = self._nameToIndex(name) | |
179 | ||
180 | if deletePos == newPos: | |
181 | # inserting over ourself is a no-op | |
182 | return | |
183 | ||
184 | # delete name from old position in list | |
185 | name = self._paneNames[deletePos] | |
186 | del self._paneNames[deletePos] | |
187 | ||
188 | # place in new position | |
189 | self._paneNames[newPos:newPos] = [name] | |
190 | ||
191 | # force everything to redraw | |
192 | self._plotHandles() | |
193 | self._updateSizes() | |
194 | ||
195 | def _nameToIndex(self, nameOrIndex): | |
196 | try: | |
197 | pos = self._paneNames.index(nameOrIndex) | |
198 | except ValueError: | |
199 | pos = nameOrIndex | |
200 | ||
201 | return pos | |
202 | ||
203 | def _initPaneOptions(self, name): | |
204 | # Set defaults. | |
205 | self._size[name] = 0 | |
206 | self._relsize[name] = None | |
207 | self._min[name] = 0 | |
208 | self._relmin[name] = None | |
209 | self._max[name] = 100000 | |
210 | self._relmax[name] = None | |
211 | ||
212 | def _parsePaneOptions(self, name, args): | |
213 | # Parse <args> for options. | |
214 | for arg, value in args.items(): | |
215 | if type(value) == types.FloatType: | |
216 | relvalue = value | |
217 | value = self._absSize(relvalue) | |
218 | else: | |
219 | relvalue = None | |
220 | ||
221 | if arg == 'size': | |
222 | self._size[name], self._relsize[name] = value, relvalue | |
223 | elif arg == 'min': | |
224 | self._min[name], self._relmin[name] = value, relvalue | |
225 | elif arg == 'max': | |
226 | self._max[name], self._relmax[name] = value, relvalue | |
227 | else: | |
228 | raise ValueError, 'keyword must be "size", "min", or "max"' | |
229 | ||
230 | def _absSize(self, relvalue): | |
231 | return int(round(relvalue * self._majorSize)) | |
232 | ||
233 | def _sepName(self, n): | |
234 | return 'separator-%d' % n | |
235 | ||
236 | def _buttonName(self, n): | |
237 | return 'handle-%d' % n | |
238 | ||
239 | def _addSeparator(self): | |
240 | n = len(self._paneNames) - 1 | |
241 | ||
242 | downFunc = lambda event, s = self, num=n: s._btnDown(event, num) | |
243 | upFunc = lambda event, s = self, num=n: s._btnUp(event, num) | |
244 | moveFunc = lambda event, s = self, num=n: s._btnMove(event, num) | |
245 | ||
246 | # Create the line dividing the panes. | |
247 | sep = self.createcomponent(self._sepName(n), | |
248 | (), 'Separator', | |
249 | Tkinter.Frame, (self.interior(),), | |
250 | borderwidth = 1, | |
251 | relief = self['separatorrelief']) | |
252 | self._separator.append(sep) | |
253 | ||
254 | sep.bind('<ButtonPress-1>', downFunc) | |
255 | sep.bind('<Any-ButtonRelease-1>', upFunc) | |
256 | sep.bind('<B1-Motion>', moveFunc) | |
257 | ||
258 | if self['orient'] == 'vertical': | |
259 | cursor = 'sb_v_double_arrow' | |
260 | sep.configure(height = self._separatorThickness, | |
261 | width = 10000, cursor = cursor) | |
262 | else: | |
263 | cursor = 'sb_h_double_arrow' | |
264 | sep.configure(width = self._separatorThickness, | |
265 | height = 10000, cursor = cursor) | |
266 | ||
267 | self._totalSize = self._totalSize + self._separatorThickness | |
268 | ||
269 | # Create the handle on the dividing line. | |
270 | handle = self.createcomponent(self._buttonName(n), | |
271 | (), 'Handle', | |
272 | Tkinter.Frame, (self.interior(),), | |
273 | relief = 'raised', | |
274 | borderwidth = 1, | |
275 | width = self._handleSize, | |
276 | height = self._handleSize, | |
277 | cursor = cursor, | |
278 | ) | |
279 | self._button.append(handle) | |
280 | ||
281 | handle.bind('<ButtonPress-1>', downFunc) | |
282 | handle.bind('<Any-ButtonRelease-1>', upFunc) | |
283 | handle.bind('<B1-Motion>', moveFunc) | |
284 | ||
285 | self._plotHandles() | |
286 | ||
287 | for i in range(1, len(self._paneNames)): | |
288 | self._separator[i].tkraise() | |
289 | for i in range(1, len(self._paneNames)): | |
290 | self._button[i].tkraise() | |
291 | ||
292 | def _btnUp(self, event, item): | |
293 | self._buttonIsDown = 0 | |
294 | self._updateSizes() | |
295 | try: | |
296 | self._button[item].configure(relief='raised') | |
297 | except: | |
298 | pass | |
299 | ||
300 | def _btnDown(self, event, item): | |
301 | self._button[item].configure(relief='sunken') | |
302 | self._getMotionLimit(item) | |
303 | self._buttonIsDown = 1 | |
304 | self._movePending = 0 | |
305 | ||
306 | def _handleConfigure(self, event = None): | |
307 | self._getNaturalSizes() | |
308 | if self._totalSize == 0: | |
309 | return | |
310 | ||
311 | iterRange = list(self._paneNames) | |
312 | iterRange.reverse() | |
313 | if self._majorSize > self._totalSize: | |
314 | n = self._majorSize - self._totalSize | |
315 | self._iterate(iterRange, self._grow, n) | |
316 | elif self._majorSize < self._totalSize: | |
317 | n = self._totalSize - self._majorSize | |
318 | self._iterate(iterRange, self._shrink, n) | |
319 | ||
320 | self._plotHandles() | |
321 | self._updateSizes() | |
322 | ||
323 | def _getNaturalSizes(self): | |
324 | # Must call this in order to get correct winfo_width, winfo_height | |
325 | self.update_idletasks() | |
326 | ||
327 | self._totalSize = 0 | |
328 | ||
329 | if self['orient'] == 'vertical': | |
330 | self._majorSize = self.winfo_height() | |
331 | self._minorSize = self.winfo_width() | |
332 | majorspec = Tkinter.Frame.winfo_reqheight | |
333 | else: | |
334 | self._majorSize = self.winfo_width() | |
335 | self._minorSize = self.winfo_height() | |
336 | majorspec = Tkinter.Frame.winfo_reqwidth | |
337 | ||
338 | bw = string.atoi(str(self.cget('hull_borderwidth'))) | |
339 | hl = string.atoi(str(self.cget('hull_highlightthickness'))) | |
340 | extra = (bw + hl) * 2 | |
341 | self._majorSize = self._majorSize - extra | |
342 | self._minorSize = self._minorSize - extra | |
343 | ||
344 | if self._majorSize < 0: | |
345 | self._majorSize = 0 | |
346 | if self._minorSize < 0: | |
347 | self._minorSize = 0 | |
348 | ||
349 | for name in self._paneNames: | |
350 | # adjust the absolute sizes first... | |
351 | if self._relsize[name] is None: | |
352 | #special case | |
353 | if self._size[name] == 0: | |
354 | self._size[name] = apply(majorspec, (self._frame[name],)) | |
355 | self._setrel(name) | |
356 | else: | |
357 | self._size[name] = self._absSize(self._relsize[name]) | |
358 | ||
359 | if self._relmin[name] is not None: | |
360 | self._min[name] = self._absSize(self._relmin[name]) | |
361 | if self._relmax[name] is not None: | |
362 | self._max[name] = self._absSize(self._relmax[name]) | |
363 | ||
364 | # now adjust sizes | |
365 | if self._size[name] < self._min[name]: | |
366 | self._size[name] = self._min[name] | |
367 | self._setrel(name) | |
368 | ||
369 | if self._size[name] > self._max[name]: | |
370 | self._size[name] = self._max[name] | |
371 | self._setrel(name) | |
372 | ||
373 | self._totalSize = self._totalSize + self._size[name] | |
374 | ||
375 | # adjust for separators | |
376 | self._totalSize = (self._totalSize + | |
377 | (len(self._paneNames) - 1) * self._separatorThickness) | |
378 | ||
379 | def _setrel(self, name): | |
380 | if self._relsize[name] is not None: | |
381 | if self._majorSize != 0: | |
382 | self._relsize[name] = round(self._size[name]) / self._majorSize | |
383 | ||
384 | def _iterate(self, names, proc, n): | |
385 | for i in names: | |
386 | n = apply(proc, (i, n)) | |
387 | if n == 0: | |
388 | break | |
389 | ||
390 | def _grow(self, name, n): | |
391 | canGrow = self._max[name] - self._size[name] | |
392 | ||
393 | if canGrow > n: | |
394 | self._size[name] = self._size[name] + n | |
395 | self._setrel(name) | |
396 | return 0 | |
397 | elif canGrow > 0: | |
398 | self._size[name] = self._max[name] | |
399 | self._setrel(name) | |
400 | n = n - canGrow | |
401 | ||
402 | return n | |
403 | ||
404 | def _shrink(self, name, n): | |
405 | canShrink = self._size[name] - self._min[name] | |
406 | ||
407 | if canShrink > n: | |
408 | self._size[name] = self._size[name] - n | |
409 | self._setrel(name) | |
410 | return 0 | |
411 | elif canShrink > 0: | |
412 | self._size[name] = self._min[name] | |
413 | self._setrel(name) | |
414 | n = n - canShrink | |
415 | ||
416 | return n | |
417 | ||
418 | def _updateSizes(self): | |
419 | totalSize = 0 | |
420 | ||
421 | for name in self._paneNames: | |
422 | size = self._size[name] | |
423 | if self['orient'] == 'vertical': | |
424 | self._frame[name].place(x = 0, relwidth = 1, | |
425 | y = totalSize, | |
426 | height = size) | |
427 | else: | |
428 | self._frame[name].place(y = 0, relheight = 1, | |
429 | x = totalSize, | |
430 | width = size) | |
431 | ||
432 | totalSize = totalSize + size + self._separatorThickness | |
433 | ||
434 | # Invoke the callback command | |
435 | cmd = self['command'] | |
436 | if callable(cmd): | |
437 | cmd(map(lambda x, s = self: s._size[x], self._paneNames)) | |
438 | ||
439 | def _plotHandles(self): | |
440 | if len(self._paneNames) == 0: | |
441 | return | |
442 | ||
443 | if self['orient'] == 'vertical': | |
444 | btnp = self._minorSize - 13 | |
445 | else: | |
446 | h = self._minorSize | |
447 | ||
448 | if h > 18: | |
449 | btnp = 9 | |
450 | else: | |
451 | btnp = h - 9 | |
452 | ||
453 | firstPane = self._paneNames[0] | |
454 | totalSize = self._size[firstPane] | |
455 | ||
456 | first = 1 | |
457 | last = len(self._paneNames) - 1 | |
458 | ||
459 | # loop from first to last, inclusive | |
460 | for i in range(1, last + 1): | |
461 | ||
462 | handlepos = totalSize - 3 | |
463 | prevSize = self._size[self._paneNames[i - 1]] | |
464 | nextSize = self._size[self._paneNames[i]] | |
465 | ||
466 | offset1 = 0 | |
467 | ||
468 | if i == first: | |
469 | if prevSize < 4: | |
470 | offset1 = 4 - prevSize | |
471 | else: | |
472 | if prevSize < 8: | |
473 | offset1 = (8 - prevSize) / 2 | |
474 | ||
475 | offset2 = 0 | |
476 | ||
477 | if i == last: | |
478 | if nextSize < 4: | |
479 | offset2 = nextSize - 4 | |
480 | else: | |
481 | if nextSize < 8: | |
482 | offset2 = (nextSize - 8) / 2 | |
483 | ||
484 | handlepos = handlepos + offset1 | |
485 | ||
486 | if self['orient'] == 'vertical': | |
487 | height = 8 - offset1 + offset2 | |
488 | ||
489 | if height > 1: | |
490 | self._button[i].configure(height = height) | |
491 | self._button[i].place(x = btnp, y = handlepos) | |
492 | else: | |
493 | self._button[i].place_forget() | |
494 | ||
495 | self._separator[i].place(x = 0, y = totalSize, | |
496 | relwidth = 1) | |
497 | else: | |
498 | width = 8 - offset1 + offset2 | |
499 | ||
500 | if width > 1: | |
501 | self._button[i].configure(width = width) | |
502 | self._button[i].place(y = btnp, x = handlepos) | |
503 | else: | |
504 | self._button[i].place_forget() | |
505 | ||
506 | self._separator[i].place(y = 0, x = totalSize, | |
507 | relheight = 1) | |
508 | ||
509 | totalSize = totalSize + nextSize + self._separatorThickness | |
510 | ||
511 | def pane(self, name): | |
512 | return self._frame[self._paneNames[self._nameToIndex(name)]] | |
513 | ||
514 | # Return the name of all panes | |
515 | def panes(self): | |
516 | return list(self._paneNames) | |
517 | ||
518 | def configurepane(self, name, **kw): | |
519 | name = self._paneNames[self._nameToIndex(name)] | |
520 | self._parsePaneOptions(name, kw) | |
521 | self._handleConfigure() | |
522 | ||
523 | def updatelayout(self): | |
524 | self._handleConfigure() | |
525 | ||
526 | def _getMotionLimit(self, item): | |
527 | curBefore = (item - 1) * self._separatorThickness | |
528 | minBefore, maxBefore = curBefore, curBefore | |
529 | ||
530 | for name in self._paneNames[:item]: | |
531 | curBefore = curBefore + self._size[name] | |
532 | minBefore = minBefore + self._min[name] | |
533 | maxBefore = maxBefore + self._max[name] | |
534 | ||
535 | curAfter = (len(self._paneNames) - item) * self._separatorThickness | |
536 | minAfter, maxAfter = curAfter, curAfter | |
537 | for name in self._paneNames[item:]: | |
538 | curAfter = curAfter + self._size[name] | |
539 | minAfter = minAfter + self._min[name] | |
540 | maxAfter = maxAfter + self._max[name] | |
541 | ||
542 | beforeToGo = min(curBefore - minBefore, maxAfter - curAfter) | |
543 | afterToGo = min(curAfter - minAfter, maxBefore - curBefore) | |
544 | ||
545 | self._beforeLimit = curBefore - beforeToGo | |
546 | self._afterLimit = curBefore + afterToGo | |
547 | self._curSize = curBefore | |
548 | ||
549 | self._plotHandles() | |
550 | ||
551 | # Compress the motion so that update is quick even on slow machines | |
552 | # | |
553 | # theRootp = root position (either rootx or rooty) | |
554 | def _btnMove(self, event, item): | |
555 | self._rootp = event | |
556 | ||
557 | if self._movePending == 0: | |
558 | self._timerId = self.after_idle( | |
559 | lambda s = self, i = item: s._btnMoveCompressed(i)) | |
560 | self._movePending = 1 | |
561 | ||
562 | def destroy(self): | |
563 | if self._timerId is not None: | |
564 | self.after_cancel(self._timerId) | |
565 | self._timerId = None | |
566 | Pmw.MegaWidget.destroy(self) | |
567 | ||
568 | def _btnMoveCompressed(self, item): | |
569 | if not self._buttonIsDown: | |
570 | return | |
571 | ||
572 | if self['orient'] == 'vertical': | |
573 | p = self._rootp.y_root - self.winfo_rooty() | |
574 | else: | |
575 | p = self._rootp.x_root - self.winfo_rootx() | |
576 | ||
577 | if p == self._curSize: | |
578 | self._movePending = 0 | |
579 | return | |
580 | ||
581 | if p < self._beforeLimit: | |
582 | p = self._beforeLimit | |
583 | ||
584 | if p >= self._afterLimit: | |
585 | p = self._afterLimit | |
586 | ||
587 | self._calculateChange(item, p) | |
588 | self.update_idletasks() | |
589 | self._movePending = 0 | |
590 | ||
591 | # Calculate the change in response to mouse motions | |
592 | def _calculateChange(self, item, p): | |
593 | ||
594 | if p < self._curSize: | |
595 | self._moveBefore(item, p) | |
596 | elif p > self._curSize: | |
597 | self._moveAfter(item, p) | |
598 | ||
599 | self._plotHandles() | |
600 | ||
601 | def _moveBefore(self, item, p): | |
602 | n = self._curSize - p | |
603 | ||
604 | # Shrink the frames before | |
605 | iterRange = list(self._paneNames[:item]) | |
606 | iterRange.reverse() | |
607 | self._iterate(iterRange, self._shrink, n) | |
608 | ||
609 | # Adjust the frames after | |
610 | iterRange = self._paneNames[item:] | |
611 | self._iterate(iterRange, self._grow, n) | |
612 | ||
613 | self._curSize = p | |
614 | ||
615 | def _moveAfter(self, item, p): | |
616 | n = p - self._curSize | |
617 | ||
618 | # Shrink the frames after | |
619 | iterRange = self._paneNames[item:] | |
620 | self._iterate(iterRange, self._shrink, n) | |
621 | ||
622 | # Adjust the frames before | |
623 | iterRange = list(self._paneNames[:item]) | |
624 | iterRange.reverse() | |
625 | self._iterate(iterRange, self._grow, n) | |
626 | ||
627 | self._curSize = p |