(…after too much grief today installing Mephisto and mucking with Apache virtualhosts; I’ll get Part I back from the ether eventually) Update: Done. Update: This is a post moved over from the short-lived Mephisto blog, and ported back in time.
First of all, the alexandria binary is just a ruby script that does a require ‘alexandria’ and runs Alexandria.main.
Alexandria.main is a method on the Alexandria ‘module’ that is used throughout the code (modules are ‘namespaces’ to avoid naming conflicts). This method is found in lib/alexandria.rb:
module Alexandria # ... bunch of constants used in the 'about' dialogue def self.main $DEBUG = !ENV['DEBUG'].nil? $DEBUG = true if ARGV.include? "--debug" if $DEBUG Alexandria.log.level = Logger::DEBUG end Alexandria.log.debug { "Initializing Alexandria..." } ENV['http_proxy'] = nil if !ENV['http_proxy'].nil? \ and URI.parse(ENV['http_proxy']).userinfo.nil? Alexandria::UI.main end # ... bunch of requires below here end
As you should be able to see, this method isn’t doing anything but setting up some global variables (like $DEBUG) and logging, and doing something weird with http_proxy. The real line is Alexandria::UI.main. That’s in lib/alexandria/ui.rb:
module Pango def self.ellipsizable? @ellipsizable ||= Pango.constants.include?('ELLIPSIZE_END') end end module Alexandria module UI def self.main Gnome::Program.new('alexandria', VERSION).app_datadir = Config::MAIN_DATA_DIR Icons.init MainApp.new Gtk.main end end end
Gtk.main is the main loop of a gtk program. You set up your windows and widgets before running it, and it makes them all spin until you exit. So, after Icons.init runs (guess what that does), MainApp.new does all the work from now on.
The Pango code above this is interesting for seeing some Ruby syntax and features. Pango is a text-rendering and layout library inside gtk. The code is adding an elipsizable? “question” method (return true/false) to the Pango module. self.elipsizable? means that it’s defining a class method, a method on a class that doesn’t depend on instance data. ||= is a way of saying, “set the variable to this unless it’s already been set to something else (ie, it’s not nil)”.
Unfortunately, MainApp.new is in the massive MainApp class at lib/alexandria/ui/main_app.rb. This class does a lot (too much). The main thing it does is handle all the callbacks from the main window and its widgets. Let’s just take a look at the top:
module Alexandria module UI class MainApp < GladeBase attr_accessor :main_app, :actiongroup, :appbar include Logging include GetText GetText.bindtextdomain(Alexandria::TEXTDOMAIN, nil, nil, "UTF-8") module Columns COVER_LIST, COVER_ICON, TITLE, TITLE_REDUCED, AUTHORS, ISBN, PUBLISHER, PUBLISH_DATE, EDITION, RATING, IDENT, NOTES, REDD, OWN, WANT, TAGS = (0..16).to_a end # The maximum number of rating stars displayed. MAX_RATING_STARS = 5 def initialize super("main_app.glade") @prefs = Preferences.instance load_libraries initialize_ui on_books_selection_changed restore_preferences end #... snip end # ... snip end end
A couple points here. MainApp inherits from GladeBase. The attr_accessor is a declaration that makes the @main_app, @actiongroup and @appbar instance variables publicly readable and settable. super(“main_app.glade”) calls the initialize method on GladeBase with the glade file that contains the definitions for all the widgets Alexandria uses. The names of the methods tell you about what they do (good!). Because these methods need to know about what the user’s preferences are, @prefs has been made available before they are called.
To understand what MainApp is doing, it seems like we need to understand what GladeBase is.
module Alexandria module UI class GladeBase def initialize(filename) file = File.join(Alexandria::Config::DATA_DIR, 'glade', filename) glade = GladeXML.new(file, nil, Alexandria::TEXTDOMAIN) { |handler| method(handler) } glade.widget_names.each do |name| begin instance_variable_set("@#{name}".intern, glade[name]) rescue end end end end end end
So GladeBase is using GladeXML to get the widgets out of the xml file and load them into memory. It then iterates through them, *adding them to MainApp (instance_variable_set is doing the work). So if there’s a widget called @main_menu, MainApp will get this variable to work with. These widgets work exactly as though they had been created “by hand”.
If you’ve been following, take a look at load_libraries and see if the code there makes sense. Here’s a short snippet:
def load_libraries completion_models = CompletionModels.instance if @libraries @libraries.all_regular_libraries.each do |library| if library.is_a?(Library) library.delete_observer(self) completion_models.remove_source(library) end end @libraries.reload else #On start @libraries = Libraries.instance @libraries.reload # ...
This is where things start to get confusing. load_libraries is also being used to reload libraries, so first it checks to see if @library has been defined already (refactoring opportunity). In the normal case, Libraries gets called by by invoking Libraries.instance. To understand this, you have to know that Libraries uses a factory class method to make sure that Libraries only gets created once (making the Libraries instance a “singleton”).
At the bottom of load_libraries is some interesting code:
# ... @libraries.all_regular_libraries.each do |library| library.add_observer(self) completion_models.add_source(library) end # ...
This is telling each library in @libraries (the Libraries singleton) to add self as an “observer”. What does this mean? It means that class Library is “observable”. To see what that means you have to look at Library. First let’s look at Libraries, in lib/alexandria/library.rb:
class Libraries attr_reader :all_libraries, :ruined_books include Observable include Singleton # ... snip ####### private ####### def initialize @all_libraries = [] end def notify(action, library) changed notify_observers(self, action, library) end end end
Libraries is including the Observable and Singleton modules to give it special methods (in Python these are called “mixins”). Singleton gave it the instance method. Observable is giving it the notify_observers method. What this method does is “call up” all the observers of this instance by calling their update methods.
Libraries has many Librarys (it’s a little weird to give a class a plural name). Each library is an observer of Libraries. Library is also Observable:
class Library < Array include Logging # ... include Observable
As we saw above, MainApp adds itself as an observer to each library. If you look on MainApp you’ll see that it has an update method:
def update(*ary) # ... end
*ary means that it accepts an array as its argument. This method gets called from many places in Library, like this:
source_library.notify_observers(source_library, BOOK_REMOVED, book)
That’s all for now. To learn more about Observers read this.