Setup for Alexandria Development: III

The following code requires Alexandria trunk. For more information see Cathal’s article. You can get this file here or as a full gem source package through svn:

svn co .
$:.unshift File.dirname(__FILE__)
This is a sample (but useable) app for maintaining a reading list that takes
its reading options directly from Alexandria. At the moment it allows you to
move books to the reading list, remove them, mark them as read, and reorder
them. It's commented so as to be a kind of tutorial. The reader is encouraged
to play with it and try to add functionality. 
This code demonstrates some of the most common operations in a Gtk app:
creating menus, hooking up a treeview and syncing it to data, and widget
packing. Of these, working with the TreeView will probably be the most
confusing. The basic concept behind treeviews is that a treeview is the
graphical widget that administers a "store" or short-term database (usually
either a ListStore or a TreeStore) which in turn is concerned with managing
either a list-like or tree-like structure of TreeIters. In the case of a
ListStore, a TreeIter represents a row that you see in the TreeView, and the
indices of the TreeIter (like iter[0], etc.) return the values for the columns
in that row. One thing to know about GTK Iter objects is that they like to get
"invalidated" whenever the managing View changes, so for example if you get the
iter for the user's selection of the third row of a TreeView and store it for
later, you'll find that the iter itself (as opposed to its TreePath, or
"absolute" location) is no longer available. Clear? It's definitely recommended
that you take a look at this tutorial:
module Readinglist
  require 'yaml'
  require 'alexandria'
  require 'gtk2'
  class ReadingListApp
    FILE_FORMAT = {:to_read => [], :have_read => []}
    READING_LIST_FILE = File.join(ENV["HOME"], ".reading_list.yml" )
    # I try to decompose methods as much as possible to show the procedural
    # skeleton of the program. I name the methods to read like sentences. 
    def initialize
    # We use YAML as the "serialization" strategy. That is, we make sure a hash
    # is stored in a file in the user's home directory. We load it into memory,
    # manipulate it, and always make sure to sync it back to the file. This is
    # exactly how books are loaded in Alexandria. Alexandria even stores the
    # class instance to the file. 
    def load_reading_list
      unless File.exist?(READING_LIST_FILE), 'w') do |file|
      database = YAML.load_file(File.join(ENV["HOME"], ".reading_list.yml" ))
      @reading_list = database[:to_read]
      @have_read = database[:have_read]
      puts @reading_list.inspect
    # I have to make sure the listview and the data store are in sync, and I
    # want to make sure that when the program finishes the latest changes are
    # committed to file. This could get too expensive with enough books,
    # though. In Alexandria, some changes happen immediately (changes in book
    # attributes, covers) while others only occur on a clean program shutdown
    # (deleting, saving certain preferences).
    def save_to_yaml
      database = FILE_FORMAT
      database[:to_read] = @reading_list 
      database[:have_read] = @have_read, 'w') do |file|
    # I initialize the Libraries simpleton (available through the above
    # require) and ask it to reload its list of libaries. Then the loadall class
    # method on Library gives me an array of arrays of books, which I then
    # flatten; that is, make into one long array. 
    def get_books
      libraries_simpleton = Alexandria::Libraries.instance
      libraries = Alexandria::Library.loadall
      @books = libraries.flatten
    # More declarative-style methods for erecting the GUI. This type of code
    # ends up being very mechanical to write, which is why people like to use
    # tools like Glade. In fact, keeping this gui layout code in the open is
    # probably a better idea for long-term maintenance. 
    def setup_gui
      @window.show_all # This needs to be called after widgets are packed. 
    # For information on packing, see
    # . Here
    # @window, which can only contain one child widget, gets a VBox. I'll put
    # several widgets inside @vbox, including more "container" widgets (like
    # HBox), in which I put yet more widgets.
    def setup_window
      @window ="Reading List")
      @window.add(@vbox =
    # For information on callbacks in Gtk see
    # . Ruby-gnome2
    # callbacks use the Ruby do/end block syntax. Since I don't like to define
    # the callback method inside the block, I use the method() function to turn
    # the method into a proc, and use the & syntax for passing in a "proc"
    # object to the block.
    def setup_callbacks
      @available_treeview.signal_connect("row-activated", &method(:on_row_activated))
      @window.signal_connect("delete-event", &method(:on_quit))      
    # Pretty straight-forward, if wordy. The ImageMenuItems can use a
    # Gtk::Stock:: constant to save some work and get pretty icons. :expand =>
    # false is used to keep widgets from bulging out.
    def setup_menus
      @vbox.add(menubar =, :expand => false)
      menubar.append(file_menu ="_File"))
      menubar.append(edit_menu ="_Edit"))
      menubar.append(help_menu ="_Help"))
      file_menu.submenu = file_submenu = 
      edit_menu.submenu = edit_submenu = 
      help_menu.submenu = help_submenu = 
      file_submenu.add(quit_item =
      quit_item.signal_connect("activate", &method(:on_quit))
      edit_submenu.add("Mark selected _read"))
      help_submenu.add(about_submenu =
    # To setup the "current reading list" on top. The TreeView is connected
    # directly to the ListStore, but the TreeViewColumns and the CellRenderer*s
    # connected to them are responsible for telling the TreeView _how_ to
    # display the data within the ListStore. The argument :text => n is a
    # shorthand to tell the TreeViewColumn to associate with a position or
    # index in the TreeIter (row).
    def setup_current_reading_list
      @vbox.add("Books I am reading:"), :expand => false)
      @vbox.add(hbox =
      hbox.add(scrolley1 =
      hbox.add(@button_vbox =, :expand => false)
      scrolley1.add(@reading_treeview =
      @reading_treeview.model = @reading_store =, String, String)
      renderer = 
      reading_column1 ="Order", renderer, :text => 0)
      reading_column2 ="Title", renderer, :text => 1)
      reading_column3 ="Author", renderer, :text => 2)
    def setup_side_buttons
      @button_vbox.layout_style = Gtk::VButtonBox::START 
      @button_vbox.add(up_button =
      @button_vbox.add(down_button =
      @button_vbox.add(read_button ="Read"))
      @button_vbox.add(remove_button =
      up_button.signal_connect("clicked", &method(:on_click_up))
      down_button.signal_connect("clicked", &method(:on_click_down))
      remove_button.signal_connect("clicked", &method(:on_click_remove))
      read_button.signal_connect("clicked", &method(:on_click_read))
    # Same as above, except with the added wrinkle that a TreeModelSort, using
    # a TreeModelFilter, is acting as a kind of proxy for the ListStore. This
    # is to support sorting of columns. This code is ripped off wholesale from
    # Alexandria.  
    def setup_available_books_list
      @vbox.add("Available books:"), :expand => false)
      @vbox.add(scrolley2 =
      scrolley2.add(@available_treeview =
      @list_store =, String, String)
      @filter =
      @available_treeview.model = @available_books_model =
      renderer =
      column2 ="Author", renderer, :text => 2)
      column2.resizable = true
      column2.sort_column_id = 2
    # This TreeViewColumn has two widgets, a CellRendererPixbuf for the book's
    # icon, and a regular CellRendererText for the book's Title. I have to tell
    # the CellRendererPixBuf how to display itself int the set_cell_data_func.
    # The convert_iter_to_child_iter is some kind of bookkeeping for the
    # TreeModelFilter.
    def setup_available_books_title_column
      column ="Title")
      renderer = 
      column.sizing = Gtk::TreeViewColumn::FIXED
      column.fixed_width = 200 
      column.widget ="Title").show
      column.pack_start(renderer, expand = false)
      column.set_cell_data_func(renderer) do |column, cell, model, iter|
        iter = @available_treeview.model.convert_iter_to_child_iter(iter)
        iter = @filter.convert_iter_to_child_iter(iter)
        cell.pixbuf = iter[0]
      renderer =
      renderer.ellipsize = Pango::ELLIPSIZE_END if Pango.ellipsizable?
      column.pack_start(renderer, expand = true)
      column.add_attribute(renderer, :text, 1)
      column.sort_column_id = 1
      column.resizable = true
    # This supplies the actual data to @available_treeview. Icons.cover creates
    # a Gdk::PixBuf (image object) from cover files stored in the .alexandria
    # directory.
    def load_books_into_listview
      @books.each do |book|
        icon = Alexandria::UI::Icons.cover(book.library, book)
        icon = icon.scale(20,25) 
        iter= @list_store.append
        iter[0] = icon 
        iter[1] = book.title
        iter[2] = book.authors.join(" ")
    # Call this when you want to repopulate and reorder the reading_list. 
    def refresh_reading_list 
      @count = 0
      @reading_list.each do |item| 
        iter = @reading_store.append # Gets a new iter (row) to work with.
        iter[0] = @count += 1
        iter[1] = item[0]
        iter[2] = item[1]
    # @reading_treeview.selection.selected is the iter of the selected row. 
    def on_click_remove widget
      selection = @reading_treeview.selection.selected
      @reading_list.delete_at(selection[0].to_i - 1)
    def on_click_read widget
      selection = @reading_treeview.selection.selected
      @have_read << @reading_list.delete_at(selection[0].to_i - 1)
    # The idea is to swap the iters in the TreeView and mirror the swap in the
    # reading list. The iter's path is like its current map coordinates. Since
    # iters are always getting invalidated, it's a good plan to work with the
    # path. 
    def on_click_up widget
      selection = @reading_treeview.selection.selected
      position = selection[0].to_i - 1
      previous_path = selection.path
      previous = @reading_store.get_iter(previous_path)
      @reading_store.move_before(selection, previous)
      unless (position - 1) < 0
        @reading_list.insert(position - 1, @reading_list.delete_at(position))
    # on_click_up and on_click_down call for refactoring to reduce duplicated
    # code. Try it for yourself if you're interested.  
    def on_click_down widget
      selection = @reading_treeview.selection.selected
      position = selection[0].to_i - 1
      previous_path = selection.path
      after_path = selection.path!
      after = @reading_store.get_iter(after_path) 
      unless (position + 1) == @reading_list.length
        @reading_store.move_after(selection, after)
        @reading_list.insert(position + 1, @reading_list.delete_at(position))
    # This is the callback for when a row in the lower available books listview
    # gets clicked.
    def on_row_activated widget, path, column
      iter = @available_treeview.model.get_iter(path)
      puts "#{iter[0]} #{iter[1]} #{iter[2]}"
      reading_list_item = []
      reading_iter = @reading_store.append 
      reading_iter[0] = @count += 1
      reading_iter[1] = iter[1]
      reading_list_item << iter[1]
      reading_iter[2] = iter[2]
      reading_list_item << iter[2]
      @reading_list << reading_list_item 
    # This method is called when the window is closed and when the quit menu
    # option is activated. Event is used for when the window connects to the
    # 'delete-event' signal and requires both parameters. Gtk.main_quit kills
    # the Gtk.main loop. 
    def on_quit widget, event = nil
  def self.main
Some features that could be added: 
* connect up menu items 
* figure out how save to... works; does it export to one specific format or
* Not a feature, but can the code be made cleaner, cleaner, more testable? 
* Something indescribably awesome...
A couple random tips: 
install ruby-debug gem to step through this code to see how it works
See for more information
install the utility_belt gem for colorized irb and add  
require 'rubygems'
require 'utility_belt'
to a file ~/.irbrc
if __FILE__ == $0