#!/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 , 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 {'' : self.on_click, '' : self.on_down, '' : self.on_up, '' : self.scrollUp, '' : self.scrollDown, '' : self.scrollDownPage, '' : 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: - '' - '' - '' - '' - '' - '' - '' - 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=[""], bg='red') self.butAdd = Button(self, text="Add", command=self.addItem, activebackground="#80ff80", activeforeground="black", ) self.addwidget(self.butAdd, nobind=[""], 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