#
#  FILE: TreeBrowser.py
#
#  DESCRIPTION:
#    This file provides a generic hierarchical tree browser widget.
#
#  AUTHOR:  Steve Kinneberg <skinneberg@mvista.com>,
#           MontaVista Software, Inc. <source@mvista.com>
#
#  Copyright 2001 MontaVista Software Inc.
#
#  This program is free software; you can redistribute  it and/or modify it
#  under  the terms of  the GNU General  Public License as published by the
#  Free Software Foundation;  either version 2 of the  License, or (at your
#  option) any later version.
#
#  THIS  SOFTWARE  IS PROVIDED   ``AS  IS'' AND   ANY  EXPRESS OR IMPLIED
#  WARRANTIES,   INCLUDING, BUT NOT  LIMITED  TO, THE IMPLIED WARRANTIES OF
#  MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.  IN
#  NO  EVENT  SHALL   THE AUTHOR  BE    LIABLE FOR ANY   DIRECT, INDIRECT,
#  INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
#  NOT LIMITED   TO, PROCUREMENT OF  SUBSTITUTE GOODS  OR SERVICES; LOSS OF
#  USE, DATA,  OR PROFITS; OR  BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
#  ANY THEORY OF LIABILITY, WHETHER IN  CONTRACT, STRICT LIABILITY, OR TORT
#  (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
#  THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#
#  You should have received a copy of the  GNU General Public License along
#  with this program; if not, write  to the Free Software Foundation, Inc.,
#  675 Mass Ave, Cambridge, MA 02139, USA.
#


import types
import tkinter
import Pmw
import collections


class _Branching:
    def __init__(self):
        # List of branch names
        self._nodeNames = []

        # Map from branch name to branch info
        #   branch      Either _LeafNode or _BranchNode widget of the branch
        #   nodetype    Either 'TreeNode' or 'LeafNode'
        self._nodeAttrs = {}

    def addbranch(self, branchName = None, **kw):
        kw['indent'] = self['indent']
        return self._insertnode(*('tree', branchName, len(self._nodeNames),
                      self._treeRoot), **kw)

    def addleaf(self, leafName = None, **kw):
        return self._insertnode(*('leaf', leafName, len(self._nodeNames),
                      self._treeRoot), **kw)

    def insertbranch(self, branchName = None, before = 0, **kw):
        kw['indent'] = self['indent']
        return self._insertnode(*('tree', branchName, before, self._treeRoot), **kw)

    def insertleaf(self, leafName = None, before = 0, **kw):
        return self._insertnode(*('leaf', leafName, before, self._treeRoot), **kw)

    def _insertnode(self, type, nodeName, before, treeRoot, **kw):
        if 'selectbackground' not in list(kw.keys()):
            kw['selectbackground'] = self['selectbackground']

        if 'selectforeground' not in list(kw.keys()):
            kw['selectforeground'] = self['selectforeground']

        if 'background' not in list(kw.keys()):
            kw['background'] = self['background']

        if 'foreground' not in list(kw.keys()):
            kw['foreground'] = self['foreground']

        if nodeName == None:
            nodeName = self._nodeName + ".%d" % (len(self._nodeNames) + 1)

        if nodeName in self._nodeAttrs:
            msg = 'Node "%s" already exists.' % nodeName
            raise ValueError(msg)

        # Do this early to catch bad <before> spec before creating any items.
        beforeIndex = self.index(before, 1)
        attributes = {}

        last = (beforeIndex == len(self._nodeNames))
        if last and len(self._nodeNames) > 0:
            # set the previous node to not last
            self._nodeAttrs[self._nodeNames[-1]]['branch']._setlast(0)

        if(type == 'tree'):
            node = self.createcomponent(*('branch%d'%len(self._nodeNames),
                                                (), None,
                                                _BranchNode,
                                                self._branchFrame,
                                                nodeName,
                                                treeRoot,
                                                self,
                                                last,
                                                ), **kw)
            attributes['nodetype'] = 'TreeNode'
        else:
            node = self.createcomponent(*('leaf%d'%len(self._nodeNames),
                                                (), None,
                                                _LeafNode,
                                                self._branchFrame,
                                                nodeName,
                                                treeRoot,
                                                self,
                                                last,
                                                ), **kw)
            attributes['nodetype'] = 'LeafNode'

        if len(self._nodeNames) == beforeIndex:
            node.pack(anchor='w')
        else:
            bname = self._nodeNames[beforeIndex]
            battrs = self._nodeAttrs[bname]
            node.pack(anchor='w', before=battrs['branch'])

        attributes['branch'] = node

        self._nodeAttrs[nodeName] = attributes
        self._nodeNames.insert(beforeIndex, nodeName)
        self._sizechange()
        return node

    def delete(self, *nodes):
        curSel = self._treeRoot.curselection()[0]
        for node in nodes:
            index = self.index(node)
            name = self._nodeNames.pop(index)
            dnode = self._nodeAttrs[name]['branch']
            del self._nodeAttrs[name]
            if dnode == curSel:
                self._treeRoot._unhightlightnode(dnode)
            dnode.destroy()
        self._sizechange()

    def destroy(self):
        for node in len(self._nodeNames):
            self.delete(node)
        Pmw.MegaWidget.destroy(self)

    def index(self, index, forInsert = 0):
        if isinstance(index, _LeafNode):
            index = index._nodeName
        listLength = len(self._nodeNames)
        if type(index) == int:
            if forInsert and index <= listLength:
                return index
            elif not forInsert and index < listLength:
                return index
            else:
                raise ValueError('index "%s" is out of range' % index)
        elif type(index) == bytes:
            if index in self._nodeNames:
                return self._nodeNames.index(index)
            raise ValueError('bad branch or leaf name: %s' % index)
        elif index is Pmw.END:
            if forInsert:
                return listLength
            elif listLength > 0:
                return listLength - 1
            else:
                raise ValueError('TreeNode has no branches')
        #elif index is Pmw.SELECT:
        #    if listLength == 0:
        #       raise ValueError, 'TreeNode has no branches'
        #    return self._pageNames.index(self.getcurselection())
        else:
            validValues = 'a name, a number, Pmw.END, Pmw.SELECT, or a reference to a TreeBrowser Leaf or Branch'
            raise ValueError('bad index "%s": must be %s' % (index, validValues))

    def getnodenames(self):
        return self._nodeNames

    def getnode(self, node):
        nodeName = self._nodeNames[self.index(node)]
        return self._nodeAttrs[nodeName]['branch']


