img_4292Welcome back. In part I we created a basic HotCocoa application and took a look at the created files and some of their workings. In part II we’re going get our UI laid out and hooked up.

We’ll be creating a text field, button, scroll view and a table. When the button is pressed, for now, the content of the text field will be appended to the table view.

A good thing to keep in mind when hacking around with MacRuby and HotCocoa is that the methods available in the OS X frameworks you’re using are available in Ruby. This means, if you don’t know how to do something, the Apple documentation is awesome at trying to figure out the right methods use.

HotCocoa doesn’t have wrappers for all of the methods we need so, sometimes, we’ll drop down and call the Cocoa methods directly. Hopefully as HotCocoa matures more of these methods will have wrappers in Ruby land. I’ve created few patches, here, here, here and here for some of the wrappings I thought would be handy as I worked on this portion of our tale.

I’m going to start off with the complete listing of the code. Don’t worry if this doesn’t make any sense at the moment as we’re going to go through all of the relevant portions. Some of this should look familiar from the basic HotCocoa generated code seen in part I.

require 'hotcocoa'

class Postie
  include HotCocoa

  def start
    application(:name => "Postie") do |app|
      app.delegate = self
      window(:size => [640, 480], :center => true, :title => "Postie", :view => :nolayout) do |win|
        win.will_close { exit }

        win.view = layout_view(:layout => {:expand => [:width, :height],
                                           :padding => 0, :margin => 0}) do |vert|
          vert << layout_view(:frame => [0, 0, 0, 40], :mode => :horizontal,
                              :layout => {:padding => 0, :margin => 0,
                                          :start => false, :expand => [:width]}) do |horiz|
            horiz << label(:text => "Feed", :layout => {:align => :center})
            horiz << @feed_field = text_field(:layout => {:expand => [:width]})
            horiz << button(:title => 'go', :layout => {:align => :center}) do |b|
              b.on_action { load_feed }
            end
          end

          vert << scroll_view(:layout => {:expand => [:width, :height]}) do |scroll|
            scroll.setAutohidesScrollers(true)
            scroll << @table = table_view(:columns => [column(:id => :data, :title => '')],
                                          :data => []) do |table|
               table.setUsesAlternatingRowBackgroundColors(true)
               table.setGridStyleMask(NSTableViewSolidHorizontalGridLineMask)
            end
          end
        end
      end
    end
  end

  def load_feed
    str = @feed_field.stringValue
    unless str.nil? || str =~ /^\s*$/
      @table.dataSource.data << {:data => str}
      @table.reloadData
    end
  end
end

Postie.new.start

I’m going to skip the parts that we saw in part I and just mention the changes and new additions in part II.

window(_:size => [640, 480], :center => true_, :title => "Postie", :view => :nolayout)

Instead of using the :frame => [100, 100, 500, 500] as seen in part I, I prefer to use :size => [640, 480]and:center => true to set the window with a starting size of 640x480 position in the center of the screen.

You’ll also notice the addition of :view => :nolayout tacked on the end of the window method. This isn’t strictly necessary but saves the creation of an object we’re just going to destroy anyway. By default when a window is created a LayoutView will be created and appended to the window. I’m going to be creating my own layout and overriding the created one so I’m just telling the window to skip the creation of the default view.

Before we dig into the next chunk of code lets take a little diversion to look at layout_view. The layout_view is one of the basic building blocks for organizing your application layout. The layout_view method will create a LayoutView object which is a subclass of NSView. Any Cocoa methods available for an NSView can be called on a LayoutView.

When working with layout_view there are a few parameters we’re interested in. The first, similar to window is :frame. As with window the :frame parameter allows us to set the frame position and size for the view. Note, if you don’t set a :frame then, it appears, that the view may have 0 size. This can be changed by the children placed in the view but not always. I spent a while trying to figure out why removing the :frame => [0, 0, 0, 40] from the code above caused my label, text field and button to disappear.

A handy little trick when working with layout_view is to run your application in the $DEBUG mode of Ruby. When $DEBUG is active each layout will have a red border.

To execute your application in $DEBUG you can do:

titania:Postie dj2$ macruby -d lib/application.rb

Other parameters we’re using for the layout_view calls are :mode, :margin, :spacing and :layout.

``:mode``
Lets us specify if this view has a ``:vertical`` or ``:horizontal`` layout. The default layout is ``:vertical``.
``:margin``
Allows us to specify a margin size for the layout. The provided value is a single integer which will be applied to top, bottom, left and right margins of the view.
``:spacing``
Allows us to set the spacing for items placed into the view. The value is a single integer.

The last option we’re going to look at is :layout. The layout option isn’t restricted to just layout_view calls and is available on all of the other widgets I’ve created so far.

The :layout hash will be turned into a LayoutOptions object. The available keys are: :start, :expand, :padding, :[left | right | bottom | top]_padding and :align.

``:start``
Signifies if the view is packed at the start or end of the packing view. I'll admit, I don't really know what that means. I'm stealing it from the ``LayoutOption docs``. It appears, in my limited fiddling, that setting it to ``false`` causes your subviews to end up at the top of the layout. The default value is ``true``.
``:expand``
Specifies how the view will grow when the window is resized. The available options are: ``:height``, ``:width``, ``[:height, :width]`` and ``nil``. The default setting is ``nil``.
``:*padding``
Allows you to set the padding around the view. The padding values are specified as a float with a default of ``0.0``.
``:align``
Allows us to specify the alignment of the view as long as it isn't set to ``:expand`` in the other direction. The available options are: ``:left``, ``:center``, ``:right``, ``:top`` and ``:bottom``.

