#!/usr/bin/env python
#@+leo-ver=4
#@+node:@file widgetlistbox.py
#@@first #!/usr/bin/env python

"""
widgetlist.py - Implements a PMW-like scrolled list widget
that can hold arbitrary widgets as list items

Copyright (c) 2003 by David McNab <david@freenet.org.nz>, released
under the GNU General Public License.

No warranty, yada yada
"""

#@+others
#@+node:imports
from Tkinter import *
import Pmw
#@-node:imports
#@+node:class WidgetList
class WidgetListBox(Pmw.ScrolledFrame):
    """
    Class for displaying a list of custom widgets
    
    Each widget must be an instance of WidgetListBoxItem (or a subclass)
    """
    #@    @+others
    #@+node:__init__
    def __init__(self, parent, *args, **kw):
    
        self.bg = takeKey(kw, 'bg', '#ffffff')
        self.bgsel = takeKey(kw, 'bgsel', '#c0c0ff')
        items = takeKey(kw, 'items', ())
        Pmw.ScrolledFrame.__init__(self, parent, **kw)
        self.config(
            bg=self.bg,
            highlightbackground=self.bgsel,
            )
    
        self.frame = self.interior()
        self.view = self.component('clipper')
        self.view.configure(bg=self.bg, width=800)
        self.items = []
        self.selected = None
    
        if items:
            self.setlist(items)
        
        #self.frames[0].pack_propagate(1)
    
    #@-node:__init__
    #@+node:clear
    def clear(self):
        self.setlist(())
        self.reposition()
    #@-node:clear
    #@+node:get
    def get(self, first=None, last=None):
        """
        Similar to Pmw.ScrolledListBox.get()
        
        Retrieves one, some or all the items in the list.
        Note that the items returned are widget refs
        
        Arguments:
            - first - index of first item to get. Omit to get all items
            - last - index of last item to get. Omit to get just one item
        """
        nitems = len(self.items)
        if first == None:
            return self.items[:]
        if first < 0 or first >= nitems:
            raise Exception("Range error - first=%s, nitems=%s" % (first, nitems))
        if last == None:
            return self.items[first]
        elif last == END:
            return self.items[first:]
        else:
            return self.items[first:last]
    
                
    #@nonl
    #@-node:get
    #@+node:getcurselection
    def getcurselection(self):
        """
        See getvalue()
        """
        return self.getvalue()
    #@-node:getcurselection
    #@+node:getvalue
    def getvalue(self):
        """
        Returns a list of the currently selected items
        """
        if self.selected:
            return [self.selected]
        else:
            return []
    #@-node:getvalue
    #@+node:setlist
    def setlist(self, lst, **kw):
        """
        Toss all the items in the list, and replace them
        with a whole new list of items.
        
        Each element of lst must be an instance of WidgetListBoxItem (or subclass)
        """
        nitems = len(self.items)
    
        i = 0
        while i < nitems:
            self.items[i].pack_forget()
            i += 1
        for item in lst:
            self.items.append(item)
            self.itemPackAndBind(item)
        self.reposition()
    #@-node:setlist
    #@+node:setvalue
    def setvalue(self, itemOrItems):
        """
        Sets the current selection for the WidgetListBox to itemOrItems
        
        Argument:
            - itemOrItems - either a single item (instance of WidgetListBoxItem or subclass),
              or a sequence of such
        """
        if type(itemOrItems) in [type(()), type([])]:
            nitems = len(itemOrItems)
            if nitems == 0:
                self.selectItem(-1)
            elif nitems > 1:
                raise("Multiple item selections not implemented yet")
        elif not isinstance(itemOrItems, WidgetListBoxItem) or itemOrItems not in self.items:
            raise("Nonexistent item")
        else:
            itemOrItems = [itemOrItems]
        
        for item in itemOrItems:
            self.selectItem(item)
    #@-node:setvalue
    #@+node:size
    def size(self):
        """
        Returns the number of items in the list
        """
        return len(self.items)
    #@-node:size
    #@+node:append
    def append(self, item):
        """
        Append an item to the end of the list
        
        Arguments:
            - item - the item to append - must be instance of WidgetListBoxItem or subclass
            
        Returns:
            - the item appended, for your convenience, to let you use this method 'on the fly'
        """
        if not isinstance(item, WidgetListBoxItem):
            raise Exception("Item must be an instance of WidgetListBoxItem or subclass")
    
        self.items.append(item)
        self.itemPackAndBind(item)
        self.reposition()
        return item
    #@-node:append
    #@+node:delete
    def delete(self, idxOrItem):
        """
        Deletes an item from the end of the list
        
        Arguments:
            - idxOrItem - either a ref to an item currently on the list, or the
              'index' of the item within the list
        """
        nitems = len(self.items)
    
        # convert arg to an index, if needed
        if type(idxOrItem) == type(0):
            idx = item
        else:
            # attempt to find item
            try:
                idx = self.items.index(idxOrItem)
            except:
                raise Exception("Tried to delete item not present in list")
    
        # range check
        if idx >= nitems:
            raise Exception("out of range err: idx=%s, nitems=%s" % (idx, nelems))
    
        # ditch the item
        self.items.pop(idx).pack_forget()
    
        # try to select neighbouring item
        nitems -= 1
        if idx == nitems:
            if idx > 0:
                self.selectItem(idx-1)
        else:
            self.selectItem(idx)
        self.reposition()
    #@-node:delete
    #@+node:insert
    def insert(self, idx, item):
        """
        Inserts an item at an arbitrary position within the list
        
        Arguments:
            - idx - index at which to insert the item, or -1 to append to end
            - item - the item to insert - must be instance of WidgetListBoxItem or subclass
    
        Returns:
            - the item appended, for your convenience, to let you use this method 'on the fly'
        """    
        nelems = len(self.items)
        if idx == -1:
            idx = nelems
        if idx > nelems or idx < 0:
            raise Exception("out of range err: idx=%s, nitems=%s" % (idx, len(self.widgets)))
    
        if not isinstance(item, WidgetListBoxItem):
            raise Exception("Item must be an instance of WidgetListBoxItem or subclass")
    
        #self.setItemBindings(item)
    
        if idx == nelems:
            # trivial - just append
            self.items.append(item)
            self.itemPackAndBind(item)
        else:
            # temporarily hide all items from idx onwards
            i = idx
            while i < nelems:
                self.items[i].pack_forget()
                i += 1
    
            # Stick this item in the list
            self.items.insert(idx, item)
            nelems += 1
    
            # and unhide this and all the following items
            i = idx
            while i < nelems:
                self.itemPackAndBind(self.items[i])
                i = i + 1
    
        self.reposition()
    
        return item
    
    #@-node:insert
    #@+node:selectItem
    def selectItem(self, item):
        """
        Selects one item
        
        Arguments:
            - item - either an index on the list, or a ref to one of the items on the list.
              Set to -1 to deselect all
        """
        nitems = len(self.items)
        if type(item) == type(0):
            if item < 0:
                if self.selected:
                    self.selected.setselected(0)
                    return
            elif item >= nitems:
                raise Exception("range error: item=%s, nitems=%s" % (item, nitems))
            else:
                item = self.items[item]
    
        if not isinstance(item, WidgetListBoxItem):
            raise Exception("Expected index or WidgetListBoxItem object, got a %s" % item.__class__)
        
        item.setselected(1)
    #@-node:selectItem
    #@+node:getItemIndex
    def getItemIndex(self, item):
        """
        Returns the 'index' with which the item is
        internally stored, or -1 if the item isn't known
        """
        try:
            return self.items.index(item)
        except:
            raise Exception("Item not present on list")
    #@-node:getItemIndex
    #@+node:scrollTo
    def scrollTo(self, idx):
        self.yview('moveto', float(idx-3)/len(self.items))
    #@-node:scrollTo
    #@+node:scrollUp
    def scrollUp(self, ev=None):
        self.yview('scroll', -0.33, 'pages')
    #@-node:scrollUp
    #@+node:scrollUpPage
    def scrollUpPage(self, ev=None):
        self.yview('scroll', -0.8, 'pages')
    #@-node:scrollUpPage
    #@+node:scrollDown
    def scrollDown(self, ev=None):
        self.yview('scroll', 0.33, 'pages')
    #@-node:scrollDown
    #@+node:scrollDownPage
    def scrollDownPage(self, ev=None):
        self.yview('scroll', 0.8, 'pages')
    #@-node:scrollDownPage
    #@+node:on_select
    def on_select(self, item):
        """
        Callback which gets hit whenever an item gets selected
        
        Override as desired
        """
        print "WidgetListBox.on_select: item=%s, idx=%s" % (item, self.getItemIndex(item))
    #@-node:on_select
    #@+node:on_deselect
    def on_deselect(self, item):
        """
        Callback which gets hit whenever an item gets deselected
        
        Override as desired
        """
        try:
            idx = self.getItemIndex(item)
        except:
            idx = -1
        print "WidgetListBox.on_deselect: item=%s, idx=%s" % (item, idx)
    #@-node:on_deselect
    #@+node:on_click
    def on_click(self, ev):
        """
        Callback for mouse click events.
        You likely won't need to override this.
        """
        #print "got click on widget: %s" % ev.widget.__class__
        wid = ev.widget
        wid.setselected(1)
    
    #@-node:on_click
    #@+node:on_down
    def on_down(self, ev):
        """
        Callback for 'Down' key events.
        You likely won't need to override this.
        """
        # sanity check - can't move past bottom
        if not self.selected:
            return
    
        wid = ev.widget
        try:
            idx = self.items.index(wid)
        except:
            idx = self.items.index(wid.parent)
        if idx < len(self.items) - 1:
            self.items[idx+1].setselected(1)
    
        self.scrollTo(idx)
    
    #@-node:on_down
    #@+node:on_up
    def on_up(self, ev):
        """
        Callback for 'Up' key events.
        You likely won't need to override this.
        """
        # sanity check - can't move past top
        if not self.selected:
            return
        wid = ev.widget
        try:
            idx = self.items.index(wid)
        except:
            idx = self.items.index(wid.parent)
        if idx > 0:
            self.items[idx-1].setselected(1)
    
        self.scrollTo(idx)
    #@-node:on_up
    #@+node:itemPackAndBind
    def itemPackAndBind(self, item):
        """
        Pack the item and set its bindings
        """
        #print "itemPackAndBind: item=%s" % item.__class__
        #print "isinstance=%s" % isinstance(item, WidgetListBoxItem)
    
        if isinstance(item, WidgetListBoxItem):
            for wid in item.widgets:
                #print "packing item: %s" % wid.__class__
                self.itemPackAndBind(wid)
            item.pack(side=TOP, fill='x', expand=1, anchor='nw')
        else:
            item.pack(side=LEFT, anchor='w')
    
        noBind = getattr(item, "noBind", [])
    
        #print "noBind = %s" % noBind
    
        for evtype, action in {'<Button-1>' : self.on_click,
                               '<Down>'     : self.on_down,
                               '<Up>'       : self.on_up,
                               '<Button-4>' : self.scrollUp,
                               '<Button-5>' : self.scrollDown,
                               '<Next>'     : self.scrollDownPage,
                               '<Prior>'    : self.scrollUpPage
                               }.items():
            if evtype not in noBind:
                item.bind(evtype, action)
    
    #@-node:itemPackAndBind
    #@-others