class _LeafNode(Pmw.MegaWidget):

    def __init__(self, parent, nodeName, treeRoot, parentnode, last = 1, **kw):
        colors = Pmw.Color.getdefaultpalette(parent)

        self._nodeName = nodeName
        self._treeRoot = treeRoot
        self._parentNode = parentnode

        self._last = last
        # Define the megawidget options.
        INITOPT = Pmw.INITOPT
        optiondefs = (
            ('selectbackground', colors['selectBackground'], INITOPT),
            ('selectforeground', colors['selectForeground'], INITOPT),
            ('background',       colors['background'],       INITOPT),
            ('foreground',       colors['foreground'],       INITOPT),
            ('selectcommand',    None,                       None),
            ('deselectcommand',  None,                       None),
            ('labelpos',         'e',                        INITOPT),
            ('labelmargin',      0,                          INITOPT),
            ('label',            None,                       None),
        )
        self.defineoptions(kw, optiondefs)

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

        # Create the components
        interior = self._hull


        labelpos = self['labelpos']

        if self['label'] == None:
            self._labelWidget = self.createcomponent('labelwidget',
                                                     (), None,
                                                     Pmw.LabeledWidget,
                                                     (interior,),
                                                     #background = self['background'],
                                                     #foreground = self['foreground'],
                                                     )
        else:
            self._labelWidget = self.createcomponent('labelwidget',
                                                     (), None,
                                                     Pmw.LabeledWidget,
                                                     (interior,),
                                                     label_background = self['background'],
                                                     label_foreground = self['foreground'],
                                                     labelpos = labelpos,
                                                     labelmargin = self['labelmargin'],
                                                     label_text = self['label'],
                                                     )
            self._labelWidget.component('label').bind('<ButtonRelease-1>',
                                                      self._selectevent)

        self._labelWidget.grid(column = 1, row = 0, sticky = 'w')

        self._labelWidget.update()

        self._labelheight = self._labelWidget.winfo_height()

        self._lineCanvas = self.createcomponent('linecanvas',
                                                (), None,
                                                tkinter.Canvas,
                                                (interior,),
                                                width = self._labelheight,
                                                height = self._labelheight,
                                                )
        self._lineCanvas.grid( column = 0, row = 0, sticky = 'news')
        self._lineCanvas.update()

        cw = int(self._lineCanvas['width'])
        ch = int(self._lineCanvas['height'])

        self._lineCanvas.create_line(cw/2, ch/2, cw, ch/2, tag='hline')
        if last:
            self._lineCanvas.create_line(cw/2, 0, cw/2, ch/2, tag='vline')
        else:
            self._lineCanvas.create_line(cw/2, 0, cw/2, ch, tag='vline')

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


    def interior(self):
        return self._labelWidget.interior()

    def select(self):
        self._highlight()

    def getname(self):
        return self._nodeName

    def getlabel(self):
        return self['label']

    def _selectevent(self, event):
        self._highlight()

    def _highlight(self):
        self._treeRoot._highlightnode(self)
        #self._subHull.configure(background = self._selectbg, relief = 'raised')
        if self['label'] != None:
            self._labelWidget.configure(label_background = self['selectbackground'])
            self._labelWidget.configure(label_foreground = self['selectforeground'])
        #self._viewButton.configure(background = self._selectbg)
        cmd = self['selectcommand']
        if hasattr(cmd, '__call__'):
            cmd(self)

    def _unhighlight(self):
        #self._subHull.configure(background = self._bg, relief = 'flat')
        if self['label'] != None:
            self._labelWidget.configure(label_background = self['background'])
            self._labelWidget.configure(label_foreground = self['foreground'])
        #self._viewButton.configure(background = self._bg)
        cmd = self['deselectcommand']
        if hasattr(cmd, '__call__'):
            cmd(self)

    def _setlast(self, last):
        self._last = last

        cw = int(self._lineCanvas['width'])
        ch = int(self._lineCanvas['height'])

        if last:
            self._lineCanvas.create_line(cw/2, 0, cw/2, ch/2, tag='vline')
        else:
            self._lineCanvas.create_line(cw/2, 0, cw/2, ch, tag='vline')


