--------------080608050401040503050008
Content-Type: text/plain; charset=ISO-8859-1; format=flowed
Content-Transfer-Encoding: 7bit

Well, I was kind of waiting to see what other people came up with, but 
since the list seems quiet on this topic, I guess I'll go ahead and post 
first.

This is a VERY rough implementation. It uses ruby-gtk2, and is one of my 
first projects using that interface, so I've doubtless done all kinds of 
things wrong. :) But it works.

By default, it displays the "main" object. You can see the class, 
superclass, instance/class variables, public/private/protected methods, 
and constants (where any of them apply and are non-empty).

I wanted to add the ability to modify values, but didn't quite have time 
to get that far.

This was a great quiz, though. I'd love to see a more sophisticated 
version of this. I can use mine, for instance, to do a kind of 
breakpoint in my code:

   ObjectBrowser.browse( @foo )

And the program will stop, display the window, and wait for the window 
to close before proceeding.

Anyway. Comments?

- Jamis

-- 
Jamis Buck
jgb3 / email.byu.edu
http://www.jamisbuck.org/jamis

--------------080608050401040503050008
Content-Type: text/plain;
 name="object-browser.rb"
Content-Transfer-Encoding: 7bit
Content-Disposition: inline;
 filename="object-browser.rb"

require 'gtk2'

DEFAULT_OBJECTBROWSER_ROOT = self