#@-node:class WidgetList
#@+node:class WidgetListItem
class WidgetListBoxItem(Frame):
    """
    Base class for creating multi-widget items which can be displayed
    in WidgetListBox objects
    
    Note that when you subclass this, you need to support the following
    methods:
        - setcolor - shades the item with a colour to indicate selection/deselection
        - on_select, on_deselect (optional)

    When inserting your own widgets into a WidgetListBoxItem, you must instantiate
    them with this WidgetListBoxItem object as parent, then call L{self.addwidget}

    Attributes you might be interested in:
        - widgets - a list of component widgets
        - parent - the WidgetListBox object into which this item has been inserted
    """
    #@    @+others
    #@+node:__init__
    def __init__(self, parent, *args, **kw):
        """
        Base constructor.
        
        You should override this to populate the item with usable widgets, and
        make sure in your constructor to call this constructor.
        
        Args:
            - parent - the WidgetListBox object on which this item will be displayed
        """
        # sanity check
        if not isinstance(parent, WidgetListBox):
            raise Exception("parent must be an instance of WidgetListBox or subclass")
        Frame.__init__(self, parent.frame, width=300, bg=parent.bg)
        self.parent = parent
        self.bg = parent.bg
        self.bgsel = parent.bgsel
        self.widgets = []
    #@-node:__init__
    #@+node:setselected
    def setselected(self, isSelected, ignore=0):
    
        if isSelected:
            # deselect prev selection
            prev = self.parent.selected
            if prev:
                prev.setselected(0)
    
            # set selected style, invoke callback
            self.parent.selected = self
            self.setcolor('select')
            self.focus_set()
            if not ignore:
                self.on_select()
                self.parent.on_select(self)
        else:
            # set deselected style, invoke callback
            self.parent.selected = None
            self.setcolor('deselect')
            if not ignore:
                self.on_deselect()
                self.parent.on_deselect(self)
    #@-node:setselected
    #@+node:setcolor
    def setcolor(self, state):
        """
        Sets the background of all items on this widget to color
        
        Override this in your subclass
        """
        if state == 'select':
            color = self.bgsel
        elif state == 'deselect':
            color = self.bg
        else:
            color = "#ffe0ff"
        self.configure(bg=color)
        for wid in self.widgets:
            wid.configure(background=color)
    
    #@-node:setcolor
    #@+node:addwidget
    def addwidget(self, wid, **kw):
        """
        Add a newly-created widget to your list item.
    
        This will chain the widget in to the listbox item, so that it will
        receive appropriate bindings, and be appropriately styled when selected
        and deselected.
        
        Arguments:
            - wid - widget to add (must have been created with this WidgetListBoxItem object
              as parent
        
        Keywords:
            - nobind - a list of binding names NOT to apply to this widget. One or more of:
                - '<Button-1>'
                - '<Down>'
                - '<Up>'
                - '<Button-4>'
                - '<Button-5>'
                - '<Next>'
                - '<Prior>'
            - bg - the default background colour of the widget
        """
        wid.noBind = kw.get('nobind', [])
        self.widgets.append(wid)
        wid.setselected = self.setselected
        bg = kw.get('bg', self.parent.bg)
        #print "bg=%s" % bg
        wid.configure(bg=bg)
        wid.parent = self
    
    #@-node:addwidget
    #@+node:on_select
    def on_select(self):
        """
        Called whenever this widget is selected, whether through
        mouse or keyboard actions
        
        Override as desired
        """
        print "item selected"
    #@-node:on_select
    #@+node:on_deselect
    def on_deselect(self):
        """
        Called whenever this widget is deselected, whether through
        mouse or keyboard actions
        
        Override as desired
        """
        print "item deselected"
    
    #@-node:on_deselect
    #@-others
