Armed with a text editor

mu's views on program and recipe! design

#!/usr/bin/env python

import gtk, gobject
from warnings import warn

class CellRendererProgressMaskStateWarning(UserWarning): pass

class CellRendererProgressMask(gtk.GenericCellRenderer):

    __gproperties__ = {
        'slots': (object, 'slots', 'Mask of filled bins',
            gobject.PARAM_READWRITE),
    }

    def on_get_size(self, widget, cell):
        return 0, 0, -1, self.props.ypad + 6 

    def do_get_property(self, property):
        return getattr(self, '_CellRendererProgressMask__' + property.name)

    def do_set_property(self, property, value):
        setattr(self, '_CellRendererProgressMask__' + property.name, value)

    def set_states(self, states):
        self.__states = states

    def on_render(self, window, widget, bg_area, cell, expose, flags):
        cr = window.cairo_create()
        cr.rectangle(expose.x, expose.y, expose.width, expose.height)
        cr.clip()

        xpad = self.props.xpad
        xalign = self.props.xalign
        ypad = self.props.ypad
        yalign = self.props.yalign
        cr.translate(cell.x + xpad * xalign, cell.y + ypad * yalign)

        slots = self.props.slots
        if not slots: return
        bins = len(slots)

        def draw(start, width, cr=cr, a=[1.0]):
            try:
                cr.set_source_rgba(*(list(self.__states[start[0]]) + a)[:4])
            except (IndexError, KeyError):
                warn('Invalid State %r in slot %d' % start,
                        CellRendererProgressMaskStateWarning)
            else:
                cr.rectangle(start[1], 0, width, 1)
                cr.fill()

        cr.save()
        cr.scale(float(cell.width - xpad) / bins, cell.height - ypad)
        start = slots[0], 0
        for i, val in enumerate(slots):
            if val != start[0]: # collect like-bins to avoid striping
                draw(start, i - start[1])
                start = val, i
        else:
            draw(start, i + 1 - start[1])

        def round(x, y, cr=cr):
            x, y = map(int, cr.user_to_device(x, y))
            return cr.device_to_user(x + 0.5, y + 0.5)

        one_pixel = cr.device_to_user_distance(1, 1)
        if max(one_pixel) < 0.25:
            cr.set_line_width(min(one_pixel))
            cr.set_source_rgba(0, 0, 0, 0.9)
            for i in xrange(1, bins):
                cr.move_to(*round(i, 0))
                cr.line_to(*round(i, 1))
            cr.stroke()

        cr.restore()
        cr.rectangle(0.5, 0.5, cell.width - xpad, cell.height - ypad)

        if flags & gtk.CELL_RENDERER_PRELIT:
            cr.set_source_rgba(1, 1, 1, 0.25)
            cr.fill_preserve()

        cr.set_source_rgba(0, 0, 0, 0.9)
        cr.set_line_width(1)
        cr.stroke()

def __main__():
    model = gtk.ListStore(int, object)

    model.append([5, [1, 0, 2, 0, 1]])
    model.append([6, [0, 1, 0, 1, 0, 1]])
    model.append([7, [1, 0] * 4])
    model.append([8, [0, 1] * 4])
    model.append([31, [1, 1] * 16])
    model.append([63, [1, 1] * 32])
    model.append([1023, [1, 1, 1, 0, 2, 2, 2, 0] * 128])
    
    crpm = CellRendererProgressMask()
    tv = gtk.TreeView(model)
    #tv.append_column(gtk.TreeViewColumn('bins', gtk.CellRendererText(), text=0))
    tv.append_column(gtk.TreeViewColumn('progress', crpm, slots=1))

    crpm.set_states([(1, 1, 1, 0.3), (0, 0.5, 0), (0.8, 0, 0)])
    #crpm.props.xpad = 10
    #crpm.props.xalign = 0.0
    crpm.props.ypad = 8
    #crpm.props.yalign = 0.5

    for column in tv.get_columns():
        column.set_resizable(True)

    win = gtk.Window()
    win.add(tv)
    tv.show()
    win.connect('delete-event', gtk.main_quit)
    win.present()
    gtk.main()

if __name__ == '__main__': __main__()