Sep 07 2012

python, pygtk crumblist

Category: pygtk,pythonerm @ 10:13 am

I needed a hbox that would automatically take children widgets from one row and put it into another row depending on the width of the widget. This is similar to how html elements are handled on a web page.

The desired result was to basically make a crumb/tag list with a close button that you could easily remove elements from.

#!/usr/bin/env python
# lib/crumbs.py -- Create crumb type list
#    Copyright (C) 2012 Eugene Miller 
#
#    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 3 of the License, or
#    (at your option) any later version.
#
#    This program is distributed in the hope that it will be useful,
#    but WITHOUT ANY WARRANTY; without even the implied warranty of
#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#    GNU General Public License for more details.
#
#    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., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
#

import gtk, gobject,sys, random

class Crumbs(gtk.VBox):
    def __init__(self, target_width = 600):
        gtk.VBox.__init__(self)
        self.straighten_lock = False
        self.allocation_lock = False
        self.show_all()
        self.target_width = target_width
        self.pack_start(gtk.HBox(),False,False)
        self.connect("size-allocate", self.on_allocate)

    def on_allocate(self, widget, allocation):
        if self.allocation_lock or self.straighten_lock:
            return
        self.allocation_lock = True
        parent = self.get_parent()
        parent_allocation = parent.get_allocation()
        self.target_width = parent_allocation.width
        self.straighten()
        self.allocation_lock = False

    def add_crumb(self, widget):
        parent = self.get_parent()
        if parent:
            # Change target_width to parent's allocation.
            parent_allocation = parent.get_allocation()
            self.target_width = parent_allocation.width

        # Ensure there is at least 1 empty row.
        children = self.get_children()
        if not children:
            # Add new row.
            self.pack_start(gtk.HBox(),False,False)
            children = self.get_children()

        last_row = children[-1]
        width, height = last_row.size_request()
        if width >= self.target_width:
            # Add another row because the last row is too wide.
            self.pack_start(gtk.HBox(),False,False)
            children = self.get_children()
            last_row = children[-1]

        last_row.show() # makes sure the last row is showing (we could have added a row.)

        # Add widget to crumbs.
        last_row.pack_start(widget, False, False)
        self.straighten()

    def straighten(self):
        if self.straighten_lock:
            return
        self.straighten_lock = True
        rebuild = False
        rows = self.get_children()
        for i, row in enumerate(rows):
            row_width, r_height = row.size_request()
            crumbs = row.get_children()
            if not crumbs:
                # Don't process empty rows.
                continue

            try:
                next_row_children = rows[i+1].get_children()
            except IndexError, err:
                self.pack_start(gtk.HBox(), False, False)
                rows = self.get_children()
                rows[i+1].show()
                next_row_children = rows[i+1].get_children()

            if row_width >= self.target_width:
                if len(crumbs) == 1:
                    # The crumb takes up the entire row so we do nothing.
                    continue
                c = crumbs[-1]
                # Remove the last crumb from the current row, and add it to the
                # next row
                rebuild = True
                row.remove(c)
                rows[i+1].pack_start(c, False, False)
                rows[i+1].reorder_child(c, 0)
                c.show()

            elif row_width < self.target_width and next_row_children:
                # Next row has children, and the current row's width
                # is < our target_width
                next_row_first_child = next_row_children[0]
                next_row_first_child_width, c_height = next_row_first_child.size_request()
                if (next_row_first_child_width + row_width) < self.target_width:
                    # the next row's first child will fit into the current row.
                    # remove it from the next row, and add it to the current row.
                    rebuild = True
                    rows[i+1].remove(next_row_first_child)
                    row.pack_start(next_row_first_child,False,False)
                    next_row_first_child.show()
                    if not rows[i+1].get_children():
                        # the next row has no children so we remove it.
                        rows[i+1].destroy()

        self.straighten_lock = False
        if rebuild:
            # run straighten again to ensure crumbs are in the right place.
            self.straighten()


if __name__ == '__main__':

    def on_remove(w, crumb, text):
        print "removing:",text
        crumb.destroy()

    def on_info(w, crumb, text):
        print "This does nothing, but here's where you'd do a db lookup about that tag."
        print "text:",text

    w = gtk.Window()
    w.set_default_size(600, 400)
    w.set_position(gtk.WIN_POS_CENTER)

    crbs = Crumbs()
    crbs.target_width = 600
    crbs.show_all()
    
    if "-vbox" in sys.argv:
        # This is for testing behavior in a vbox
        container = gtk.VBox()
        container.pack_start(crbs, False, False)
    elif "-hbox" in sys.argv:
        # This is for testing behavior in a hbox
        container = gtk.HBox()
        container.pack_start(crbs, True, True) # hboxes work better with True,True
    else:
        # By default use a scrolled window for the container
        container = gtk.ScrolledWindow()
        container.set_policy(gtk.POLICY_AUTOMATIC,gtk.POLICY_AUTOMATIC)
        container.add_with_viewport(crbs)

    

    crumb_list = [
        "Allison Krauss Yo-yo Ma, Edgar Meyer, Mark O'connor",
        "Yo-Yo Ma, Edgar Meyer, Mark O'Connor",
        "Yo-Yo Ma",
        "Edgar Meyer",
        "Mark O'Connor",
        "Allison Krauss Yo-yo Ma",
        "Mark O'connor",
    ]
    random.shuffle(crumb_list)

    for i, t in enumerate(crumb_list):
        # create buttons out of crumb_list text

        # Setup event boxes so we have a little bit of style for our buttons.
        # (two buttons next to each other looks kinda crappy)
        border = gtk.EventBox()
        border.show()
        border.modify_bg(gtk.STATE_NORMAL, gtk.gdk.color_parse("black"))
        border.set_border_width(1) # padding so there is space between our buttons.

        body = gtk.EventBox()
        body.show()
        body.set_border_width(1) # padding so black shows.

        # Style our buttons.
        body.modify_bg(gtk.STATE_NORMAL, gtk.gdk.color_parse("white"))
        body.modify_fg(gtk.STATE_NORMAL, gtk.gdk.color_parse("black"))

        hbox = gtk.HBox() # For putting out buttons into.
        hbox.show()

        ## Tag button
        btn = gtk.Button()
        btn.set_property("relief",gtk.RELIEF_NONE) # Remove default borders
        btn.set_label("%s %s" % (i,t)) # Add i debugging.
        btn.connect("activate", on_info, border, t)
        btn.connect("pressed", on_info, border, t)
        btn.show()
        hbox.pack_start(btn,True,True)

        ## Remove button
        close_image = gtk.image_new_from_stock(gtk.STOCK_CLOSE, gtk.ICON_SIZE_MENU)
        btn = gtk.Button()
        btn.set_property("relief",gtk.RELIEF_NONE) # Remove default borders
        btn.set_image(close_image)
        btn.set_image_position(gtk.POS_RIGHT)
        btn.connect("activate", on_remove, border, t)
        btn.connect("pressed", on_remove, border, t)
        btn.show()
        hbox.pack_end(btn,False,False)

        border.add(body)
        body.add(hbox)
        crbs.add_crumb(border)

    container.show()
    w.add(container)
    w.connect("destroy", gtk.main_quit)
    w.show()
    gtk.main()

Leave a Reply

You must be logged in to post a comment. Login now.