class _BranchNode(_LeafNode, _Branching): #Pmw.MegaWidget):

    def __init__(self, parent, nodeName, treeRoot, parentnode, last = 1, **kw):
        # Define the megawidget options.
        INITOPT = Pmw.INITOPT
        optiondefs = (
            ('view',            'collapsed', None),
            ('expandcommand',   None,        None),
            ('collapsecommand', None,        None),
            ('indent',          0,           INITOPT)
        )
        self.defineoptions(kw, optiondefs)

        # Initialise the base class (after defining the options).
        _LeafNode.__init__(*(self, parent, nodeName, treeRoot, parentnode, last), **kw)
        _Branching.__init__(self)

        # Create the components
        interior = self._hull

        # Create the expand/collapse button
        self._viewButton = self.createcomponent('viewbutton', (), None,
                                                tkinter.Canvas,
                                                (interior,),
                                                background = self['background'],
                                                width = self._labelheight - 4,
                                                height = self._labelheight - 4,
                                                borderwidth = 2,
                                                relief = 'raised')

        self._viewButton.grid(column = 0, row = 0, sticky='se')
        self._viewButton.bind('<ButtonPress-1>', self._showbuttonpress)
        self._viewButton.bind('<ButtonRelease-1>', self._toggleview)

        # The label widget is already created by the base class, however
        # we do need to make some slight modifications.
        if self['label'] != None:
            self._labelWidget.component('label').bind('<Double-1>',
                                                      self._toggleview)
        self._labelWidget.grid(column=1, row=0, columnspan = 3, sticky='sw')

        # A line canvas is already created for us, we just need to make
        # some slight modifications
        self._lineCanvas.delete('hline')
        self._lineCanvas.grid_forget()


        # Set the minsize of column 1 to control additional branch frame indentation
        self.grid_columnconfigure(1, minsize = self['indent'])

        # Create the branch frame that will contain all the branch/leaf nodes
        self._branchFrame = self.createcomponent('frame', (), None,
                                                 tkinter.Frame, (interior,),
                                                 #borderwidth=2,
                                                 #relief='ridge',
                                                 )
        self.grid_columnconfigure(2,minsize=0, weight=1)
        #self.grid_rowconfigure(0,minsize=0)

        if(self['view'] == 'expanded'):
            Pmw.drawarrow(self._viewButton,
                          self['foreground'],
                          'down', 'arrow')
            self._branchFrame.grid(column = 2, row = 1, sticky='nw')
            if not self._last:
                self._branchFrame.update()
                bh = self._branchFrame.winfo_height()
                self._lineCanvas.configure(height = bh)
                self._lineCanvas.grid(column = 0, row = 1, sticky='news')
                cw = int(self._lineCanvas['width'])
                ch = int(self._lineCanvas['height'])
                #self._lineCanvas.create_line(cw/2, 1, cw/2, ch, tag = 'vline')
                self._lineCanvas.coords('vline', cw/2, 1, cw/2, ch)
        else:
            Pmw.drawarrow(self._viewButton,
                          self['foreground'],
                          'right', 'arrow')
            self._viewButton.configure(relief = 'raised')


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



    def _showbuttonpress(self, event):
        self._viewButton.configure(relief = 'sunken')

    def _toggleview(self, event):
        self._viewButton.configure(relief = 'sunken')
        self.select()
        if(self['view'] == 'expanded'):
            self.collapsetree()
        else:
            self.expandtree()
        self._viewButton.configure(relief = 'raised')

    def expandtree(self):
        if(self['view'] == 'collapsed'):
            cmd = self['expandcommand']
            if cmd is not None:
                cmd(self)
            self['view'] = 'expanded'
            Pmw.drawarrow(self._viewButton,
                          self['foreground'],
                          'down', 'arrow')
            self._branchFrame.grid(column = 2, row = 1, sticky='nw')

            if not self._last:
                self._branchFrame.update()
                bh = self._branchFrame.winfo_height()
                self._lineCanvas.configure(height = bh)
                self._lineCanvas.grid(column = 0, row = 1, sticky='news')
                cw = int(self._lineCanvas['width'])
                ch = int(self._lineCanvas['height'])
                #self._lineCanvas.create_line( cw/2, 1, cw/2, ch, tag = 'vline')
                self._lineCanvas.coords('vline', cw/2, 1, cw/2, ch)
            self._parentNode._sizechange()

    def collapsetree(self):
        if(self['view'] == 'expanded'):
            cmd = self['collapsecommand']
            if cmd is not None:
                cmd(self)
            self['view'] = 'collapsed'
            Pmw.drawarrow(self._viewButton,
                          self['foreground'],
                          'right', 'arrow')
            self._branchFrame.grid_forget()
            if not self._last:
                #self._lineCanvas.delete('vline')
                self._lineCanvas.grid_forget()
            self._parentNode._sizechange()

    def _setlast(self, last):
        self._last = last
        if self['view'] == 'expanded':
            self._branchFrame.update()
            bh = self._branchFrame.winfo_height()
            self._lineCanvas.configure(height = bh)
            cw = int(self._lineCanvas['width'])
            ch = int(self._lineCanvas['height'])
            self._lineCanvas.delete('vline')
            if not last:
                self._lineCanvas.create_line(cw/2, 1, cw/2, ch, tag='vline')


    def _sizechange(self):
        if not self._last and self['view'] == 'expanded':
            self._branchFrame.update()
            bh = self._branchFrame.winfo_height()
            self._lineCanvas.configure(height = bh)
            if self._lineCanvas.coords('vline')[3] < bh:
                cw = int(self._lineCanvas['width'])
                ch = int(self._lineCanvas['height'])
                #self._lineCanvas.delete('vline')
                #self._lineCanvas.create_line(cw/2, 1, cw/2, ch, tag='vline')
                self._lineCanvas.coords('vline', cw/2, 1, cw/2, ch)
        self._parentNode._sizechange()

