[macruby-changes] [1676] MacRubyWebsite/trunk/content

source_changes at macosforge.org source_changes at macosforge.org
Sat May 30 22:29:42 PDT 2009


Revision: 1676
          http://trac.macosforge.org/projects/ruby/changeset/1676
Author:   mattaimonetti at gmail.com
Date:     2009-05-30 22:29:41 -0700 (Sat, 30 May 2009)
Log Message:
-----------
added d2j's articles

Modified Paths:
--------------
    MacRubyWebsite/trunk/content/css/master.css

Added Paths:
-----------
    MacRubyWebsite/trunk/content/hotcocoa/application_layout.txt
    MacRubyWebsite/trunk/content/hotcocoa/functionality.txt
    MacRubyWebsite/trunk/content/hotcocoa/getting_started.txt

Modified: MacRubyWebsite/trunk/content/css/master.css
===================================================================
--- MacRubyWebsite/trunk/content/css/master.css	2009-05-31 05:07:55 UTC (rev 1675)
+++ MacRubyWebsite/trunk/content/css/master.css	2009-05-31 05:29:41 UTC (rev 1676)
@@ -336,3 +336,27 @@
 .hc_status_partial { color: blue; }
 .hc_status_complete { color: green; }
 .hc_status_skip { color: black; }
+
+img.alignleft, img.alignright {
+  padding: 5px;
+}
+img.alignleft { float: left; padding-left: 0; }
+img.alignright { float: right; padding-right: 0; }
+
+th {
+  background-color: #000;
+  color: #fff;
+  font-weight: bold;
+  text-align: center;
+  padding-bottom: 5px;
+  padding-top: 5px;
+}
+td {
+  border: 1px solid #eee;
+  padding: 2px;
+}
+
+dt { font-style: italic; }
+dd { padding-left: 15px; }
+dl { padding-bottom: 10px; }
+

