Custom signals with Ruby-GTK2

Fri, 05 Jun 2009 22:57:39 -0000
eng, pdo, ruby, tips

How to create a custom signal in Ruby-GTK2

I'm playing with the GTK2 bindings for Ruby. When coding GUI things, I usually like to build my own higher level widgets by subclassing containers and populating them with other widgets.

For example, I'm creating an image browser and I like to make an ImageList class that subclasses a Gtk::ScrolledWindow and builds the TreeView inside it:

class ImageList < Gtk::ScrolledWindow
    def initialize
        super

        @files = Gtk::ListStore.new(String, String, Gdk::Pixbuf)

        @fileList = Gtk::TreeView.new(@files)

        renderer = Gtk::CellRendererPixbuf.new
        col = Gtk::TreeViewColumn.new("Thumb", renderer, :pixbuf => 2)
        @fileList.append_column(col)

        renderer = Gtk::CellRendererText.new
        col = Gtk::TreeViewColumn.new("File name", renderer, :text => 1)
        @fileList.append_column(col)

        self.add @fileList
    end

    def readDir (root)
        Dir.foreach root do |dir|
            file = root+'/'+dir
            if !FileTest.directory?(file) && dir =~ /\.jpe?g$/ && FileTest.readable?(file)
                thumb = getThumbnail(file)
                iter = @files.append
                iter[0] = file
                iter[1] = dir
                iter[2] = Gdk::Pixbuf.new thumb
                File.delete(thumb)
            end
        end
    end
end

When working like this, I find myself very soon in need of creating custom signals: in this case I want to make a signal that ImageList emits when the user select an image from the TreeView, like this:

fileList.signal_connect("selected") do |filename|
      imageDetails.load(filename)
end

Now, how do I implement that extra 'selected' signal in my ImageList? Ruby documentation is usually very good and easy to find, but this time Google had little to say to me.

When the going gets tough, the tough gets to IRC::

enrico> Hi.  http://ruby-gnome2.sourceforge.jp/?News_20031116_1 says that it is
        possible to create custom signals, however I could not find any
        documentation on how to do it.  Anyone has a link or a short explanation?
pterjan> glib/sample in the source IIRC
pterjan>   # define new signal "hoge"
pterjan>   signal_new("hoge",                  # name
pterjan>              GLib::Signal::RUN_FIRST, # flags
pterjan>              nil,                     # accumulator (XXX: not supported yet)
pterjan>              nil,                     # return type (void == nil)
pterjan>              Integer, Integer         # parameter types
pterjan>              )
pterjan> in glib/sample/type-register.rb
enrico> ooh!
enrico> pterjan: thanks!
enrico> I'll blog about it, it might make it easier for others to google this answer

Thanks pterjan, and oh, coolness! So there are nice examples that I was silly enough to miss. Are they included in the Debian packages? Oh, yes! Right in /usr/share/doc/libglib2-ruby/examples/type-register.rb.

Here's the ImageList class with the new signal::

class ImageList < Gtk::ScrolledWindow
    type_register

    signal_new("selected",              # name
           GLib::Signal::RUN_FIRST, # flags
           nil,                     # accumulator (XXX: not supported yet)
           nil,                     # return type (void == nil)
           String           # parameter types
           )

    def initialize
        super

        @files = Gtk::ListStore.new(String, String, Gdk::Pixbuf)

        @fileList = Gtk::TreeView.new(@files)

        renderer = Gtk::CellRendererPixbuf.new
        col = Gtk::TreeViewColumn.new("Thumb", renderer, :pixbuf => 2)
        @fileList.append_column(col)

        renderer = Gtk::CellRendererText.new
        col = Gtk::TreeViewColumn.new("File name", renderer, :text => 1)
        @fileList.append_column(col)

        self.add @fileList

        sel = @fileList.selection

        sel.signal_connect("changed") do |sel|
            if iter = sel.selected
                puts "Double-clicked row contains name #{iter[0]}!"
                self.signal_emit("selected", iter[0])
            end
        end
    end

    def signal_do_selected(file)
          puts "Selected " + file
          #p caller
    end

    def readDir (root)
        Dir.foreach root do |dir|
            file = root+'/'+dir
            if !FileTest.directory?(file) && dir =~ /\.jpe?g$/ && FileTest.readable?(file)
                thumb = getThumbnail(file)
                iter = @files.append
                iter[0] = file
                iter[1] = dir
                iter[2] = Gdk::Pixbuf.new thumb
                File.delete(thumb)
            end
        end
    end
end

And here's the controller code that binds the signal to some effect::

imageList.signal_connect("selected") do |imagelist, filename|
      puts "File #{filename} was clicked!"
      image.load(filename)
end

It's quite easy, and (finally!) it does what I want without needing to write thousands of lines of code. YAY! And I finally fulfull my dream of connecting closures instead of callbacks to GTK signals.

Yes, I could have done it in Perl. Yes I could have done it in Python. But Ruby is soo cute!