class TreeBrowser(Pmw.MegaWidget, _Branching):

    def __init__(self, parent = None, nodeName = '0', **kw):
        colors = Pmw.Color.getdefaultpalette(parent)

        # Define the megawidget options.
        INITOPT = Pmw.INITOPT
        optiondefs = (
            ('indent',           0,                          INITOPT),
            ('selectbackground', colors['selectBackground'], INITOPT),
            ('selectforeground', colors['selectForeground'], INITOPT),
            ('background',       colors['background'],       INITOPT),
            ('foreground',       colors['foreground'],       INITOPT),
            #('selectrelief',     'raised',       INITOPT),
        )

        self.defineoptions(kw, optiondefs)

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


        # Create the components
        interior = self._hull

        browserFrame = self.createcomponent('frame', (), None,
                                            Pmw.ScrolledFrame,
                                            (interior,),
                                            )

        browserFrame.pack(expand = 1, fill='both')

        self._branchFrame = browserFrame.interior()

        self._highlightedNode = None
        self._treeRoot = self
        self._nodeName = nodeName
        # Check keywords and initialise options.
        self.initialiseoptions()

    def _highlightnode(self, newNode):
        if self._highlightedNode != newNode:
            if self._highlightedNode != None:
                self._highlightedNode._unhighlight()
            self._highlightedNode = newNode

    def _unhighlightnode(self):
        if self._highlightedNode != None:
            self._highlightedNode._unhighlight()
            self._highlightedNode = None

    def curselection(self):
        retVal = None
        if self._highlightedNode != None:
            retVal = (self._highlightedNode,
                      self._highlightedNode._nodeName,
                      self._highlightedNode['label'])
        return retVal

    def getname(self):
        return self._nodeName

    # The top-level TreeBrowser widget only shows nodes in an expanded view
    # but still provides collapsetree() and expandtree() methods so that users
    # don't have to special case the top-level node

    def collapsetree(self):
        return

    def expandtree(self):
        return

    def _sizechange(self):
        return