Added: MacRubyWebsite/trunk/content/hotcocoa/application_layout.txt
===================================================================
--- MacRubyWebsite/trunk/content/hotcocoa/application_layout.txt	                        (rev 0)
+++ MacRubyWebsite/trunk/content/hotcocoa/application_layout.txt	2009-05-31 05:29:41 UTC (rev 1676)
@@ -0,0 +1,220 @@
+---
+title:      HotCocoa Tutorial -- Application Layout
+created_at: 2009-05-29 22:10:00.0 -05:00
+updated_at: 2009-05-29 22:10:00.0 -05:00
+tutorial:   true
+author:     dj2
+filter:
+  - erb
+  - textile
+---
+h1(title). <%= h(@page.title) %>
+
+<div class="author">
+  By <%= member_name(@page.author) %>
+</div>
+
+<div class='tutorial'>
+
+h3. Introduction
+
+Welcome back. In "HotCocoa Tutorial -- Getting Started":/hotcocoa/getting_started.html we created a basic "HotCocoa":http://www.macruby.org/trac/wiki/HotCocoa application and took a look at the created files and some of their workings. In this installment 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.
+
+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 the getting started tutorial
+
+<% coderay :lang => 'ruby' do -%>
+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
+<% end %>
+
+I'm going to skip the parts that we saw in part I and just mention the changes and new additions in part II.
+
+<% coderay :lang => 'ruby' do -%>
+window(<em>:size => [640, 480], :center => true</em>, :title => "Postie", :view => :nolayout)
+<% end %>
+
+Instead of using the <code>:frame => [100, 100, 500, 500]</code> as seen in part I, I prefer to use <code>:size => [640, 480]</code><code> and </code><code>:center => true</code> to set the window with a starting size of 640x480 position in the center of the screen.
+
+You'll also notice the addition of <code>:view => :nolayout</code> tacked on the end of the <code>window</code> 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 <code>LayoutView</code> 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 <code>layout_view</code>. The <code>layout_view</code> is one of the basic building blocks for organizing your application layout. The <code>layout_view</code> method will create a <code>LayoutView</code> object which is a subclass of <code>NSView</code>. Any Cocoa methods available for an <code>NSView</code> can be called on a <code>LayoutView</code>.
+
+When working with <code>layout_view</code> there are a few parameters we're interested in. The first, similar to <code>window</code> is <code>:frame</code>. As with <code>window</code> the <code>:frame</code> parameter allows us to set the frame position and size for the view. Note, if you don't set a <code>:frame</code> 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 <code>:frame => [0, 0, 0, 40]</code> from the code above caused my label, text field and button to disappear.
+
+A handy little trick when working with <code>layout_view</code> is to run your application in the  <code>$DEBUG</code> mode of Ruby. When <code>$DEBUG</code> is active each layout will have a red border.
+
+To execute your application in <code>$DEBUG</code> you can do:
+
+<% coderay :lang => 'sh' do -%>
+titania:Postie dj2$ macruby -d lib/application.rb
+<% end %>
+
+Other parameters we're using for the <code>layout_view</code> calls are <code>:mode</code>, <code>:margin</code>, <code>:spacing</code> and <code>:layout</code>.
+
+<dl>
+ <dt><code>:mode</code></dt>
+ <dd>Lets us specify if this view has a <code>:vertical</code> or <code>:horizontal</code> layout. The default layout is <code>:vertical</code>.</dd>
+
+ <dt><code>:margin</code></dt>
+ <dd>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.</dd>
+
+ <dt><code>:spacing</code></dt>
+ <dd>Allows us to set the spacing for items placed into the view. The value is a single integer.</dd>
+</dl>
+
+The last option we're going to look at is <code>:layout</code>. The layout option isn't restricted to just <code>layout_view</code> calls and is available on all of the other widgets I've created so far.
+
+The <code>:layout</code> hash will be turned into a <code>LayoutOptions</code> object. The available keys are: <code>:start</code>, <code>:expand</code>, <code>:padding</code>, <code>:[left | right | bottom | top]_padding</code> and <code>:align</code>.
+
+<dl>
+ <dt><code>:start</code></dt>
+ <dd>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 <code>LayoutOption docs</code>. It appears, in my limited fiddling, that setting it to <code>false</code> causes your subviews to end up at the top of the layout. The default value is <code>true</code>.</dd>
+
+ <dt><code>:expand</code></dt>
+ <dd>Specifies how the view will grow when the window is resized. The available options are: <code>:height</code>, <code>:width</code>, <code>[:height, :width]</code> and <code>nil</code>. The default setting is <code>nil</code>.</dd>
+
+ <dt><code>:*padding</code></dt>
+ <dd>Allows you to set the padding around the view. The padding values are specified as a float with a default of <code>0.0</code>.</dd>
+
+ <dt><code>:align</code></dt>
+ <dd>Allows us to specify the alignment of the view as long as it isn't set to <code>:expand</code> in the other direction. The available options are: <code>:left</code>, <code>:center</code>, <code>:right</code>, <code>:top</code> and <code>:bottom</code>.</dd>
+</dl>
+
+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.
+
+<% coderay :lang => 'ruby' do -%>
+win.view = layout_view(:layout => {:expand => [:width, :height], :padding => 0}) do |vert|
+<% end %>
+
+<a href="/images/hotcocoa/layout.png"><img src="/images/hotcocoa/layout_thumb.png" class="alignright" /></a> We start by creating the main window view. If you remember, we created the window with <code>:view => :nolayout</code> so there is currently no view in our window. We assign the new <code>layout_view</code> to the <code>win.view</code> instead of using <code>&lt;&lt;</code> to pack it into the view. We remove the padding on the view and set the expand to <code>[:width, :height]</code> so the view will fill the entire window and resize correctly.
+
+Since we didn't specify a <code>:mode</code> 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 <code>layout_view</code> call. The block will be called with the <code>LayoutView</code> object that was just created. This makes it easy to pack subviews into the views as they're created.
+
+<br />
+
+<% coderay :lang => 'ruby' do -%>
+vert << layout_view(:frame => [0, 0, 0, 40], :mode => :horizontal,
+                    :layout => {:padding => 0, :start => false, :expand => [:width]}) do |horiz|
+<% end %>
+
+Next we pack a horizontal view (<code>:mode => :horizontal</code>) to hold the label, text field and button. We set the view to <code>:expand => [:width]</code> so we'll only get horizontal expansion and maintain the height specified in our <code>:frame</code> parameter. You'll notice we're setting a <code>:frame</code> on this <code>layout_view</code>. If we don't have this parameter it appears that the view will be drawn with 0 height. Effectively making the view invisible.
+
+<% coderay :lang => 'ruby' do -%>
+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 %>
+
+Into the horizontal layout we pack our <code>label</code>, <code>text_field</code> and <code>button</code>. For both the label and button we're specifying an <code>:align => :center</code>  to line them up with the center of the text field. The only item we're setting an expand on is the <code>text_field</code>. The other two widgets will maintain their positions and sizes when the window is resized.
+
+When we create the <code>button</code> we attach a block for the <code>on_action</code> callback. This will be triggered when the button is pressed. In our case we're just calling the <code>load_feed</code> method.
+
+<% coderay :lang => 'ruby' do -%>
+vert << scroll_view(:layout => {:expand => [:width, :height]}) do |scroll|
+  scroll.setAutohidesScrollers(true)
+<% end %>
+
+With the horizontal view out of the way we create a <code>scroll_view</code> and pack it into our main vertical view. We want the scroll view to <code>:expand => [:width, :height]</code> 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 <code>setAutohidesScrollers</code> to set the scrollbars to auto hide.
+
+<% coderay :lang => 'ruby' do -%>
+scroll << @table = table_view(:columns => [column(:id => :data, :title => '')],
+                              :data => []) do |table|
+  table.setUsesAlternatingRowBackgroundColors(true)
+  table.setGridStyleMask(NSTableViewSolidHorizontalGridLineMask)                             
+end
+<% end %>
+
+The last view we pack is a <code>table_view</code>.  We're again dropping down to Cocoa for the <code>setUsesAlternatingRowBackgroundColors</code> and <code>setGridStyleMask</code>.
+
+Now, the <code>table_view</code> 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 <code>:columns => [column(:id => :data, :title => '')]</code> parameter. You provide an array of <code>column</code> objects which define the table columns. The <code>:title</code> will be displayed at the top of a column. If you don't provide a <code>:title</code> the default is <em>Column</em>. We also providing an <code>:id</code> 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 <code>numberOfRowsInTable(tableView) => integer</code> and <code>tableView(view, objectValueForTableColumn:column, row:i) => string</code>. 
+
+If you opt to use the default data source then you provide an array of hashes. The keys into the hash are the <code>:id</code> values set when we created our columns.
+
+<img src="/images/hotcocoa/data_cell.png" class="alignright" />Hopefully 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:
+
+<% coderay :lang => 'ruby' do -%>
+ at 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'}])
+<% end %>
+
+With the table view finished all that's left is to define the <code>load_feed</code> 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.
+
+<% coderay :lang => 'ruby' do -%>
+def load_feed
+  str = @feed_field.stringValue
+  unless str.nil? || str =~ /^\s*$/
+    @table.dataSource.data << {:data => str}
+    @table.reloadData
+  end
+end
+<% end %>
+
+<img src="/images/hotcocoa/final_layout.png" class="alignleft" />We use the Cocoa <code>stringValue</code> method to retrieve the value set into the text field. As long as it isn't blank we append a new hash (with our <code>:data</code> key as defined in our <code>column</code> setup) into the <code>@table.dataSource.data</code>. We then call <code>@table.reloadData</code> 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":http://postrank.com and displaying it in a custom cell in our table.
+
+</div>
\ No newline at end of file