With that out of the way, back to our code. As you can see by the layout image, our layout isn’t overly complicated. All of the layout is handled by two layout views and a scroll view.

win.view = layout_view(:layout => {:expand => [:width, :height], :padding => 0}) do |vert|

postie_layoutWe start by creating the main window view. If you remember, we created the window with :view => :nolayout so there is currently no view in our window. We assign the new layout_view to the win.view instead of using << to pack it into the view. We remove the padding on the view and set the expand to [:width, :height] so the view will fill the entire window and resize correctly.

Since we didn’t specify a :mode the main window will layout its packed widgets in a vertical fashion.

As each layout view is created you can attach a block to the layout_view call. The block will be called with the LayoutView object that was just created. This makes it easy to pack subviews into the views as they’re created.

vert << layout_view(:frame => [0, 0, 0, 40], :mode => :horizontal,
                    :layout => {:padding => 0, :start => false, :expand => [:width]}) do |horiz|

Next we pack a horizontal view (:mode => :horizontal) to hold the label, text field and button. We set the view to :expand => [:width] so we’ll only get horizontal expansion and maintain the height specified in our :frame parameter. You’ll notice we’re setting a :frame on this layout_view. If we don’t have this parameter it appears that the view will be drawn with 0 height. Effectively making the view invisible.

horiz << label(:text => "Feed", :layout => {:align => :center})
horiz << @feed_field = text_field(:layout => {:expand => [:width]})
horiz << button(:title => 'go', :layout => {:align => :center}) do |b|
  b.on_action { load_feed }
end

Into the horizontal layout we pack our label, text_field and button. For both the label and button we’re specifying an :align => :center to line them up with the center of the text field. The only item we’re setting an expand on is the text_field. The other two widgets will maintain their positions and sizes when the window is resized.

When we create the button we attach a block for the on_action callback. This will be triggered when the button is pressed. In our case we’re just calling the load_feed method.

vert << scroll_view(:layout => {:expand => [:width, :height]}) do |scroll|
  scroll.setAutohidesScrollers(true)

With the horizontal view out of the way we create a scroll_view and pack it into our main vertical view. We want the scroll view to :expand => [:width, :height] so it fills the entire window on resize. There is currently no exposed HotCocoa sugar for auto hiding the scrollbars so we drop down and call setAutohidesScrollers to set the scrollbars to auto hide.

scroll << @table = table_view(:columns => [column(:id => :data, :title => '')],
                              :data => []) do |table|
  table.setUsesAlternatingRowBackgroundColors(true)
  table.setGridStyleMask(NSTableViewSolidHorizontalGridLineMask)
end

The last view we pack is a table_view. We’re again dropping down to Cocoa for the setUsesAlternatingRowBackgroundColors and setGridStyleMask.

Now, the table_view itself. The table view is more complicated then the other widgets we’ve looked at in that it requires column and data source information.

The column information is provided by the :columns => [column(:id => :data, :title => '')] parameter. You provide an array of column objects which define the table columns. The :title will be displayed at the top of a column. If you don’t provide a :title the default is Column. We also providing an :id parameter to our column method. This parameter will be passed when we’re accessing our data source, and, if you’re using the default data source, used to access the specific column information. We’ll dig into columns and their relations to data sources in a moment.

There are two ways to provide data source information. You can either pass an array, which is what we’re doing here or provide your own data source object.

If you decide to define your own data source then it must respond to numberOfRowsInTable(tableView) => integer and tableView(view, objectValueForTableColumn:column, row:i) => string.

If you opt to use the default data source then you provide an array of hashes. The keys into the hash are the :id values set when we created our columns.

picture-11Hopefully an example will make this a bit clearer. Lets say we want to create a table with three columns, name, age and sex. We would define our table as:

@table = table_view(:columns => [column(:id => :name, :title => 'Name'),
                                 column(:id => :age, :title => 'Age'),
                                 column(:id => :sex, :title => 'Sex')],
                    :data => [{ :name => 'Betty Sue', :age => 29, :sex => 'F' },
                              { :name => 'Brandon Oberon', :age => 0.005, :sex => 'M' },
                              { :name => 'Sally Joe', :age => 48, :sex => 'F'}])

With the table view finished all that’s left is to define the load_feed method. For this first iteration we’re just going to take the content of the feed text field and load it into our table view.

def load_feed
  str = @feed_field.stringValue
  unless str.nil? || str =~ /^\s*$/
    @table.dataSource.data << {:data => str}
    @table.reloadData
  end
end

Postie V1We use the Cocoa stringValue method to retrieve the value set into the text field. As long as it isn’t blank we append a new hash (with our :data key as defined in our column setup) into the @table.dataSource.data. We then call @table.reloadData to get the table to re-display its content.

That’s it. You should be able to run the application, enter some text in the text field, hit go and see the text appear in the table view. As you add more table rows, scrollbars should appear and you can scroll in the table as needed.

The third part of this series will be, hopefully, pulling real data back from PostRank and displaying it in a custom cell in our table.

Update: Part III is now available.