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