Added: MacRubyWebsite/trunk/content/hotcocoa/functionality.txt
===================================================================
--- MacRubyWebsite/trunk/content/hotcocoa/functionality.txt	                        (rev 0)
+++ MacRubyWebsite/trunk/content/hotcocoa/functionality.txt	2009-05-31 05:29:41 UTC (rev 1676)
@@ -0,0 +1,280 @@
+---
+title:      HotCocoa Tutorial -- Adding Functionality
+created_at: 2009-05-29 22:11:00.0 -05:00
+updated_at: 2009-05-29 22:11:00.0 -05:00
+tutorial:   true
+author:     dj2
+filter:
+  - erb
+  - textile
+---
+h1(title). <%= h(@page.title) %>
+
+<div class="author">
+  By <%= member_name(@page.author) %>
+</div>
+
+<div class='tutorial'>
+
+h3. Introduction
+
+In the words of Homer Simpsons, "forward not backwards, upwards not forwards and always twirling, twirling, twirling towards freedom". With that, we're back for part III of my "HotCocoa":http://www.macruby.org/trac/wiki/HotCocoa tutorial. For those of you just joining the party, you'll probably want to take a look at "Getting Started":/hotcocoa/getting_started.html and "Application Layout":/hotcocoa/application_layout.html.
+
+The latest code to our application is available "on GitHub":http://github.com/dj2/Postie.
+
+<a href="/images/hotcocoa/functional_app.png"><img src="/images/hotcocoa/functional_app_thumb.png" class="alignright" /></a>When we last left off we'd created the basic layout for our application with our button and table views setup. With this installment, we're going to go a step further and get a fully working application. We're going to use the feed entered into the text field to query PostRank to get the current posts in the feed along with there PostRank and metric information. I'm going to be skipping over sections of code that haven't changed from part I or part II for the sake of brevity. 
+
+OK, let's go.
+<br /><br />
+
+<% coderay :lang => 'ruby' do -%>
+POSTRANK_URL_BASE = "http://api.postrank.com/v2"
+APPKEY = "appkey=Postie"
+<% end %>
+
+All of our calls to PostRank will use the same URL prefix and we'll need to provide our appkey. I've placed both of these into constants.
+
+<% coderay :lang => 'ruby' do -%>
+vert << scroll_view(:layout => {:expand => [:width, :height]}) do |scroll|
+  scroll.setAutohidesScrollers(true)
+
+  pr_column = column(:id => :postrank, :title => '')
+  pr_column.setDataCell(PostRankCell.new)
+  pr_column.setMaxWidth(34)
+  pr_column.setMinWidth(34)
+            
+  info_column = column(:id => :data, :title => '')
+  info_column.setDataCell(PostCell.new)
+            
+  scroll << @table = table_view(:columns => [pr_column, info_column],
+                                :data => []) do |table|
+    table.setRowHeight(PostCell::ROW_HEIGHT)
+    table.setUsesAlternatingRowBackgroundColors(true)
+    table.setGridStyleMask(NSTableViewSolidHorizontalGridLineMask)                             
+    table.setDelegate(self)
+    table.setDoubleAction(:table_clicked)
+  end
+end
+<% end %>
+
+I've made one layout modification which was to add an extra column to our table to display the PostRank for each post. The PostRank column and post data columns use custom cell formatters so we can get the layout we want. I also wanted to constrain the PostRank column to a set size, 34 pixels seemed to look good.  In order to use my custom formatters I use <code>setDataCell</code> on the <code>column</code> objects. The parameter to <code>setDataCell</code> is an instantiated instance of our formatter class. I have two classes, <code>PostRankCell</code> and <code>PostCell</code> for the PostRank and post columns respectively.
+
+Along with the column changes we're also setting a default height on the table rows as defined in the <code>PostCell</code> class.  We set the Postie instance as the delegate for the table so we can receive the <code>tableView(table, heightOfRow:row)</code> callback (thanks "@macruby":http://www.twitter.com/macruby for the pointer).  The last addition to the table is to hookup the double click action with <code>@table.setDoubleAction(:table_clicked)</code>. The parameter is the name of the method that will be called, as a symbol.
+
+<% coderay :lang => 'ruby' do -%>
+def table_clicked
+  url = NSURL.URLWithString(@table.dataSource.data[@table.clickedRow][:data][:link])
+  NSWorkspace.sharedWorkspace.openURL(url)
+end
+<% end %>
+
+When a table row is double clicked we want to open the corresponding posts page in the users brower. We'll be storing the link in the data attached to our table. We can get the clicked row with <code>@table.clickedRow</code> and access the link with <code>@table.dataSource.data[@table.clickedRow][:data][:link]</code>. We then create a <code>NSURL</code> with this string. The created URL object is passed to <code>NSWorkspace.sharedWorkspace.openURL(url)</code> causing the page to open in the browser.
+
+<% coderay :lang => 'ruby' do -%>
+def tableView(table, heightOfRow:row)
+  metrics = @table.dataSource.data[row][:data][:metrics].keys.length
+    
+  num_rows = (metrics / PostCell::NUM_METRICS_PER_ROW) + 1
+  num_rows -= 1 if metrics > 0 && (metrics % PostCell::NUM_METRICS_PER_ROW) == 0
+  num_rows = 0 if metrics == 0
+
+  num_rows * PostCell::ROW_HEIGHT + PostCell::ROW_HEIGHT  # 2nd row height for the title
+end
+<% end %>
+
+<a href="/images/hotcocoa/cell_wrapping.png"><img src="/images/hotcocoa/cell_wrapping_thumb.png" class="alignright" /></a>The <code>tableView(table, heightOfRow:row)</code> callback is triggered each time the table is rendered out to determine the height for a given row. In the case of Postie we're going to display the post title on the first line and the metrics on subsequent lines. I've constrained the metrics to allow a maximum of 6 metrics on each line.  All of the metrics are stored in a <code>:metrics</code> key of the data attached to our table. Both the number of metrics and a row and the row height are constants stored in the <code>PostCell</code> class.
+
+<% coderay :lang => 'ruby' do -%>
+def load_feed
+  @table.data = []
+
+  str = @feed_field.stringValue
+  unless str.nil? || str =~ /^s*$/
+    fetch_feed(str)
+  end
+end
+<% end %>
+
+<code>load_feed</code> has been updated to empty our tables data by assigning a new array and, assuming we've received a valid feed, call <code>fetch_feed</code> to start retrieving the feed data.
+
+There are a few different ways we could go about querying the data from the PostRank APIs. We could use <em>Net::HTTP</em>, <em>Curb</em>, <em>NSXMLDocument</em> or, as I've done, <em>NSURLConnection</em>. The reason I used <code>NSURLConnection</code> is so that I can have the requests run asynchronously. As well, the UI won't block as we're off fetching the data. A handy feature when you want things to remain responsive.
+
+Let's take a quick look at the wrapper class I've put around <code>NSURLConnection</code> before looking at <code>fetch_feed</code>.  The reason I created a wrapper is that <code>NSURLConnection</code>, because it's asynchronous, works through callbacks. I'm going to need to query three different PostRank APIs and take different actions for each query. Instead of trying to do some magic in the callbacks, I've created a wrapper class that accepts a block. The block is called when the data has been successfully retrieved. (The wrapper just spits out an error if something goes wrong, the block is never called.) For example, to download the Google.ca homepage we could do:
+
+<% coderay :lang => 'ruby' do -%>
+DataRequest.new("http://google.ca") do |data|
+  NSLog "Data: #{data}"
+end
+<% end %>
+
+<% coderay :lang => 'ruby' do -%>
+class DataRequest
+  def get(url, &blk)
+    @buf = NSMutableData.new
+    @blk = blk
+    req = NSURLRequest.requestWithURL(NSURL.URLWithString(url))
+    NSURLConnection.alloc.initWithRequest(req, delegate:self)
+  end
+  
+  def connection(conn, didReceiveResponse:resp)
+    @buf.setLength(0)
+  end
+  
+  def connection(conn, didReceiveData:data)
+    @buf.appendData(data)
+  end
+
+  def connection(conn, didFailWithError:err)
+    NSLog "Request failed"
+  end
+  
+  def connectionDidFinishLoading(conn)
+    @blk.call(NSString.alloc.initWithData @buf, encoding:NSUTF8StringEncoding)
+  end
+end
+<% end %>
+
+As <code>NSURLConnection</code> executes it returns data to the application. We're storing this data in a <code>NSMutableData</code> object called <code>@buf</code>.  The callbacks we're interested in are:
+<dl>
+ <dt><code>connection(conn, didReceiveResponse:resp)</code></dt>
+ <dd>Called when we receive a response from the server. This can be called multiple times if there are any server redirects in place. We reset the length of our data buffer each time this callback is called.</dd>
+ <dt><code>connection(conn, didReceiveData:data)</code></dt>
+ <dd>Called each time data is received from the server. This can be called multiple times and we just append the data to our buffer each time.</dd>
+ <dt><code>connection(conn, didFailWithError:err)</code></dt>
+ <dd>Called if there is an error retrieving the data. We, basically, just ignore the error. You'd probably want to do something sane in your application.</dd>
+ <dt><code>connectionDidFinishLoading(conn)</code></dt>
+ <dd>Called when all of the data has been retrieved from the remote server. Since we're not working with binary data I convert the <code>NSMutableData</code> to an <code>NSString</code> using the <code>initWithData:encoding</code> method. Note the use of <code>alloc</code> on the <code>NSString</code>. If you try to use <code>new</code> you'll, like me, spend the next 30 minutes trying to figure out why your application is crashing.</dd>
+</dl>
+
+<% coderay :lang => 'ruby' do -%>
+def fetch_feed(url)
+  DataRequest.new.get("#{POSTRANK_URL_BASE}/feed/info?id=#{url}&#{APPKEY}") do |data|
+    feed_info = JSON.parse(data)
+    unless feed_info.has_key?('error')
+      DataRequest.new.get("#{POSTRANK_URL_BASE}/feed/#{feed_info['id']}?#{APPKEY}") do |data|
+        feed = JSON.parse(data)
+        feed['items'].each do |item|
+          post_data = {:title => item['title'], :link => item['original_link'], :metrics => {}}
+          @table.dataSource.data << {:data => post_data,
+                                     :postrank => {:value => item['postrank'],
+                                                   :color => item['postrank_color']}}
+          DataRequest.new.get("#{POSTRANK_URL_BASE}/entry/#{item['id']}/metrics?#{APPKEY}") do |data|
+            metrics = JSON.parse(data)
+            metrics[item['id']].each_pair do |key, value|
+              next if key == 'friendfeed_comm' || key == 'friendfeed_like'
+              post_data[:metrics][key.to_sym] = value
+            end
+            @table.reloadData
+          end
+        end
+      end
+    end
+  end
+end
+<% end %>
+
+Most of the code in <code>fetch_feed</code> should probably be refactored into <code>Feed</code>, <code>Post</code> and <code>Metrics</code> classes, but, for the tutorial, I'm not going to bother.
+
+You can see we're doing three successive data requests. The first is to the "Feed Info":http://www.postrank.com/developers/api#feed_info API. From this call we can retrieve the <em>feed_hash</em> which allows us to uniquely identify our feed in the PostRank system. By default all the PostRank API calls will return the data in JSON format. We could, and I did this initially use <code>format=xml</code> and <code>NSXMLDocument.initWithContentsOfURL</code> to pullback and parse all the data (the problem being, metrics only responds in JSON).
+
+Now, as long as the query to Feed Info didn't return an error we use the <code>id</code> to access the "Feed":http://www.postrank.com/developers/api#feed API. The Feed API will return the posts in the given feed. The default is to return 10 posts which works for our purposes. We could, if we wished, add a button to retrieve the next set of posts from the API using the <code>start</code> and <code>num</code> parameters.
+
+With the feed in hand we're interested in the <code>items</code> attribute. This is an array of the posts in the feed. Using these items we can start to create our table data. For each item we're going to create two hashes of data, one for each column of our table.  The PostRank column will contain the <code>:postrank</code> and <code>:postrank_color</code> and the post column will contain the <code>:title</code>, <code>:link</code> and <code>:metrics</code>.
+
+Finally, we query the metrics API for each post to retrieve the metrics data. The metrics API will provide us with a hash with a single key based on our post's ID. Under this key we receive a hash containing the metric source names and the values. We're skipping <code>friendfeed_comm</code> and <code>friendfeed_like</code> as they've been renamed to <code>ff_comments</code> and <code>ff_links</code> and only remain as legacy.
+
+Once we've got all the metrics source information packed into our <code>post_data</code> hash we call <code>@table.reloadData</code> so everything gets rendered properly.
+
+Since the calls to <code>DataRequest</code> are asynchronous, we have to call reload  inside the metrics block. This guarantees the table will be reloaded after we've received our data.
+
+With that out of the way, we're onto our formatting cells. In order to get our custom table display we need to subclass <code>NSCell</code> and override the <code>drawInteriorWithFrame(frame, inView:view)</code> where we can layout our cell as desired.
+
+<% coderay :lang => 'ruby' do -%>
+class PostRankCell < NSCell
+  def drawInteriorWithFrame(frame, inView:view)
+    m = objectValue[:color].match(/#(..)(..)(..)/)
+    NSColor.colorWithCalibratedRed(m[1].hex/ 255.0, green:m[2].hex/255.0, blue:m[3].hex/255.0, alpha:100).set
+    NSRectFill(frame)
+  
+    rank_frame = NSMakeRect(frame.origin.x + (frame.size.width / 2) - 12,
+                            frame.origin.y + (frame.size.height / 2) - 8, frame.size.width, 17)
+  
+    objectValue[:value].to_s.drawInRect(rank_frame, withAttributes:nil)
+  end
+end
+<% end %>
+
+The <code>PostRankCell</code> is pretty simple. We parse the provided PostRank colour, which comes as <code>#ffffff</code> into separate red, green and blue values. These values are passed to <code>NSColor.colorWithCalibratedRed(red, green:green, blue:blue, alpha:alpha)</code> in order to create a <code>NSColor</code> object representing our PostRank colour. We need to divided each value by <code>255</code> as <code>colorWithCalibratedRed:green:blue:alpha:</code> expects a value between <code>0.0</code> and <code>1.0</code>.  Once we've got our colour we call <code>set</code> to make that colour active and, using <code>NSRectFill</code> we fill then entire frame with the provided <code>postrank_color</code>.
+
+I'm, kinda, sorta, centering the PostRank values in the column so we need to create a <code>NSRect</code> to specify the box where we want to draw the numbers. This is done by calling <code>NSMakeRect</code> and providing the x, y, width and height values for the rectange. Once we've got our <code>NSRect</code> in hand we call <code>drawInRect(rank_frame, withAttributes:nil)</code> on the PostRank value. This will draw the string in the rectangle specified. We could set extra attributes on the string but, I don't need any, so I just leave it <code>nil</code>.
+
+You'll notice I'm using <code>objectValue</code> in a few places. <code>objectValue</code> is a <code>NSCell</code> method that will return the value assigned to this cell as retrieved based on the column key from our table data source.
+
+<% coderay :lang => 'ruby' do -%>
+class PostCell < NSCell
+  ROW_HEIGHT = 20
+  NUM_METRICS_PER_ROW = 6
+  SPRITE_SIZE = 16
+  
+  @@sprites = {:default => 0, :blogines => 16, :reddit => 32, :reddit_votes => 32,
+      :technorati => 48, :magnolia => 64, :digg => 80, :twitter => 96, :comments => 112,
+      :icerocket => 128, :delicious => 144, :google => 160, :pownce => 176, :views => 192,
+      :bookmarks => 208, :clicks => 224, :jaiku => 240, :digg_comments => 256,
+      :diigo => 272, :feecle => 288, :brightkite => 304, :furl => 320, :twitarmy => 336,
+      :identica => 352, :ff_likes => 368, :blip => 384, :tumblr => 400,
+      :reddit_comments => 416, :ff_comments => 432}
+  @@sprite = nil
+
+  def drawInteriorWithFrame(frame, inView:view)
+    unless @@sprite
+      bundle = NSBundle.mainBundle
+      @@sprite = NSImage.alloc.initWithContentsOfFile(bundle.pathForResource("sprites", ofType:"png"))
+      @@sprite.setFlipped(true)
+    end
+
+  	title_rect = NSMakeRect(frame.origin.x, frame.origin.y + 1, frame.size.width, 17)
+  	metrics_rect = NSMakeRect(frame.origin.x, frame.origin.y + ROW_HEIGHT, frame.size.width, 17)
+
+    title_str = "#{objectValue[:title]}"
+    title_str.drawInRect(title_rect, withAttributes:nil)
+
+    count = 0
+    orig_x_orign = metrics_rect.origin.x
+    
+    objectValue[:metrics].each_pair do |key, value|
+      s = metrics_rect.size.width
+      metrics_rect.size.width = SPRITE_SIZE
+      
+      y = if @@sprites.has_key?(key)
+        @@sprites[key.to_sym]
+      else
+        0
+      end
+      r = NSMakeRect(0, y, SPRITE_SIZE, SPRITE_SIZE)
+      @@sprite.drawInRect(metrics_rect, fromRect:r,
+                          operation:NSCompositeSourceOver, fraction:1.0)
+      metrics_rect.origin.x += 21
+      metrics_rect.size.width = s - 21
+       
+      "#{value}".drawInRect(metrics_rect, withAttributes:nil)
+      s = "#{value}".sizeWithAttributes(nil)
+      metrics_rect.origin.x += s.width + 15
+      
+      count += 1
+      if count == NUM_METRICS_PER_ROW
+        metrics_rect.origin.y += ROW_HEIGHT
+        metrics_rect.origin.x = orig_x_orign
+        count = 0
+      end
+    end
+  end
+end
+<% end %>
+
+<code>PostRankCell</code> is similar to <code>PostCell</code> in that we're basically creating bounding rectangles and drawing into them. The extra little bit we're doing here is loading up a <code>NSImage</code> which is our sprite set and using that to pull out all of the individual service icons. <code>NSImage</code> makes it easy to work with our sprite image by providing <code>drawInRect(rect, fromRect:from_rect, operation:op, fraction:val)</code>. <code>drawInRect:fromRect:operation:fraction:</code> draws into the rectangle defined by <code>rect</code> retrieving the pixels in your <code>NSImage</code> that are inside the <code>from_rect</code>. I'm using <code>NSCompositeSourceOver</code> because some of my images are semi-transparent. The fraction parameter is a the alpha setting for the image.
+
+With that, well, you'll probably need to download the source code to see it all in one file, you should have a working application that will query PostRank for a feed and display the posts and metrics for the feed.
+
+As for the next installment. I've got a few things I still want to do, including: tabbing between widgets, submitting the text field on return, a progress indicator as the feed information is being retrieved and adding a tabbed interface to allow showing feed, post and top post information. I'm not sure which of these things I'll tackle next. There are a few helper methods I want to try to add to HotCocoa from this article that I'll probably do first.  So, until next time.
+
+</div>
\ No newline at end of file

Added: MacRubyWebsite/trunk/content/hotcocoa/getting_started.txt
===================================================================
--- MacRubyWebsite/trunk/content/hotcocoa/getting_started.txt	                        (rev 0)
+++ MacRubyWebsite/trunk/content/hotcocoa/getting_started.txt	2009-05-31 05:29:41 UTC (rev 1676)
@@ -0,0 +1,243 @@
+---
+title:      HotCocoa Tutorial -- Getting Started
+created_at: 2009-05-29 22:09:00.0 -05:00
+updated_at: 2009-05-29 22:09:00.0 -05:00
+tutorial:   true
+author:     dj2
+filter:
+  - erb
+  - textile
+---
+h1(title). <%= h(@page.title) %>
+
+<div class="author">
+  By <%= member_name(@page.author) %>
+</div>
+
+<div class='tutorial'>
+
+h3. Introduction
+
+I've had my eye on giving "HotCocoa":http://www.macruby.org/trac/wiki/HotCocoa a test run for a while now. Other things have conspired to come up over the last few months so I haven't had a chance to give it a spin. That is, until now. I started poking at it the other day, a few things still confuse me, but I'm getting there.
+
+I figured I'd write stuff down as I plow my way through the code and create a simple little application. The application is nothing fancy, I'm going to query "PostRank":http://www.postrank.com and pull back engagement information on a feed entered into a text box. This will be a multi-part tutorial.
+
+In the usual fashion, let's start at the start. What is HotCocoa? Well, HotCocoa is a layer of Ruby code that sits on top of the Mac OS X frameworks including "Cocoa":http://developer.apple.com/cocoa/. HotCocoa is part of the "MacRuby":http://www.macruby.org distribution which will ship with figure versions of OS X. MacRuby is a port of Ruby 1.9 to run on top of Objective-C.
+
+I'm going to assume you have MacRuby installed for this tutorial.
+
+The HotCocoa developers makes life easy to get started developing your application. There is a <code>hotcocoa</code> command that is installed when you install MacRuby. This will create the basic application structure and Rakefile to get you up and running.
+
+<% coderay :lang => 'sh' do -%>
+titania:Development dj2$ hotcocoa Postie
+<% end %>
+
+<img class="alignleft" src="/images/hotcocoa/getting_started.png" />We can then execute our application by changing into the <em>Postie</em> directory and executing <code>macrake</code>. <strong>Note</strong>, this uses <em>mac</em>rake and not regular rake. MacRuby installs alongside the normal Ruby runtime on OS X. You'll need to make sure you use <em>macrake</em>, <em>macirb</em>, <em>macgem</em> and <em>macruby</em> to work with the MacRuby versions.  You should see a <em>Hello from HotCooca</em> window if everything worked correctly.
+
+You'll notice that you now have a Postie.app in your root directory. This application can be executed by double clicking like any other Mac application, although, you'll need MacRuby installed for it to execute. You can also execute <code>macruby lib/application.rb</code> to execute the application. This allows passing flags to <em>macruby</em> for things like enabling debug mode.
+
+Let's take a quick look at the files generated by the <em>hotcocoa</em> command.
+
+<% coderay :lang => 'sh' do -%>
+./Rakefile
+./config/build.yml
+./resources/HotCocoa.icns
+./lib/application.rb
+./lib/menu.rb
+<% end %>
+
+The <em>build.yml</em> file contains information used by <em>hotcocoa</em> to build your application. This includes things like the name, version, icon and source files. The icon, by default, is <em>HotCocoa.icns</em>. The main files we're interested in are <em>application.rb</em> and <em>menu.rb</em>.
+
+<% coderay :lang => 'ruby' do -%>
+module HotCocoa
+  def application_menu
+    menu do |main|
+      main.submenu :apple do |apple|
+        apple.item :about, :title => "About #{NSApp.name}"
+        apple.separator
+        apple.item :preferences, :key => ","
+        apple.separator
+        apple.submenu :services
+        apple.separator
+        apple.item :hide, :title => "Hide #{NSApp.name}", :key => "h"
+        apple.item :hide_others, :title => "Hide Others", :key => "h", :modifiers => [:command, :alt]
+        apple.item :show_all, :title => "Show All"
+        apple.separator
+        apple.item :quit, :title => "Quit #{NSApp.name}", :key => "q"
+      end
+      main.submenu :file do |file|
+        file.item :new, :key => "n"
+        file.item :open, :key => "o"
+      end
+      main.submenu :window do |win|
+        win.item :minimize, :key => "m"
+        win.item :zoom
+        win.separator
+        win.item :bring_all_to_front, :title => "Bring All to Front", :key => "o"
+      end
+      main.submenu :help do |help|
+        help.item :help, :title => "#{NSApp.name} Help"
+      end
+    end
+  end
+end
+<% end %>
+
+The <em>menu.rb</em> file contains information about the menu for our applcation. This includes the menu names, hot keys, modifiers and general layout.
+
+The <em>:apple</em> submenu is special and will appear with a menu title based on your application name, as is typical for OS X applications. For the other submenus, by default, the menu title will be the capitalized version of the symbol name converted to a string. You can also provide a <code>:title => 'foo'</code> option to specify a different name. <code>menu.submenu :postrank, :title => 'PostRank'</code>.
+
+The symbol provided to each submenu <em>item</em>, e.g. <em>file.item :new</em> will be used to form the name of the method invoked in your application delegate. The methods are named <em>on_<key></em>. For our <em>:new</em> item the <em>on_new</em> menu item will be invoked. If there is no <em>on_<key></em> method the menu item will be disabled. As you can see above you can also specify <em>:modifiers</em> and <em>:key</em> equivalents for your items.
+
+As with the menu titles, the items names will be formed from capitalizing the symbol provided unless a <em>:title</em> is provided.
+
+In the case of <em>Postie</em> I've erased everything but the <em>:apple</em> submenu for now. I don't need any extra menu items at the moment. This also means all of the <em>on_*</em> methods can be removed from <em>application.rb</em>.
+
+The default <em>application.rb</em> provided by the <em>hotcocoa</em> command is pretty short.
+
+<% coderay :lang => 'ruby' do -%>
+require 'hotcocoa'
+
+class Application
+  include HotCocoa
+
+  def start
+    application(:name => "Postie") do |app|
+      app.delegate = self
+      window(:frame => [100, 100, 500, 500], :title => "Postie") do |win|
+        win << label(:text => "Hello from HotCocoa", :layout => {:start => false})
+        win.will_close { exit }
+      end
+    end
+  end
+end
+
+Application.new.start
+<% end %>
+
+Let's take a quick look and see if we can figure out what's going on. We need to <code>require hotcocoa'</code> to get access to the needed HotCocoa classes. We then <code>include HotCocoa</code> into our <code>Application</code> class to make everything shorter. Feel free to rename <em>Application</em> just do it in the class definition and at the bottom of the file.
+
+Jumping to the bottom, you can see we're calling <code>Application.new.start</code> so the  <code>Application#start</code> method will be invoked. It's worth noting, the application will not return from <code>Application#start</code>.
+
+Going back to <code>Application#start</code> we call <code>application</code> to create our application, setting the title as desired. We then set ourselves as the application delegate. This means that our class will receive all of the callbacks that are called on the Cocoa application. This includes the menu <code>on_*</code>callbacks we talked about earlier.
+
+We then proceed to create a <code>window</code>. We're setting a <em>:frame</em> on the window to position it at X 100, Y 100 (from the bottom left) with a width of 500 and height of 500. We <em>:title</em> the window as <em>Postie</em>.  If you don't want to specify the entire frame of the window you can specify just the <em>:size => [500, 500]</em> of the window. You can also specify <em>:center => true</em> to center the window on the desktop. If you look at the Objective-C documentation for NSWindow the options available in Obj-C are available in the HotCocoa layer.
+
+Once the window is created we add a <code>label</code> to the window and set the <code>will_close</code> handler to <code>exit</code> when executed.
+
+The <code>will_close</code> callback is the HotCocoa name for the Cocoa <code>windowWillClose:</code>. Many of the Cocoa callbacks have been remapped to make the names more Ruby like.
+
+<table>
+<tr>
+ <th>Cocoa Callback</th>
+ <th>HotCocoa Callback</th>
+</tr>
+<tr>
+ <td>window:shouldDragDocumentWithEvent:from:withPasteboard:</td>
+ <td>should_drag_document?(shouldDragDocumentWithEvent, from, withPasteboard)</td>
+</tr>
+<tr>
+  <td>window:shouldPopUpDocumentPathMenu:</td>
+  <td>should_popup_path_menu?(shouldPopUpDocumentPathMenu)</td>
+</tr>
+<tr>  
+  <td>window:willPositionSheet:usingRect:</td>
+  <td>will_position_sheet(willPositionSheet, usingRect)</td>
+</tr>
+<tr>
+  <td>windowDidBecomeKey:</td>
+  <td>did_become_key</td>
+</tr>  
+<tr>
+  <td>windowDidBecomeMain:</td>
+  <td>did_become_main</td>
+</tr>  
+<tr>
+  <td>windowDidChangeScreen:</td>
+  <td>did_change_screen</td>
+</tr>  
+<tr>
+  <td>windowDidChangeScreenProfile:</td>
+  <td>did_change_screen_profile</td>
+</tr>  
+<tr>
+  <td>windowDidDeminiaturize:</td>
+  <td>did_deminiturize</td>
+</tr>  
+<tr>
+  <td>windowDidEndSheet:</td>
+  <td>did_end_sheet</td>
+</tr>  
+<tr>
+  <td>windowDidExpose:</td>
+  <td>did_expose(windowDidExpose.userInfo['NSExposedRect'])</td>
+</tr>  
+<tr>
+  <td>windowDidMiniaturize:</td>
+  <td>did_miniaturize</td>
+</tr>  
+<tr>
+  <td>windowDidMove:</td>
+  <td>did_move</td>
+</tr>  
+<tr>
+  <td>windowDidResignKey:</td>
+  <td>did_resign_key</td>
+</tr>  
+<tr>
+  <td>windowDidResignMain:</td>
+  <td>did_resign_main</td>
+</tr>  
+<tr>
+  <td>windowDidResize:</td>
+  <td>did_resize</td>
+</tr>  
+<tr>
+  <td>windowDidUpdate:</td>
+  <td>did_update</td>
+</tr>  
+<tr>
+  <td>windowShouldClose:</td>
+  <td>should_close?</td>
+</tr>  
+<tr>
+  <td>windowShouldZoom:toFrame:</td>
+  <td>should_zoom?(toFrame)</td>
+</tr>  
+<tr>
+  <td>windowWillBeginSheet:</td>
+  <td>will_begin_sheet</td>
+</tr>  
+<tr>
+  <td>windowWillClose:</td>
+  <td>will_close</td>
+</tr>  
+<tr>
+  <td>windowWillMiniaturize:</td>
+  <td>will_miniaturize</td>
+</tr>  
+<tr>
+  <td>windowWillMove:</td>
+  <td>will_move</td>
+</tr>  
+<tr>
+  <td>windowWillResize:toSize:</td>
+  <td>will_resize(toSize)</td>
+</tr>  
+<tr>
+  <td>windowWillReturnFieldEditor:toObject:</td>
+  <td>returning_field_editor(toObject)</td>
+</tr>  
+<tr>
+  <td>windowWillReturnUndoManager:</td>
+  <td>returning_undo_manager</td>
+</tr>  
+<tr>
+  <td>windowWillUseStandardFrame:defaultFrame:</td>
+  <td>will_use_standard_frame(defaultFrame)</td>
+</tr>
+</table>
+
+That's it for part I. We've now setup with our basic application structure and have an idea of what we're working with. In the next installment, we'll work on getting our application views setup as we want.
+
+</div>
\ No newline at end of file
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://lists.macosforge.org/pipermail/macruby-changes/attachments/20090530/aa175b86/attachment-0001.html>


More information about the macruby-changes mailing list