Webkit Table

#!/usr/bin/env python
#
# [SNIPPET_NAME: Webkit Table]
# [SNIPPET_CATEGORIES: Webkit, PyGTK, csv]
# [SNIPPET_DESCRIPTION: Shows how to load tabular data into a Webkit view]
# [SNIPPET_AUTHOR: Bruno Girin <[email protected]>]
# [SNIPPET_LICENSE: GPL]
#
# This snippet was derived from Andy Breiner's "Webkit Button" snippet and
# Ryan Paul's article at Ars Technica:
# http://arstechnica.com/open-source/guides/2009/07/how-to-build-a-desktop-wysiwyg-editor-with-webkit-and-html-5.ars/
# It demonstrates how to create a HTML table from the content of a CSV file,
# display it in a Webkit view and handle change events from a GTK combo box to
# change the document's style sheet.
#
# The garish colours for the "Colourful" style were generated using:
# http://colorschemedesigner.com/
#
# It's Easter, so this snippet shows details about the nutritional information
# of chocolate, found here: http://www.chokladkultur.se/facts.htm

import csv
import sys

import gtk
import webkit

class TableData:
    """
    Data model class that encapsulates the content of the CSV file.
    
    This class reads the content of the CSV file, stores the first row as a
    header and the other rows as a list of list representing the content.
    """
    def __init__(self, csv_file):
        reader=csv.reader(open(csv_file))
        self.headers = []
        self.content = []
        for row in reader:
            if reader.line_num == 1:
                self.headers = row
            else:
                self.content.append(row)

class TableView:
    """
    View class that displays the content of the data model class.
    
    This class creates a HTML table from the data held in the model class
    and uses Webkit to display it. It also provides the user with a combo box
    to change the style used to display the table.
    """
    def delete_event(self, widget, event, data=None):
        """Handles delete events and ignores them."""
        return False

    def destroy(self, widget, data=None):
        """Handles the destroy event and quits the application."""
        gtk.main_quit()

    def __init__(self, file_stem, title, data):
        """
        Initialises the view class, creates a HTML document and wires events.
        
        This is the main method in the view class. It initialises all elements
        of the view, creates a HTML document based on the data model and wires
        GTK and Webkit events to handling methods.
        """
        # Store the file stem
        self.file_stem = file_stem
        
        # Setup the window properties
        self.window = gtk.Window(gtk.WINDOW_TOPLEVEL)
        self.window.set_resizable(True)
        self.window.connect("delete_event", self.delete_event)
        self.window.connect("destroy", self.destroy)

        # Initialize webkit
        self.web = webkit.WebView()
        
        # listen for clicks of links
        self.web.connect("navigation-requested", self.on_navigation_requested)

        # the %s will be replaced later on
        self.template = """
            <html>
            <head>
                <style>
                {style}
                </style>
            </head>
            {body}
            </html>
        """
        self.body = """
            <body>
                <h1>{title}</h1>
                <div id="content">
                    <table>
                        <thead>
                        {thead}
                        </thead>
                        <tbody>
                        {tbody}
                        </tbody>
                    </table>
                </div>
            </body>
        """
        self.tr = """<tr>{content}</tr>"""
        self.th = """<th scope="{scope}">{content}</th>"""
        self.td = """<td class="{hclass}">{content}</td>"""

        self.document_body = self.create_document_body(title, data)
        document = self.create_document('plain')
        # tell webkit to load local html and this is where the %s will get
        # replaced
        self.web.load_html_string(document, "file:///")
 
        # Create the style combo box
        combobox = gtk.combo_box_new_text()
        combobox.append_text('Plain')
        combobox.append_text('Business')
        combobox.append_text('Rounded')
        combobox.append_text('Colourful')
        combobox.set_active(0)
        combobox.connect('changed', self.changed_style_combo)
        
        # Create a scroll area and add the webkit item
        scroll = gtk.ScrolledWindow()
        scroll.add(self.web)
        
        # Create a vbox and add the combo box and scroll area to it
        vbox = gtk.VBox()
        vbox.pack_start(combobox, False)    # don't expand
        vbox.pack_start(scroll, True, True) # expand and fill
 
        # add the vbox to the window and show all items on the window
        self.window.add(vbox)
        self.window.show_all()
        self.window.move(0, 10)
        self.window.resize(580, 350)

    def create_document_body(self, title, data):
        """
        Create the document's body from the content of the data model.
        
        This method creates the body of the document by inserting headers and
        body row elements in the core template.
        """
        # Create th nodes and wrap them in tr
        thead = self.tr.format(
            content = ''.join(
                [self.th.format(scope = 'col', content = h) for h in data.headers])
        )
        # Create td nodes, wrap the tr nodes and join them
        # The expression used to set the value of hclass is derived from:
        # http://code.activestate.com/recipes/52282-simulating-the-ternary-operator-in-python/
        # For more details on nested list comprehensions, as used below, see:
        # http://docs.python.org/tutorial/datastructures.html#nested-list-comprehensions
        tbody = '\n'.join(
            [self.tr.format(
                content = ''.join([self.td.format(
                    hclass = (i>0 and 'right' or 'left'), content = d
                    ) for i, d in enumerate(l)]))
            for l in data.content]
        )
        # Create the document body and return
        return self.body.format(
            title = title, thead = thead, tbody = tbody)
    
    def create_document(self, style):
        """
        Create the complete document from the body and the style sheet.
        
        This method creates the final document by reading the CSS style sheet
        file and inserting it along with the document body into the template.
        """
        # Load the style sheet
        f = open(
            '{stem}-{style}.css'.format(stem = self.file_stem, style = style), 'r')
        # Apply to the document and return
        return self.template.format(
            style = f.read(), body = self.document_body)

    def changed_style_combo(self, combobox):
        """
        Change the style by re-creating the document from the combo's selection.
        
        This method gets the current value out of the combo box, transforms it
        to lower case and uses the resulting value to re-create the complete
        document with the relevant CSS style sheet. It then re-displays the
        document using the Webkit view.
        """
        model = combobox.get_model()
        index = combobox.get_active()
        style = model[index][0].lower()
        print 'Changing style to {style}'.format(style = model[index][0])
        document = self.create_document(style)
        self.web.load_html_string(document, "file:///")
    
    def on_navigation_requested(self, view, frame, req, data=None):
        """
        Describes what to do when a href link is clicked.
        
        In this case, we ignore all navigation requests, as there are no
        clickable area in the document.
        """
        # As Ryan Paul stated he likes to use the prefix program:/
        uri = req.get_uri()
        if uri.startswith("program:/"):
            print uri.split("/")[1]
        else: 
            return False
        return True
  
    def main(self):
        """Start the main GTK thread."""
        gtk.main()

if __name__ == "__main__":
    data = TableData(sys.argv[0].replace('.py', '.csv'))
    view = TableView(
        sys.argv[0].replace('.py', ''),
        "Chocolate's nutritional information", data)
    view.main()