# PanedWidget
# a frame which may contain several resizable sub-frames

import string
import sys
import types
import tkinter
import Pmw
import collections

class PanedWidget(Pmw.MegaWidget):

    def __init__(self, parent = None, **kw):

        # Define the megawidget options.
        INITOPT = Pmw.INITOPT
        optiondefs = (
            ('command',            None,         None),
            ('orient',             'vertical',   INITOPT),
            ('separatorrelief',    'sunken',     INITOPT),
            ('separatorthickness', 2,            INITOPT),
            ('handlesize',         8,            INITOPT),
            ('hull_width',         400,          None),
            ('hull_height',        400,          None),
        )
        self.defineoptions(kw, optiondefs,
                dynamicGroups = ('Frame', 'Separator', 'Handle'))

        # Initialise the base class (after defining the options).
        Pmw.MegaWidget.__init__(self, parent)

        self.bind('<Configure>', self._handleConfigure)

        if self['orient'] not in ('horizontal', 'vertical'):
            raise ValueError('bad orient option ' + repr(self['orient']) + \
                ': must be either \'horizontal\' or \'vertical\'')

        self._separatorThickness = self['separatorthickness']
        self._handleSize = self['handlesize']
        self._paneNames = []            # List of pane names
        self._paneAttrs = {}            # Map from pane name to pane info

        self._timerId = None
        self._frame = {}
        self._separator = []
        self._button = []
        self._totalSize = 0
        self._movePending = 0
        self._relsize = {}
        self._relmin = {}
        self._relmax = {}
        self._size = {}
        self._min = {}
        self._max = {}
        self._rootp = None
        self._curSize = None
        self._beforeLimit = None
        self._afterLimit = None
        self._buttonIsDown = 0
        self._majorSize = 100
        self._minorSize = 100

        # Check keywords and initialise options.
        self.initialiseoptions()

    def insert(self, name, before = 0, **kw):
        # Parse <kw> for options.
        self._initPaneOptions(name)
        self._parsePaneOptions(name, kw)

        insertPos = self._nameToIndex(before)
        atEnd = (insertPos == len(self._paneNames))

        # Add the frame.
        self._paneNames[insertPos:insertPos] = [name]
        self._frame[name] = self.createcomponent(name,
                (), 'Frame',
                tkinter.Frame, (self.interior(),))

        # Add separator, if necessary.
        if len(self._paneNames) > 1:
            self._addSeparator()
        else:
            self._separator.append(None)
            self._button.append(None)

        # Add the new frame and adjust the PanedWidget
        if atEnd:
            size = self._size[name]
            if size > 0 or self._relsize[name] is not None:
                if self['orient'] == 'vertical':
                    self._frame[name].place(x=0, relwidth=1,
                                            height=size, y=self._totalSize)
                else:
                    self._frame[name].place(y=0, relheight=1,
                                            width=size, x=self._totalSize)
            else:
                if self['orient'] == 'vertical':
                    self._frame[name].place(x=0, relwidth=1,
                                            y=self._totalSize)
                else:
                    self._frame[name].place(y=0, relheight=1,
                                            x=self._totalSize)
        else:
            self._updateSizes()

        self._totalSize = self._totalSize + self._size[name]
        return self._frame[name]

    def add(self, name, **kw):
        return self.insert(*(name, len(self._paneNames)), **kw)

    def delete(self, name):
        deletePos = self._nameToIndex(name)
        name = self._paneNames[deletePos]
        self.destroycomponent(name)
        del self._paneNames[deletePos]
        del self._frame[name]
        del self._size[name]
        del self._min[name]
        del self._max[name]
        del self._relsize[name]
        del self._relmin[name]
        del self._relmax[name]

        last = len(self._paneNames)
        del self._separator[last]
        del self._button[last]
        if last > 0:
            self.destroycomponent(self._sepName(last))
            self.destroycomponent(self._buttonName(last))

        self._plotHandles()

    def setnaturalsize(self):
        self.update_idletasks()
        totalWidth = 0
        totalHeight = 0
        maxWidth = 0
        maxHeight = 0
        for name in self._paneNames:
            frame = self._frame[name]
            w = frame.winfo_reqwidth()
            h = frame.winfo_reqheight()
            totalWidth = totalWidth + w
            totalHeight = totalHeight + h
            if maxWidth < w:
                maxWidth = w
            if maxHeight < h:
                maxHeight = h

        # Note that, since the hull is a frame, the width and height
        # options specify the geometry *outside* the borderwidth and
        # highlightthickness.
        
        bw = int(str(self.cget('hull_borderwidth')))
        hl = int(str(self.cget('hull_highlightthickness')))
        extra = (bw + hl) * 2
        if str(self.cget('orient')) == 'horizontal':
            totalWidth = totalWidth + extra
            maxHeight = maxHeight + extra
            self.configure(hull_width = totalWidth, hull_height = maxHeight)
        else:
            totalHeight = (totalHeight + extra +
                    (len(self._paneNames) - 1) * self._separatorThickness)
            maxWidth = maxWidth + extra
            self.configure(hull_width = maxWidth, hull_height = totalHeight)

    def move(self, name, newPos, newPosOffset = 0):

        # see if we can spare ourselves some work
        numPanes = len(self._paneNames)
        if numPanes < 2:
            return

        newPos = self._nameToIndex(newPos) + newPosOffset
        if newPos < 0 or newPos >=numPanes:
            return

        deletePos = self._nameToIndex(name)

        if deletePos == newPos:
            # inserting over ourself is a no-op
            return

        # delete name from old position in list
        name = self._paneNames[deletePos]
        del self._paneNames[deletePos]

        # place in new position
        self._paneNames[newPos:newPos] = [name]

        # force everything to redraw
        self._plotHandles()
        self._updateSizes()

    def _nameToIndex(self, nameOrIndex):
        try:
            pos = self._paneNames.index(nameOrIndex)
        except ValueError:
            pos = nameOrIndex

        return pos

    def _initPaneOptions(self, name):
        # Set defaults.
        self._size[name] = 0
        self._relsize[name] = None
        self._min[name] = 0
        self._relmin[name] = None
        self._max[name] = 100000
        self._relmax[name] = None

    def _parsePaneOptions(self, name, args):
        # Parse <args> for options.
        for arg, value in list(args.items()):
            if type(value) == float:
                relvalue = value
                value = self._absSize(relvalue)
            else:
                relvalue = None

            if arg == 'size':
                self._size[name], self._relsize[name] = value, relvalue
            elif arg == 'min':
                self._min[name], self._relmin[name] = value, relvalue
            elif arg == 'max':
                self._max[name], self._relmax[name] = value, relvalue
            else:
                raise ValueError('keyword must be "size", "min", or "max"')

    def _absSize(self, relvalue):
        return int(round(relvalue * self._majorSize))

    def _sepName(self, n):
        return 'separator-%d' % n

    def _buttonName(self, n):
        return 'handle-%d' % n

    def _addSeparator(self):
        n = len(self._paneNames) - 1

        downFunc = lambda event, s = self, num=n: s._btnDown(event, num)
        upFunc = lambda event, s = self, num=n: s._btnUp(event, num)
        moveFunc = lambda event, s = self, num=n: s._btnMove(event, num)

        # Create the line dividing the panes.
        sep = self.createcomponent(self._sepName(n),
                (), 'Separator',
                tkinter.Frame, (self.interior(),),
                borderwidth = 1,
                relief = self['separatorrelief'])
        self._separator.append(sep)

        sep.bind('<ButtonPress-1>', downFunc)
        sep.bind('<Any-ButtonRelease-1>', upFunc)
        sep.bind('<B1-Motion>', moveFunc)

        if self['orient'] == 'vertical':
            cursor = 'sb_v_double_arrow'
            sep.configure(height = self._separatorThickness,
                    width = 10000, cursor = cursor)
        else:
            cursor = 'sb_h_double_arrow'
            sep.configure(width = self._separatorThickness,
                    height = 10000, cursor = cursor)

        self._totalSize = self._totalSize + self._separatorThickness

        # Create the handle on the dividing line.
        handle = self.createcomponent(self._buttonName(n),
                (), 'Handle',
                tkinter.Frame, (self.interior(),),
                    relief = 'raised',
                    borderwidth = 1,
                    width = self._handleSize,
                    height = self._handleSize,
                    cursor = cursor,
                )
        self._button.append(handle)

        handle.bind('<ButtonPress-1>', downFunc)
        handle.bind('<Any-ButtonRelease-1>', upFunc)
        handle.bind('<B1-Motion>', moveFunc)

        self._plotHandles()

        for i in range(1, len(self._paneNames)):
            self._separator[i].tkraise()
        for i in range(1, len(self._paneNames)):
            self._button[i].tkraise()

    def _btnUp(self, event, item):
        self._buttonIsDown = 0
        self._updateSizes()
        try:
            self._button[item].configure(relief='raised')
        except:
            pass

    def _btnDown(self, event, item):
        self._button[item].configure(relief='sunken')
        self._getMotionLimit(item)
        self._buttonIsDown = 1
        self._movePending = 0

    def _handleConfigure(self, event = None):
        self._getNaturalSizes()
        if self._totalSize == 0:
            return

        iterRange = list(self._paneNames)
        iterRange.reverse()
        if self._majorSize > self._totalSize:
            n = self._majorSize - self._totalSize
            self._iterate(iterRange, self._grow, n)
        elif self._majorSize < self._totalSize:
            n = self._totalSize - self._majorSize
            self._iterate(iterRange, self._shrink, n)

        self._plotHandles()
        self._updateSizes()

    def _getNaturalSizes(self):
        # Must call this in order to get correct winfo_width, winfo_height
        self.update_idletasks()

        self._totalSize = 0

        if self['orient'] == 'vertical':
            self._majorSize = self.winfo_height()
            self._minorSize = self.winfo_width()
            majorspec = tkinter.Frame.winfo_reqheight
        else:
            self._majorSize = self.winfo_width()
            self._minorSize = self.winfo_height()
            majorspec = tkinter.Frame.winfo_reqwidth

        bw = int(str(self.cget('hull_borderwidth')))
        hl = int(str(self.cget('hull_highlightthickness')))
        extra = (bw + hl) * 2
        self._majorSize = self._majorSize - extra
        self._minorSize = self._minorSize - extra

        if self._majorSize < 0:
            self._majorSize = 0
        if self._minorSize < 0:
            self._minorSize = 0

        for name in self._paneNames:
            # adjust the absolute sizes first...
            if self._relsize[name] is None:
                #special case
                if self._size[name] == 0:
                    self._size[name] = majorspec(*(self._frame[name],))
                    self._setrel(name)
            else:
                self._size[name] = self._absSize(self._relsize[name])

            if self._relmin[name] is not None:
                self._min[name] = self._absSize(self._relmin[name])
            if self._relmax[name] is not None:
                self._max[name] = self._absSize(self._relmax[name])

            # now adjust sizes
            if self._size[name] < self._min[name]:
                self._size[name] = self._min[name]
                self._setrel(name)

            if self._size[name] > self._max[name]:
                self._size[name] = self._max[name]
                self._setrel(name)

            self._totalSize = self._totalSize + self._size[name]

        # adjust for separators
        self._totalSize = (self._totalSize +
                (len(self._paneNames) - 1) * self._separatorThickness)

    def _setrel(self, name):
        if self._relsize[name] is not None:
            if self._majorSize != 0:
                self._relsize[name] = round(self._size[name]) / self._majorSize

    def _iterate(self, names, proc, n):
        for i in names:
            n = proc(*(i, n))
            if n == 0:
                break

    def _grow(self, name, n):
        canGrow = self._max[name] - self._size[name]

        if canGrow > n:
            self._size[name] = self._size[name] + n
            self._setrel(name)
            return 0
        elif canGrow > 0:
            self._size[name] = self._max[name]
            self._setrel(name)
            n = n - canGrow

        return n

    def _shrink(self, name, n):
        canShrink = self._size[name] - self._min[name]

        if canShrink > n:
            self._size[name] = self._size[name] - n
            self._setrel(name)
            return 0
        elif canShrink > 0:
            self._size[name] = self._min[name]
            self._setrel(name)
            n = n - canShrink

        return n

    def _updateSizes(self):
        totalSize = 0

        for name in self._paneNames:
            size = self._size[name]
            if self['orient'] == 'vertical':
                self._frame[name].place(x = 0, relwidth = 1,
                                        y = totalSize,
                                        height = size)
            else:
                self._frame[name].place(y = 0, relheight = 1,
                                        x = totalSize,
                                        width = size)

            totalSize = totalSize + size + self._separatorThickness

        # Invoke the callback command
        cmd = self['command']
        if hasattr(cmd, '__call__'):
            cmd(list(map(lambda x, s = self: s._size[x], self._paneNames)))

    def _plotHandles(self):
        if len(self._paneNames) == 0:
            return

        if self['orient'] == 'vertical':
            btnp = self._minorSize - 13
        else:
            h = self._minorSize

            if h > 18:
                btnp = 9
            else:
                btnp = h - 9

        firstPane = self._paneNames[0]
        totalSize = self._size[firstPane]

        first = 1
        last = len(self._paneNames) - 1

        # loop from first to last, inclusive
        for i in range(1, last + 1):

            handlepos = totalSize - 3
            prevSize = self._size[self._paneNames[i - 1]]
            nextSize = self._size[self._paneNames[i]]

            offset1 = 0

            if i == first:
                if prevSize < 4:
                    offset1 = 4 - prevSize
            else:
                if prevSize < 8:
                    offset1 = (8 - prevSize) / 2

            offset2 = 0

            if i == last:
                if nextSize < 4:
                    offset2 = nextSize - 4
            else:
                if nextSize < 8:
                    offset2 = (nextSize - 8) / 2

            handlepos = handlepos + offset1

            if self['orient'] == 'vertical':
                height = 8 - offset1 + offset2

                if height > 1:
                    self._button[i].configure(height = height)
                    self._button[i].place(x = btnp, y = handlepos)
                else:
                    self._button[i].place_forget()

                self._separator[i].place(x = 0, y = totalSize,
                                         relwidth = 1)
            else:
                width = 8 - offset1 + offset2

                if width > 1:
                    self._button[i].configure(width = width)
                    self._button[i].place(y = btnp, x = handlepos)
                else:
                    self._button[i].place_forget()

                self._separator[i].place(y = 0, x = totalSize,
                                         relheight = 1)

            totalSize = totalSize + nextSize + self._separatorThickness

    def pane(self, name):
        return self._frame[self._paneNames[self._nameToIndex(name)]]

    # Return the name of all panes
    def panes(self):
        return list(self._paneNames)

    def configurepane(self, name, **kw):
        name = self._paneNames[self._nameToIndex(name)]
        self._parsePaneOptions(name, kw)
        self._handleConfigure()

    def updatelayout(self):
        self._handleConfigure()

    def _getMotionLimit(self, item):
        curBefore = (item - 1) * self._separatorThickness
        minBefore, maxBefore = curBefore, curBefore

        for name in self._paneNames[:item]:
            curBefore = curBefore + self._size[name]
            minBefore = minBefore + self._min[name]
            maxBefore = maxBefore + self._max[name]

        curAfter = (len(self._paneNames) - item) * self._separatorThickness
        minAfter, maxAfter = curAfter, curAfter
        for name in self._paneNames[item:]:
            curAfter = curAfter + self._size[name]
            minAfter = minAfter + self._min[name]
            maxAfter = maxAfter + self._max[name]

        beforeToGo = min(curBefore - minBefore, maxAfter - curAfter)
        afterToGo = min(curAfter - minAfter, maxBefore - curBefore)

        self._beforeLimit = curBefore - beforeToGo
        self._afterLimit = curBefore + afterToGo
        self._curSize = curBefore

        self._plotHandles()

    # Compress the motion so that update is quick even on slow machines
    #
    # theRootp = root position (either rootx or rooty)
    def _btnMove(self, event, item):
        self._rootp = event

        if self._movePending == 0:
            self._timerId = self.after_idle(
                    lambda s = self, i = item: s._btnMoveCompressed(i))
            self._movePending = 1

    def destroy(self):
        if self._timerId is not None:
            self.after_cancel(self._timerId)
            self._timerId = None
        Pmw.MegaWidget.destroy(self)

    def _btnMoveCompressed(self, item):
        if not self._buttonIsDown:
            return

        if self['orient'] == 'vertical':
            p = self._rootp.y_root - self.winfo_rooty()
        else:
            p = self._rootp.x_root - self.winfo_rootx()

        if p == self._curSize:
            self._movePending = 0
            return

        if p < self._beforeLimit:
            p = self._beforeLimit

        if p >= self._afterLimit:
            p = self._afterLimit

        self._calculateChange(item, p)
        self.update_idletasks()
        self._movePending = 0

    # Calculate the change in response to mouse motions
    def _calculateChange(self, item, p):

        if p < self._curSize:
            self._moveBefore(item, p)
        elif p > self._curSize:
            self._moveAfter(item, p)

        self._plotHandles()

    def _moveBefore(self, item, p):
        n = self._curSize - p

        # Shrink the frames before
        iterRange = list(self._paneNames[:item])
        iterRange.reverse()
        self._iterate(iterRange, self._shrink, n)

        # Adjust the frames after
        iterRange = self._paneNames[item:]
        self._iterate(iterRange, self._grow, n)

        self._curSize = p

    def _moveAfter(self, item, p):
        n = p - self._curSize

        # Shrink the frames after
        iterRange = self._paneNames[item:]
        self._iterate(iterRange, self._shrink, n)

        # Adjust the frames before
        iterRange = list(self._paneNames[:item])
        iterRange.reverse()
        self._iterate(iterRange, self._grow, n)

        self._curSize = p
