{"id":502,"date":"2012-09-07T10:13:20","date_gmt":"2012-09-07T16:13:20","guid":{"rendered":"http:\/\/blog.the-erm.com\/?p=502"},"modified":"2012-09-07T10:21:07","modified_gmt":"2012-09-07T16:21:07","slug":"python-pygtk-crumblist","status":"publish","type":"post","link":"https:\/\/blog.the-erm.com\/?p=502","title":{"rendered":"python, pygtk crumblist"},"content":{"rendered":"<p><a href=\"https:\/\/blog.the-erm.com\/wp-content\/uploads\/crumbs.py_.screenshot.png\"><img loading=\"lazy\" decoding=\"async\" src=\"https:\/\/blog.the-erm.com\/wp-content\/uploads\/crumbs.py_.screenshot-300x104.png\" alt=\"\" title=\"crumbs.py.screenshot\" width=\"300\" height=\"104\" class=\"alignnone size-medium wp-image-510\" srcset=\"https:\/\/blog.the-erm.com\/wp-content\/uploads\/crumbs.py_.screenshot-300x104.png 300w, https:\/\/blog.the-erm.com\/wp-content\/uploads\/crumbs.py_.screenshot.png 512w\" sizes=\"auto, (max-width: 300px) 100vw, 300px\" \/><\/a>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.<\/p>\n<p>The desired result was to basically make a crumb\/tag list with a close button that you could easily remove elements from.<\/p>\n<p><!--more--><\/p>\n<pre style=\"overflow:auto; height:400px\">\r\n#!\/usr\/bin\/env python\r\n# lib\/crumbs.py -- Create crumb type list\r\n#    Copyright (C) 2012 Eugene Miller <theerm @gmail.com>\r\n#\r\n#    This program is free software; you can redistribute it and\/or modify\r\n#    it under the terms of the GNU General Public License as published by\r\n#    the Free Software Foundation; either version 3 of the License, or\r\n#    (at your option) any later version.\r\n#\r\n#    This program is distributed in the hope that it will be useful,\r\n#    but WITHOUT ANY WARRANTY; without even the implied warranty of\r\n#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\r\n#    GNU General Public License for more details.\r\n#\r\n#    You should have received a copy of the GNU General Public License\r\n#    along with this program; if not, write to the Free Software\r\n#    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA\r\n#\r\n\r\nimport gtk, gobject,sys, random\r\n\r\nclass Crumbs(gtk.VBox):\r\n    def __init__(self, target_width = 600):\r\n        gtk.VBox.__init__(self)\r\n        self.straighten_lock = False\r\n        self.allocation_lock = False\r\n        self.show_all()\r\n        self.target_width = target_width\r\n        self.pack_start(gtk.HBox(),False,False)\r\n        self.connect(\"size-allocate\", self.on_allocate)\r\n\r\n    def on_allocate(self, widget, allocation):\r\n        if self.allocation_lock or self.straighten_lock:\r\n            return\r\n        self.allocation_lock = True\r\n        parent = self.get_parent()\r\n        parent_allocation = parent.get_allocation()\r\n        self.target_width = parent_allocation.width\r\n        self.straighten()\r\n        self.allocation_lock = False\r\n\r\n    def add_crumb(self, widget):\r\n        parent = self.get_parent()\r\n        if parent:\r\n            # Change target_width to parent's allocation.\r\n            parent_allocation = parent.get_allocation()\r\n            self.target_width = parent_allocation.width\r\n\r\n        # Ensure there is at least 1 empty row.\r\n        children = self.get_children()\r\n        if not children:\r\n            # Add new row.\r\n            self.pack_start(gtk.HBox(),False,False)\r\n            children = self.get_children()\r\n\r\n        last_row = children[-1]\r\n        width, height = last_row.size_request()\r\n        if width >= self.target_width:\r\n            # Add another row because the last row is too wide.\r\n            self.pack_start(gtk.HBox(),False,False)\r\n            children = self.get_children()\r\n            last_row = children[-1]\r\n\r\n        last_row.show() # makes sure the last row is showing (we could have added a row.)\r\n\r\n        # Add widget to crumbs.\r\n        last_row.pack_start(widget, False, False)\r\n        self.straighten()\r\n\r\n    def straighten(self):\r\n        if self.straighten_lock:\r\n            return\r\n        self.straighten_lock = True\r\n        rebuild = False\r\n        rows = self.get_children()\r\n        for i, row in enumerate(rows):\r\n            row_width, r_height = row.size_request()\r\n            crumbs = row.get_children()\r\n            if not crumbs:\r\n                # Don't process empty rows.\r\n                continue\r\n\r\n            try:\r\n                next_row_children = rows[i+1].get_children()\r\n            except IndexError, err:\r\n                self.pack_start(gtk.HBox(), False, False)\r\n                rows = self.get_children()\r\n                rows[i+1].show()\r\n                next_row_children = rows[i+1].get_children()\r\n\r\n            if row_width >= self.target_width:\r\n                if len(crumbs) == 1:\r\n                    # The crumb takes up the entire row so we do nothing.\r\n                    continue\r\n                c = crumbs[-1]\r\n                # Remove the last crumb from the current row, and add it to the\r\n                # next row\r\n                rebuild = True\r\n                row.remove(c)\r\n                rows[i+1].pack_start(c, False, False)\r\n                rows[i+1].reorder_child(c, 0)\r\n                c.show()\r\n\r\n            elif row_width < self.target_width and next_row_children:\r\n                # Next row has children, and the current row's width\r\n                # is < our target_width\r\n                next_row_first_child = next_row_children[0]\r\n                next_row_first_child_width, c_height = next_row_first_child.size_request()\r\n                if (next_row_first_child_width + row_width) < self.target_width:\r\n                    # the next row's first child will fit into the current row.\r\n                    # remove it from the next row, and add it to the current row.\r\n                    rebuild = True\r\n                    rows[i+1].remove(next_row_first_child)\r\n                    row.pack_start(next_row_first_child,False,False)\r\n                    next_row_first_child.show()\r\n                    if not rows[i+1].get_children():\r\n                        # the next row has no children so we remove it.\r\n                        rows[i+1].destroy()\r\n\r\n        self.straighten_lock = False\r\n        if rebuild:\r\n            # run straighten again to ensure crumbs are in the right place.\r\n            self.straighten()\r\n\r\n\r\nif __name__ == '__main__':\r\n\r\n    def on_remove(w, crumb, text):\r\n        print \"removing:\",text\r\n        crumb.destroy()\r\n\r\n    def on_info(w, crumb, text):\r\n        print \"This does nothing, but here's where you'd do a db lookup about that tag.\"\r\n        print \"text:\",text\r\n\r\n    w = gtk.Window()\r\n    w.set_default_size(600, 400)\r\n    w.set_position(gtk.WIN_POS_CENTER)\r\n\r\n    crbs = Crumbs()\r\n    crbs.target_width = 600\r\n    crbs.show_all()\r\n    \r\n    if \"-vbox\" in sys.argv:\r\n        # This is for testing behavior in a vbox\r\n        container = gtk.VBox()\r\n        container.pack_start(crbs, False, False)\r\n    elif \"-hbox\" in sys.argv:\r\n        # This is for testing behavior in a hbox\r\n        container = gtk.HBox()\r\n        container.pack_start(crbs, True, True) # hboxes work better with True,True\r\n    else:\r\n        # By default use a scrolled window for the container\r\n        container = gtk.ScrolledWindow()\r\n        container.set_policy(gtk.POLICY_AUTOMATIC,gtk.POLICY_AUTOMATIC)\r\n        container.add_with_viewport(crbs)\r\n\r\n    \r\n\r\n    crumb_list = [\r\n        \"Allison Krauss Yo-yo Ma, Edgar Meyer, Mark O'connor\",\r\n        \"Yo-Yo Ma, Edgar Meyer, Mark O'Connor\",\r\n        \"Yo-Yo Ma\",\r\n        \"Edgar Meyer\",\r\n        \"Mark O'Connor\",\r\n        \"Allison Krauss Yo-yo Ma\",\r\n        \"Mark O'connor\",\r\n    ]\r\n    random.shuffle(crumb_list)\r\n\r\n    for i, t in enumerate(crumb_list):\r\n        # create buttons out of crumb_list text\r\n\r\n        # Setup event boxes so we have a little bit of style for our buttons.\r\n        # (two buttons next to each other looks kinda crappy)\r\n        border = gtk.EventBox()\r\n        border.show()\r\n        border.modify_bg(gtk.STATE_NORMAL, gtk.gdk.color_parse(\"black\"))\r\n        border.set_border_width(1) # padding so there is space between our buttons.\r\n\r\n        body = gtk.EventBox()\r\n        body.show()\r\n        body.set_border_width(1) # padding so black shows.\r\n\r\n        # Style our buttons.\r\n        body.modify_bg(gtk.STATE_NORMAL, gtk.gdk.color_parse(\"white\"))\r\n        body.modify_fg(gtk.STATE_NORMAL, gtk.gdk.color_parse(\"black\"))\r\n\r\n        hbox = gtk.HBox() # For putting out buttons into.\r\n        hbox.show()\r\n\r\n        ## Tag button\r\n        btn = gtk.Button()\r\n        btn.set_property(\"relief\",gtk.RELIEF_NONE) # Remove default borders\r\n        btn.set_label(\"%s %s\" % (i,t)) # Add i debugging.\r\n        btn.connect(\"activate\", on_info, border, t)\r\n        btn.connect(\"pressed\", on_info, border, t)\r\n        btn.show()\r\n        hbox.pack_start(btn,True,True)\r\n\r\n        ## Remove button\r\n        close_image = gtk.image_new_from_stock(gtk.STOCK_CLOSE, gtk.ICON_SIZE_MENU)\r\n        btn = gtk.Button()\r\n        btn.set_property(\"relief\",gtk.RELIEF_NONE) # Remove default borders\r\n        btn.set_image(close_image)\r\n        btn.set_image_position(gtk.POS_RIGHT)\r\n        btn.connect(\"activate\", on_remove, border, t)\r\n        btn.connect(\"pressed\", on_remove, border, t)\r\n        btn.show()\r\n        hbox.pack_end(btn,False,False)\r\n\r\n        border.add(body)\r\n        body.add(hbox)\r\n        crbs.add_crumb(border)\r\n\r\n    container.show()\r\n    w.add(container)\r\n    w.connect(\"destroy\", gtk.main_quit)\r\n    w.show()\r\n    gtk.main()\r\n<\/pre>\n<\/theerm>\n","protected":false},"excerpt":{"rendered":"<p>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 [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[83,79],"tags":[],"class_list":["post-502","post","type-post","status-publish","format-standard","hentry","category-pygtk","category-python"],"_links":{"self":[{"href":"https:\/\/blog.the-erm.com\/index.php?rest_route=\/wp\/v2\/posts\/502","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/blog.the-erm.com\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/blog.the-erm.com\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/blog.the-erm.com\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/blog.the-erm.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=502"}],"version-history":[{"count":10,"href":"https:\/\/blog.the-erm.com\/index.php?rest_route=\/wp\/v2\/posts\/502\/revisions"}],"predecessor-version":[{"id":511,"href":"https:\/\/blog.the-erm.com\/index.php?rest_route=\/wp\/v2\/posts\/502\/revisions\/511"}],"wp:attachment":[{"href":"https:\/\/blog.the-erm.com\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=502"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/blog.the-erm.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=502"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/blog.the-erm.com\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=502"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}