if __name__ == '__main__':

    rootWin = tkinter.Tk()

    Pmw.initialise()

    rootWin.title('TreeBrowser Demo')

    # Create the hierarchical tree browser widget
    treeBrowser = TreeBrowser(rootWin,
                                          #selectbackground = "darkgreen",
                                          #selectforeground = 'lightgreen',
                                          #background = 'green',
                                          #indent = 10,
                                          )


    def printselected(node):
        selection = treeBrowser.curselection()
        if selection != None:
            print("Selected node name:", selection[1], "   label:", selection[2])


    def printdeselected(node):
        selection = treeBrowser.curselection()
        if selection != None:
            print("Deselected node name:", selection[1], "   label:", selection[2])

    def printexpanded(node):
        print("Expanded node name:", node.getname(), "   label:", node.getlabel())

    def printcollapsed(node):
        print("Collapsed node name:", node.getname(), "   label:", node.getlabel())



    for i in range(3):
        # Add a tree node to the top level
        treeNodeLevel1 = treeBrowser.addbranch(label = 'TreeNode %d'%i,
                                               selectcommand = printselected,
                                               deselectcommand = printdeselected,
                                               expandcommand = printexpanded,
                                               collapsecommand = printcollapsed,
                                               )
        for j in range(3):
            # Add a tree node to the second level
            treeNodeLevel2 = treeNodeLevel1.addbranch(label = 'TreeNode %d.%d'%(i,j),
                                                      #selectforeground = 'yellow',
                                                      selectcommand = printselected,
                                                      deselectcommand = printdeselected,
                                                      expandcommand = printexpanded,
                                                      collapsecommand = printcollapsed,
                                                      )
            if i == 0 and j == 1:
                dynamicTreeRootNode = treeNodeLevel1
                dynamicTreePosNode = treeNodeLevel2

            for item in range((i+1)*(j+1)):
                # Add a leaf node to the third level
                leaf = treeNodeLevel2.addleaf(label = "Item %c"%(item+65),
                                              #selectbackground = 'blue',
                                              selectcommand = printselected,
                                              deselectcommand = printdeselected)
        for item in range(i+1):
            # Add a leaf node to the top level
            leaf = treeNodeLevel1.addleaf(label = "Item %c"%(item+65),
                                          selectcommand = printselected,
                                          deselectcommand = printdeselected)


    treeNodeLevel1 = treeBrowser.addbranch(label = 'Check Button Label',
                                           selectcommand = printselected,
                                           deselectcommand = printdeselected,
                                           expandcommand = printexpanded,
                                           collapsecommand = printcollapsed,
                                           )
    checkButton = tkinter.Checkbutton(treeNodeLevel1.interior(),
                                      text = 'Da Check Button',
                                      relief = 'ridge',
                                      command = treeNodeLevel1.select)
    checkButton.pack()

    treeNodeLevel1.addleaf(label = 'Labeled Leaf',
                           selectcommand = printselected,
                           deselectcommand = printdeselected)
    leaf = treeNodeLevel1.addleaf(label = 'Labeled Leaf w/ Checkbutton',
                                  selectcommand = printselected,
                                  deselectcommand = printdeselected)
    checkButton = tkinter.Checkbutton(leaf.interior(),
                                      text = 'Da Check Button',
                                      relief = 'ridge',
                                      command = leaf.select)
    checkButton.pack()


    treeNodeLevel1 = treeBrowser.addbranch(selectcommand = printselected,
                                           deselectcommand = printdeselected,
                                           expandcommand = printexpanded,
                                           collapsecommand = printcollapsed,
                                           )
    checkButton = tkinter.Checkbutton(treeNodeLevel1.interior(),
                                      text = 'Check Button with no label',
                                      relief = 'ridge',
                                      command = treeNodeLevel1.select)
    checkButton.pack()

    treeNodeLevel1 = treeBrowser.addbranch(label = 'Label',
                                           selectcommand = printselected,
                                           deselectcommand = printdeselected,
                                           expandcommand = printexpanded,
                                           collapsecommand = printcollapsed,
                                           )

    # setup dynamic tree node insertion and removal
    class dynTree:
        def __init__(self):
            self.dyn = tkinter.IntVar()
            self.dtree = None

            self.dLeaf = treeBrowser.addleaf(selectcommand = self.dynSelected,
                                             deselectcommand = self.dynDeselected)

            self.dCheckButton = tkinter.Checkbutton(self.dLeaf.interior(),
                                                    text = 'Enable Dynamic Tree',
                                                    variable = self.dyn,
                                                    command = self.ChkBtnHandler)
            self.dCheckButton.pack()


        def dynSelected(self, node):
            self.dCheckButton.configure(background = self.dLeaf.configure('selectbackground')[4])
            printselected(node)

        def dynDeselected(self, node):
            self.dCheckButton.configure(background = self.dLeaf.configure('background')[4])
            printdeselected(node)

        def ChkBtnHandler(self):
            self.dLeaf.select()
            if self.dyn.get() == 1:
                self.dtree = dynamicTreeRootNode.insertbranch(label = 'Dynamic Tree Node',
                                                              selectcommand = printselected,
                                                              deselectcommand = printdeselected,
                                                              expandcommand = printexpanded,
                                                              collapsecommand = printcollapsed,
                                                              before = dynamicTreePosNode)
                self.dtree.addleaf(label = 'Dynamic Leaf 1',
                                   selectcommand = printselected,
                                   deselectcommand = printdeselected)
                self.dtree.addleaf(label = 'Dynamic Leaf 2',
                                   selectcommand = printselected,
                                   deselectcommand = printdeselected)
            else:
                if self.dtree != None:
                    dynamicTreeRootNode.delete(self.dtree)
                    self.dtree = None


    foo = dynTree()


    treeBrowser.pack(expand = 1, fill='both')

    exitButton = tkinter.Button(rootWin, text="Quit", command=rootWin.quit)
    exitButton.pack()

    rootWin.mainloop()