class Object
  alias :pre_objbrowser_inspect :inspect
  def inspect
    result = pre_objbrowser_inspect
    result = $1 + " ...>" if result =~ /^(#<.*?:0x\w+) /
    result
  end
end

module ObjectBrowser

  def browse( root = DEFAULT_OBJECTBROWSER_ROOT )
    Interface.new( root ).display_and_wait
  end
  module_function :browse

  class Interface
    def initialize( root = DEFAULT_OBJECTBROWSER_ROOT )
      @root = root
      Gtk.init
    end

    def display
      window = Window.new( @root )
      window.show_all
    end

    def display_and_wait
      display
      wait
    end

    def wait
      Gtk.main
    end
  end

  class Window < Gtk::Window
    OBJECT = 1
    CLASS  = 2
    INSTANCE_VARS = 3
    PUBLIC_METHODS = 4
    PROTECTED_METHODS = 5
    PRIVATE_METHODS = 6
    CLASS_VARS = 7
    CONSTANTS = 8
    SUPERCLASS = 9
    STRING = 10
    INSTANCE_METHODS = 11

    LABEL = 0
    TYPE  = 1
    REF   = 2

    def initialize( root )
      super( Gtk::Window::TOPLEVEL )

      signal_connect "delete_event", &method( :on_delete )
      signal_connect "destroy", &method( :on_destroy )

      vbox = Gtk::VBox.new
      add(vbox)

      pane = Gtk::VPaned.new
      vbox.add pane

      sw = Gtk::ScrolledWindow.new
      sw.set_policy *[Gtk::POLICY_AUTOMATIC]*2
      sw.shadow_type = Gtk::SHADOW_IN
      pane.add sw

      @model = Gtk::TreeStore.new( String, Integer, Integer )
      add_node( nil, root )

      @tree = Gtk::TreeView.new( @model )
      @tree.set_size_request -1, 400

      renderer = Gtk::CellRendererText.new

      col = Gtk::TreeViewColumn.new( "Data", renderer )
      col.set_cell_data_func renderer, &method( :on_cell_render )

      @tree.append_column col
      @tree.expand_row Gtk::TreePath.new( "0" ), false

      @tree.signal_connect "row_expanded", &method( :on_row_expanded )

      sw.add @tree

      sw = Gtk::ScrolledWindow.new
      sw.set_policy *[Gtk::POLICY_AUTOMATIC]*2
      sw.shadow_type = Gtk::SHADOW_IN
      pane.add sw

      @text = Gtk::TextView.new
      sw.add @text

      set_default_size 650, 500
    end

    def on_delete( widget, event )
      false
    end

    def on_destroy( widget )
      Gtk.main_quit
    end

    def on_cell_render( c, r, m, i )
      case i[TYPE]
        when OBJECT
          obj = ObjectSpace._id2ref( i[REF].to_i )
          r.text = "#{i[LABEL]}#{obj.inspect}"
        when CLASS, SUPERCLASS
          obj = ObjectSpace._id2ref( i[REF].to_i )
          r.text = "#{i[LABEL]} #{obj.name}"
        else
          r.text = i[LABEL]
      end
    end

    def on_row_expanded( widget, iter, path )
      unless iter.first_child[LABEL]
        case iter[1]
          when OBJECT, CLASS, SUPERCLASS then
            obj = ObjectSpace._id2ref( iter[REF].to_i )
            add_node iter, obj, iter.first_child
          when INSTANCE_VARS then
            obj = ObjectSpace._id2ref( iter.parent[REF].to_i )
            initialize_vars_list( obj, iter, obj.instance_variables.sort,
              :instance_variable_get )
          when PUBLIC_METHODS then
            obj = ObjectSpace._id2ref( iter.parent[REF].to_i )
            initialize_methods_list( obj, iter, obj.public_methods(false).sort )
          when PROTECTED_METHODS then
            obj = ObjectSpace._id2ref( iter.parent[REF].to_i )
            initialize_methods_list( obj, iter,
              obj.protected_methods(false).sort )
          when PRIVATE_METHODS then
            obj = ObjectSpace._id2ref( iter.parent[REF].to_i )
            initialize_methods_list( obj, iter,
              obj.private_methods(false).sort )
          when INSTANCE_METHODS then
            obj = ObjectSpace._id2ref( iter.parent[REF].to_i )
            initialize_methods_list( obj, iter,
              obj.instance_methods(false).sort, true )
          when CLASS_VARS then
            obj = ObjectSpace._id2ref( iter.parent[REF].to_i )
            initialize_vars_list( obj, iter,
              obj.class_variables.sort, :class_eval )
          when CONSTANTS then
            obj = ObjectSpace._id2ref( iter.parent[REF].to_i )
            constants = obj.constants
            if obj.respond_to?(:superclass) && obj.superclass
              constants = constants - obj.superclass.constants
            end
            initialize_vars_list( obj, iter, constants.sort, :const_get )
          else
            raise "don't know what to do with row of type #{iter[TYPE]}"
        end
      end

      path_str = iter.path.to_s + ":" + ( iter.n_children - 1 ).to_s
      path = Gtk::TreePath.new( path_str )

      @tree.scroll_to_cell( path, nil, true, 1.0, 0 )
    end

    def add_node( parent, object, node=nil )
      unless node
        node = add_row( parent, "", object, OBJECT, false )
        add_row( node, "class", object.class, CLASS )
      else
        add_row( parent, "class", object.class, CLASS, true, node )
        node = parent
      end

      if object.is_a?( Module )
        if object.respond_to?(:superclass) && object.superclass
          add_row( node, "extends", object.superclass, SUPERCLASS )
        end
        add_row_unless_empty(
          object.class_variables, node, "Class Variables", CLASS_VARS )

        constants = object.constants
        if object.respond_to?(:superclass) && object.superclass
          constants = constants - object.superclass.constants
        end

        add_row_unless_empty( constants, node, "Constants", CONSTANTS )
        add_row_unless_empty( object.instance_methods(false), node,
          "Instance Methods", INSTANCE_METHODS )
      end

      add_row_unless_empty( object.instance_variables, node,
        "Instance Variables", INSTANCE_VARS )
      add_row_unless_empty( object.public_methods(false), node,
        "Public Methods", PUBLIC_METHODS )
      add_row_unless_empty( object.protected_methods(false), node,
        "Protected Methods", PROTECTED_METHODS )
      add_row_unless_empty( object.private_methods(false), node,
        "Private Methods", PRIVATE_METHODS )

      node
    end

    def add_row_unless_empty( list, node, name, type, add_empty=true )
      unless list.empty?
        summary = list.sort.join( "," )
        summary = summary[0,60] + "..." if summary.length > 63
        add_row( node, "#{name} (#{summary})", nil, type )
      end
    end

    def add_row( parent, label, value, type, add_empty=true, node=nil )
      node = @model.append( parent ) unless node

      node[ LABEL ] = label
      node[ TYPE ] = type
      node[ REF ] = value.object_id

      @model.append( node ) if add_empty

      node
    end

    def initialize_methods_list( obj, iter, list, instance=false )
      node = iter.first_child
      list.each do |item|
        if instance
          method = obj.instance_method( item.to_sym )
        else
          method = obj.method( item.to_sym )
        end
        add_row iter, item + "(#{method.arity})", obj, STRING, false, node
        node = nil
      end
    end

    def initialize_vars_list( obj, iter, list, message )
      node = iter.first_child
      list.each do |item|
        value = obj.__send__( message, item )
        add_row iter, "#{item}=", value, OBJECT, true, node
        node = nil
      end
    end
  end

end

if __FILE__ == $0
  @obj = ObjectBrowser::Interface.new
  @obj.display_and_wait
end

--------------080608050401040503050008--