#@-node:class WidgetListItem
#@+node:Functions
#@+others
#@+node:takeKey
def takeKey(somedict, keyname, default=None):
    """
    Utility function to destructively read a key from a given dict.
    Same as the dict's 'takeKey' method, except that the key (if found)
    sill be deleted from the dictionary.
    """
    if somedict.has_key(keyname):
        val = somedict[keyname]
        del somedict[keyname]
    else:
        val = default
    return val
#@-node:takeKey
#@-others
#@-node:Functions
#@+node:MAINLINE
def demo():

    r = Tk()
    r.title("WidgetListBox demo")
    t = WidgetListBox(r, usehullsize=1, hull_width=400, hull_height=200, bgsel='#ffe0ff')
    t.pack(side=LEFT, fill=BOTH, expand=1)

    class myItem(WidgetListBoxItem):
        def __init__(self, parent, txt):

            self.txt = txt

            WidgetListBoxItem.__init__(self, t, bg="#ffffff", bgsel="#ffe0ff")

            self.chk = Checkbutton(self)
            self.addwidget(self.chk, bg='yellow')

            self.butDel = Button(self,
                                 text="Del",
                                 command=self.deleteItem,
                                 activebackground="#ff8080",
                                 activeforeground="black",
                                 )
            self.addwidget(self.butDel, nobind=["<Button-1>"], bg='red')

            self.butAdd = Button(self,
                                 text="Add",
                                 command=self.addItem,
                                 activebackground="#80ff80",
                                 activeforeground="black",
                                 )
            self.addwidget(self.butAdd, nobind=["<Button-1>"], bg='green')

            self.lab = Label(self, text=txt, width=50, anchor='w')
            self.addwidget(self.lab)

        def deleteItem(self, ev=None):
            print "Don't yet know how to delete"
            self.parent.delete(self)

        def addItem(self, ev=None):
            parent = self.parent
            idx = parent.getItemIndex(self)
            parent.insert(idx+1, myItem(parent, self.txt + "*"))
            
        def setcolor(self, state):
            if state == 'select':
                color = self.bgsel
                self.configure(bg=color)
                self.lab.configure(bg=color)
                self.chk.configure(bg='#ffff80')
                self.butDel.configure(bg='#ff8080')
                self.butAdd.configure(bg='#80ff80')
            elif state == 'deselect':
                color = self.bg
                self.configure(bg=color)
                self.lab.configure(bg=color)
                self.chk.configure(bg='yellow')
                self.butDel.configure(bg='red')
                self.butAdd.configure(bg='green')


    # populate our list
    myitems = []
    for txt in ['Zeroth', 'First', 'Second', 'Third', 'Fourth', 'Fifth',
                'Sixth', 'Seventh', 'Eighth', 'Ninth', 'Tenth', 'Eleventh',
                'Twelth', 'Thirteenth',
                ]:
        myitems.append(t.append(myItem(t, txt)))

    # select the second item
    t.selectItem(1)

    # interact
    r.mainloop()

#if __name__ == '__main__':
#    demo()
#@-node:MAINLINE
#@-others
#@-node:@file widgetlistbox.py
#@-leo
