Feb 08 2010

Download and XML parsing with HotCocoa

I’ve been working on Rife, a Google Reader client, over the last few days and have been digging my way through some more HotCocoa mappings. I figured the best way to remember some of this stuff is to write it down so, the following will look at synchronous and asynchronous downloads and writing a XML parser in HotCocoa/MacRuby.

So, what are we creating you ask? Well, as I said, I’ve been playing with Google Reader APIs and we’re going to do a synchronous request to Google for an identifier token. Once we’ve successfully authenticated we’re going to make an asynchronous request for our unread items. We’ll then parse the resulting XML document and spit the titles out to the console.

As with any HotCocoa application the easiest way to get started is to have system setup the shell of our application. We’ll use the hotcocoa command to create our application which I’m calling titles.

titania:example dj2$ hotcocoa titles

In order to authenticate to Google we’re going to need your username and password. Since I’m going to do the output to the console for demonstration purposes I’ll use the main application window to show the fields for username and password and a save button.

require ‘rubygems’
require ‘hotcocoa’

class Application
  include HotCocoa
 
  def start
    application(:name => "Titles") do |app|
      app.delegate = self

      window(:frame => [100, 100, 200, 200], :title => "Titles") do |win|
        win.center
        win.will_close { exit }

        win << label(:text => "Username", :layout => {:start => false})
        win << @username_field = text_field(:layout => {:start => false, :expand => [:width]})

        win << label(:text => "Password", :layout => {:start => false})
        win << @password_field = secure_text_field(:layout => {:start => false, :expand => [:width]})

        win << save = button(:title => "save", :layout => {:start => true}) do |button|
          button.on_action { authenticate }
        end

        @username_field.setNextKeyView(@password_field)
        @password_field.setNextKeyView(save)
        save.setNextKeyView(@username_field)
      end
    end
  end

  def authenticate
    puts "DO AUTH #{@username_field.to_s} #{@password_field.to_s}"
  end
end

Application.new.start

If you run the application by executing macrake in the titles directory you should see the main application window. Typing something into the username and password fields and pressing save you should see something similar to the following in your terminal.

DO AUTH test test

With our framework setup let’s get to the interesting stuff. First up, authenticating so we can retrieve our identifier from Google. We’re going to make a synchronous request to retrieve the identifier and, if successful, call a method to start retrieving our reading list.

def authenticate
  username = @username_field.stringValue
  password = @password_field.stringValue

  query = "https://www.google.com/accounts/ClientLogin?" +
          "Email=#{CGI.escape(username)}&Passwd=#{CGI.escape(password.to_s)}" +
          "&source=HotCocoaExample&service=reader"

  url = NSURL.URLWithString(query)
  request = NSMutableURLRequest.requestWithURL(url)
  request.addValue("HotCocoaExample", forHTTPHeaderField:"source")
  request.addValue("2", forHTTPHeaderField:"GData-Version")

  response = Pointer.new("@")
  data = NSURLConnection.sendSynchronousRequest(request, returningResponse:response, error:nil)
  data = NSString.alloc.initWithData(data, encoding:NSUTF8StringEncoding)

  if data =~ /^SID=(.*)\n/
    @sid = $1

    retrieve_reading_list
  else
    raise Exception.new("Authentication failed with: #{data}")
  end
end

def retrieve_reading_list
  puts @sid
end

If you add the above to your application, and add require 'cgi', you should be able to run the program, put in your username and password and get a long line of characters spit out on the terminal. Those characters are your Google SID.

Let’s look a bit closer at what we’re doing in the authenticate method. We start by grabbing the stringValue for the username and password fields. Then, using these values, we build the query string needed for authentication. This query string is used to build a URL object by calling NSURL.URLWithString(query). With the URL in hand we can start building our request object. This is done by calling NSMutableURLRequest.requestWithURL(url). I’m using the mutable version of the request as I want to add a few extra header values. These are both added with addValue(value, forHTTPHeaderField:field).

When we execute our request the system is going to want to put our response object somewhere. In the Cocoa version the method accepts a NSURLResponse **response parameter. In order to handle the response we need to create a Pointer object which is a MacRuby object for handling these pointers to objects. We want our pointer to point to an object so we use Pointer.new("@").

With the response setup we call NSURLConnection.sendSynchronousRequest and provide our request and response objects. I don’t care about the error, but if you do, you’d want to pass in something similar to our response pointer. The request will return a NSData object which we convert to a string using the initWithData initialization method of NSString.

With the string in hand we try to extract our SID and, if successful, execute the retrieve_reading_list method which just spits out the SID.

OK, cool, we’ve now got our authentication token and are ready to move onto the asynchronous request to get our reading list.

def retrieve_reading_list
  query = "https://www.google.com/reader/atom/user/-/state/com.google/reading-list?" +
          "xt=user/-/state/com.google/read&ck=#{Time.now.to_i * 1000}&n=2"

  url = NSURL.URLWithString(query)
  request = NSMutableURLRequest.requestWithURL(url)
  request.addValue("HotCocoaExample", forHTTPHeaderField:"source")
  request.addValue("2", forHTTPHeaderField:"GData-Version")
  request.addValue("SID=#{@sid}", forHTTPHeaderField:"Cookie")

  NSURLConnection.connectionWithRequest(request, delegate:self)
end

def connectionDidFinishLoading(conn)
  puts NSString.alloc.initWithData(@receivedData, encoding:NSUTF8StringEncoding)
end

def connection(conn, didReceiveResponse:response)
  if response.statusCode != 200
    puts "BAD STATUS: #{response.statusCode}"
    p response.allHeaderFields
  end
end

def connection(conn, didReceiveData:data)
  @receivedData ||= NSMutableData.new
  @receivedData.appendData(data)
end

Similar to the synchronous method we start by building our query string, NSURL and NSMutableURLRequest. We’ve added a cookie to our request object to hold the SID retrieved earlier from Google.

We fire the request by calling NSURLConnection.connectionWithRequest(request, delegate:self). We specific ourselves as the delegate for the connection. There are a few delegate methods we can implement to receive data and get notified of request states. These are:

  • connectionDidFinishLoading(connection)
  • connection(connection, didReceiveResponse:response)
  • connection(connection, didReceiveData:data)

We’ll look at our implementation of each of these callbacks in turn. First, in connectionDidFinishLoading(conn) we’re just printing out the data retrieved. We need to convert the data, similar to what we did in the synchronous request, from a NSData object to a NSString object.

In connection(conn, didReceiveResponse:response) we’re just checking to see if we got a 200 response code from the server. In all other cases we print an error.

The main work is done in connection(conn, didReceiveData:data) where we create a NSMutableData object if needed and append any data received into the mutable data object.

Running the code at this point should dump the first two items in your reading list to the console. The data will be a big mess of XML but we’ll look at parsing that in the next step.

def connectionDidFinishLoading(conn)
  xml = HotCocoa.xml_parser(:data => @receivedData)
  @receivedData = nil

  xml.on_start_document { puts "Starting Parse" }
  xml.on_end_document do
    HotCocoa.notification(:post => true, :name => "all_entries_loaded", :object => nil, :info => nil)
  end
  xml.on_parse_error { |err| puts "Parse error #{err.inspect}" }

  xml.on_cdata { |cdata| @elem_text += cdata.to_s }
  xml.on_characters { |chars| @elem_text += chars.to_s }

  xml.on_start_element do |element, namespace, qualified_name, attributes|
    @elem_text =
  end

  xml.on_end_element do |element, namespace, qualified_name|
    puts @elem_text if element == ‘title’
  end

  xml.parse
end

We’re finally getting into some HotCocoa specific code with our XML parser. HotCocoa defines a mapping wrapper around NSXMLParser and provides a set of delegate methods. These delegates mean we don’t have to set our class as the delegate and create a bunch of methods. They mean we can attach our code as blocks on our XML object. All the better if you want to define a few parsers in one class.

We start off by creating a HotCocoa.xml_parser. The parser accepts NSData objects so we don’t need to convert our response data to a string. We then setup eight callbacks. There are actually a bunch more callbacks that can be hooked up and you should look at the xml_parser mapping code to see if you need any of them. For our purposes, we only really care about eight.

The on_start_document, on_end_document and on_parse_error callbacks, as you can probably guess, get called when we start parsing, when we finish parsing and when we receive a parse error, respectively. We don’t really care about start in this example, but I put it in anyway. When we’ve completed parsing we send a notification and other application code can then listen for this notification and do anything it needs. If we wanted we could store the entries as they’re parsed and provide them to the :object key. This would make those entries available to anyone that receives the notification.

If we receive either CDATA, with on_cdata, or text, with on_characters, we append the content to our current elements text. When we receive the open tag of a new element, on_start_element, we dump our current element text as we’ve started a new element. We can also take a look at the elements name, attributes, namespace and qualified name, if desired.

Finally, in on_end_element we print out the current element text if the element we’re finishing has a name of title.

With all the callbacks configured we use xml.parse to start the parser. You should, if you run this example, see the titles and authors of the first two posts in your reading list. (The author name is also called title and I’m not bothering to check that the parent element is entry before spitting it out.)

That’s it. You can now make synchronous and asynchronous requests for content and parse any resulting XML.

One last thing before you go. Both of the requests we did above were GET requests. You can do other types of requests using the same methods as above you just need a slightly different setup for the request. You can see a POST request below.

request = NSMutableURLRequest.requestWithURL(url)
request.addValue(SOURCE, forHTTPHeaderField:"source")
request.addValue("2", forHTTPHeaderField:"GData-Version")
request.addValue("SID=#{@sid}", forHTTPHeaderField:"Cookie")

body="first=1&second=2&third=3"

request.setHTTPMethod(‘POST’)
request.setValue(‘application/x-www-form-urlencoded’, forHTTPHeaderField:‘Content-Type’)
request.setValue(body.length.to_s, forHTTPHeaderField:‘Content-Length’)
request.setHTTPBody(body.dataUsingEncoding(NSASCIIStringEncoding))

The first few lines should look familiar from creating our asynchronous request above. Since we’re going to be posting the data we use setHTTPMethod('POST') to setup the request method. We’ve form encoded the data so we set the appropriate Content-Type and set the Content-Length. Note, we convert the length to a string before sending to setValue. Finally, we set the body of the post with setHTTPBody. You need to convert the body string into a NSData object which we do with the dataUsingEncoding method. If you don’t convert the body to NSData you’ll end up sending a nil body with your post request.

[Slashdot] [Digg] [Reddit] [del.icio.us] [Facebook] [Technorati] [Google] [StumbleUpon]

Jul 21 2009

Toolbars with HotCocoa

3407707273_c05017779fI had the opportunity to add a toolbar to my Silver Lining app the other day and though I’d write down how I did it. This would, probably, have been easier if I had an understanding of the Cocoa Toolbar.

The main thing that kept tripping me up is that you don’t add toolbar items directly to the toolbar. Each item you want to add to the toolbar is given an identifier (HotCocoa will generate one from the label if needed). Then, when you create your toolbar, you specify which toolbar items are in the toolbar by default and which items are available to be put in the bar.

Once the toolbar is created we don’t just append it to the window with the normal << operator. We need to use toolbar= to assign the toolbar to the window.

silverlining_toolbarThe toolbar we’re going to create is fairly simple. We want a button, with image, on the left for reloading and a search field on the right. We’ll put a flexible spacer in the middle to put them on opposite sides of the screen. (Note, there is currently a bug in HotCocoa where our specified items will be to the right and the spacer to the left. Things may not look correct until fixed.)

reload_item = toolbar_item(:label => "Reload",
                           :image => image(:named => "reload"))
reload_item.on_action { reload_instances }

The first toolbar item we create is the reload button. We set the image on the button to a file in the resources folder called reload.png. When the button is clicked, we want to execute the reload_instances method.

search_item = toolbar_item(:identifier => "Search") do |si|
  search = search_field(:frame => [0, 0, 250, 30],
                        :layout => {:align => :right, :start => false})
  search.on_action { |sender| filter_instances(search) }

  si.view = search
end

The second item is the search box. You’ll notice we’re setting the :identifier instead of the :label as we did for the reload button. The difference being, the label will appear below the button and we don’t want Search appearing below the search box. The reload button will have an identifier of Reload created for it by HotCocoa.

With the search toolbar item in hand we create a search_field. The search field is then assigned as the view in the search toolbar item using si.view=.

@toolbar = toolbar(:default => [reload_item, :flexible_space, search_item]) do |tb|
  win.toolbar = tb
end

Finally, we create our toolbar. This is done with the toolbar method. We want our default set of items to be the reload_item, :flexible_space and search_item. We then assign the toolbar to our window with win.toolbar = tb.

There are a few default toolbar items you can use, similar to :flexible_space. They are:

  • :separator
  • :space
  • :flexible_space
  • :show_colors
  • :show_fonts
  • :customize
  • :print

That’s all folks. Have fun.

[Slashdot] [Digg] [Reddit] [del.icio.us] [Facebook] [Technorati] [Google] [StumbleUpon]

Jun 13 2009

HotCocoa and Core Data

IMG_6093I’ve been playing around a bit with Core Data and HotCocoa the last few days and thought I’d share what I’ve got so far. When you install MacRuby you’re provided with an XCode template to start working with Core Data. I didn’t want to use XCode so I skipped the template. Some of this code is probably pretty similar to the template as I ported the Obj-C Core Data template to Ruby.

Note, this code is lightly tested, prototype, probably wrong and has already assassinated an unknown number of kitties. At this point, I’m still not sure if the approach taken is actually sane. Time will tell, but I thought it was interesting enough to share.

I stuffed all of the default Core Data stuff into a CoreData module that I’m including into my main application (mostly so I don’t have to look at the functions when working on my main code). The separation isn’t great at the moment as the should_terminate? method is currently in the module instead of the main application. I’ll be coming back to this setup once I’ve figured out how all the pieces work.

Along with the Core Data base methods I’ve also created a ManagedObject class. Each of the entities in my model I’ve created as a model class. Those model classes inherit from ManagedObject. (I just realized, the objects returned from self.create in ManagedObject aren’t of my model types. They’ll be straight up NSManagedObjects. Oops. Will need to look at that as well. Can you tell this stuff is still pretty rough?) ManagedObject contains the common create method for all of the sub-classes. When create is called the entity will be retrieved from the Core Data model and have it’s attributes and relationships added as methods on the returned object.

What this means is that, in the end, I can do the following:

  p = Post.create
  p.title = "HotData"
  p.pub_date = NSDate.date
  p.content = "Playing with Core Data from HotCocoa"
       
  p.author = Author.create(:name => ‘dj2′)
  p.tags.addObject(Tag.create(:name => "Core Data"))
  p.tags.addObject(Tag.create(:name => "HotCocoa"))
       
  p.published = true if !p.published?

One thing to note, when you’re working with Core Data you’ll be working with a .xcdatamodel file. In order to use that from code this data model needs to be compiled to a mom file and put into your application bundle. I’ve created a patch, attached to ticket 278 that builds this into the HotCocoa application builder. All you need is a build.yml similar to the following:

name: Blog
load: lib/application.rb
version: "1.0"
icon: resources/HotCocoa.icns
resources:
  – resources/**/*.*
data_models:
  – data/**/*.xcdatamodel
sources:
  – lib/**/*.rb

Picture 1Note the addition of the data_models key. This will take each of the models found, compile it, and place it in your bundles resource folder ready for use.

With that out of the way, what are we modeling? I figured I’d go simple and model a blog. I didn’t bother to write any real UI in front of it for the example as I just wanted to focus on the Core Data aspects.

The first step is to create the project: hotcocoa Blog will create the basis for us. I then updated the build.yml file to what’s seen above.

The next step was to create my data model which I saved to data/Blog.xcdatamodel. This needs to be done through XCode. Open up a new data model and create the following:

Post

  • content – string (not optional)
  • pub_date – date (not optional)
  • published – boolean (default NO)
  • title – string (not optional, default “- untitled -”)
  • author – relationship to Author table (not optional)
  • tags – relationship to the Tags table (optional, to-many)

Author

  • name – string (not optional)
  • posts – relationship to the Posts table (optional, to-many)

Tag

  • name – string (not optional)
  • posts – relationship to the Posts table (optional, to-many)

With the model in place we need to copy in our supporting code. This is the CoreData module and the ManagedObject class.

require ‘fileutils’
framework ‘CoreData’

module CoreData
  def application_support_folder
    return @application_support_folder if @application_support_folder

    paths = NSSearchPathForDirectoriesInDomains(NSApplicationSupportDirectory, NSUserDomainMask, true)
    path = (paths.count > 0) ? paths[0] : NSTemporaryDirectory
    @application_support_folder = File.join(path, NSApp.name.downcase)
    FileUtils.mkdir_p(@application_support_folder) unless File.exists?(@application_support_folder)
    @application_support_folder
  end

  def managed_object_model
    @managed_object_model ||= NSManagedObjectModel.mergedModelFromBundles(nil)
  end

  def persistent_store_coordinator
    return @persistent_store_coordinator if @persistent_store_coordinator

    @persistent_store_coordinator = NSPersistentStoreCoordinator.alloc.initWithManagedObjectModel(managed_object_model)

    error = Pointer.new_with_type(‘@’)
    url = NSURL.fileURLWithPath(File.join(application_support_folder, "#{NSApp.name}.xml"))
    unless @persistent_store_coordinator.addPersistentStoreWithType(NSXMLStoreType, configuration:nil,
                                                                    URL:url, options:nil, error:error)
      NSApplication.sharedApplication.presentError(error[0])
    end
    @persistent_store_coordinator
  end

  def managed_object_context
    return @managed_object_context if @managed_object_context

    if persistent_store_coordinator
      @managed_object_context = NSManagedObjectContext.alloc.init
      @managed_object_context.setPersistentStoreCoordinator(persistent_store_coordinator)
    end
    @managed_object_context
  end

  def returning_undo_manager
    managed_object_context.undoManager
  end

  def save
    error = Pointer.new_with_type(‘@’)
    unless managed_object_context.save(error)
      NSApplication.sharedApplication.presentError(error[0])
    end
  end

  def should_terminate?
    reply = NSTerminateNow
    if managed_object_context
      if (managed_object_context.commitEditing)
        error = Pointer.new_with_type(‘@’)
        if (managed_object_context.hasChanges and !managed_object_context.save(error))
          if NSApplication.sharedApplication.presentError(error[0])
            reply = NSTerminateCancel
          else
            alertReturn = NSRunAlertPanel(nil, "Could not save changes while quitting. Quit anyway?",
                                               "Quit anyway", "Cancel", nil)
            if (alertReturn == NSAlertAlternateReturn)
              reply = NSTerminateCancel
            end
          end
        end
      else
        reply = NSTerminateCancel
      end
    end
    reply
  end
end

There isn’t anything special here. This is, basically, the Obj-C Core Data template ported to Ruby.

require ‘hotcocoa’

class ManagedObject
  def self.create(vals={})
    name = self.to_s.gsub(/^.*::/, )
    obj = NSEntityDescription.insertNewObjectForEntityForName(name,
                                  inManagedObjectContext:NSApp.delegate.managed_object_context)

    entity = NSEntityDescription.entityForName(name,
                        inManagedObjectContext:NSApp.delegate.managed_object_context)

    map_attributes(obj, entity.attributesByName)
    map_relationships(obj, entity.relationshipsByName)

    vals.each_pair do |key, value|
      obj.send("#{key}=", value)
    end
    obj
  end

  def self.map_attributes(obj, attributes_by_name)
    attributes_by_name.each_pair do |attribute, value|
      obj.instance_eval %[
        def #{attribute}=(val)
          setValue(val, forKey:"#{attribute}")
        end

        def #{attribute}
          valueForKey("#{attribute}")
        end
      ]
      if value.attributeType == NSBooleanAttributeType
        obj.instance_eval %[
            def #{attribute}?
              send("#{attribute}".to_sym).boolValue
            end
          ]
      end

      obj.send("#{attribute}=".to_sym, value.defaultValue)
    end
  end

  def self.map_relationships(obj, relationships_by_name)
    relationships_by_name.each_pair do |relationship, value|
      if value.isToMany
        obj.instance_eval %[
            def #{relationship}
              willAccessValueForKey("#{relationship}")
              v = primitiveValueForKey("#{relationship}")
              didAccessValueForKey("#{relationship}")
              v
            end
          ]
      else
        obj.instance_eval %[
            def #{relationship}
              valueForKey("#{relationship}")
            end
           
            def #{relationship}=(val)
              setValue(val, forKey:"#{relationship}")
            end
          ]
      end
    end
  end
end

ManagedObject is a little more interesting. I didn’t want to have to use setValue:forKey:, valueForKey: and other NSManagedObject methods to work with my model. What ManagedObject does is create a new entity with NSEntityDescription.insertNewObjectForEntityForName:inManagedObjectContext: and then attach methods for the relationships and attributes of that entity.

We use NSEntityDescription.entityForName:inManagedObjectContext: to get the information on the entity. This allows us to call entity.attributesByName and entity.relationshipsByName. The last thing self.create does is take any provided options and send them to the appropriate keys. This allows us to do Author.create(:name => "dj2") instead of needing multiple lines.

The methods are created in self.map_attributes and self.map_relationships. For each attribute we attach an attribute= and attribute method. If the attribute is a boolean we also create an attribute? method. If the attribute assigns a default value we’ll send the default after the methods are added.

self.map_relationships works in a similar fashion if the relation ship isn’t to-many. If it is to-many we only create an attribute method. Currently you’ll need to use the NSSet methods to access the values in the to-many set.

The relationship code needs a bit more work as it won’t try to lookup a relationship, just blindly create a new row. I need to decide if I want to build that into create or add a find method to lookup the entities.

I’ve been storing my models in a lib/models directory. For this all we create three, almost identical, models. author.rb, post.rb and tag.rb

class Author < ManagedObject
end

class Post < ManagedObject
end

class Tag < ManagedObject
end

The last changes are to application.rb. We need to require lib/core_data, lib/managed_object and our models.

Dir.glob("lib/models/*.rb").each do |file|
  require file
end
 

Then, inside the application class we include CoreData and we're good to go. You can then do something similar to:

p = Post.create
p.title = "HotData"
p.pub_date = NSDate.date
p.content = "Playing with Core Data from HotCocoa"
       
p.author = Author.create(:name => 'dj2')
p.tags.addObject(Tag.create(:name => "Core Data"))
p.tags.addObject(Tag.create(:name => "HotCocoa"))
       
p.published = true if !p.published?
       
save

Which will create a ~/Library/Application Support/blog/Blog.xml file with your new post inside.

I've uploaded a copy of my source if you want to take a look.

Questions? Comments? Let'em rip.

[Slashdot] [Digg] [Reddit] [del.icio.us] [Facebook] [Technorati] [Google] [StumbleUpon]

May 26 2009

Heating up with HotCocoa on GitHub

I’ve imported Postie to GitHub to make it easier to follow along from home. If you’re new here, you can see the development of Postie in the Heating up with HotCocoa articles (part I, part II and part III).

The code on GitHub is based off of the code from part III with a minor addition to the button action.

b.on_action do
  load_feed
 
  @timer.invalidate unless @timer.nil?
  @timer = NSTimer.scheduledTimerWithTimeInterval(30, target:self,
                                                  selector:"refresh".to_sym, userInfo:nil,
                                                  repeats:true)
end

I wanted to have the application reload my metrics data automatically to feed my obsessive nature. I added a NSTimer that will execute a refresh method every 30 seconds. The refresh method just calls load_feed to reload everything. Not perfect, but works as a quick hack to be cleaned up later.

[Slashdot] [Digg] [Reddit] [del.icio.us] [Facebook] [Technorati] [Google] [StumbleUpon]

May 25 2009

Heating up with HotCocoa Part III

img_5183In 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 tutorial. For those of you just joining the party, you’ll probably want to take a look at part I and part II.

If you’d like to grab a copy of the code, it’s getting a bit big to post all of it in one go, you can grab the tar ball here. The file contains all of the HotCocoa files along with the sprite image that I’ve shamelessly niced from PostRank.

picture-12When 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.

Note, we’re going to be using JSON when working with the PostRank APIs. There is a bug in MacRuby, as of revision 1594, where JSON.parse would crash MacRuby. You’ll need to apply the patch attached to ticket 257 in order run this application.

OK, let’s go.

POSTRANK_URL_BASE = "http://api.postrank.com/v2"
APPKEY = "appkey=Postie"

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.

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

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 setDataCell on the column objects. The parameter to setDataCell is an instantiated instance of our formatter class. I have two classes, PostRankCell and PostCell 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 PostCell class. We set the Postie instance as the delegate for the table so we can receive the tableView(table, heightOfRow:row) callback (thanks @macruby for the pointer). The last addition to the table is to hookup the double click action with @table.setDoubleAction(:table_clicked). The parameter is the name of the method that will be called, as a symbol.

def table_clicked
  url = NSURL.URLWithString(@table.dataSource.data[@table.clickedRow][:data][:link])
  NSWorkspace.sharedWorkspace.openURL(url)
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 @table.clickedRow and access the link with @table.dataSource.data[@table.clickedRow][:data][:link]. We then create a NSURL with this string. The created URL object is passed to NSWorkspace.sharedWorkspace.openURL(url) causing the page to open in the browser.

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

picture-2The tableView(table, heightOfRow:row) 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 :metrics 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 PostCell class.

def load_feed
  @table.data = []

  str = @feed_field.stringValue
  unless str.nil? || str =~ /^s*$/
    fetch_feed(str)
  end
end

load_feed has been updated to empty our tables data by assigning a new array and, assuming we’ve received a valid feed, call fetch_feed 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 Net::HTTP, Curb, NSXMLDocument or, as I’ve done, NSURLConnection. The reason I used NSURLConnection 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 NSURLConnection before looking at fetch_feed. The reason I created a wrapper is that NSURLConnection, 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:

DataRequest.new("http://google.ca") do |data|
  NSLog "Data: #{data}"
end
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

As NSURLConnection executes it returns data to the application. We’re storing this data in a NSMutableData object called @buf. The callbacks we’re interested in are:

connection(conn, didReceiveResponse:resp)
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.
connection(conn, didReceiveData:data)
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.
connection(conn, didFailWithError:err)
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.
connectionDidFinishLoading(conn)
Called when all of the data has been retrieved from the remote server. Since we’re not working with binary data I convert the NSMutableData to an NSString using the initWithData:encoding method. Note the use of alloc on the NSString. If you try to use new you’ll, like me, spend the next 30 minutes trying to figure out why your application is crashing.
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

Most of the code in fetch_feed should probably be refactored into Feed, Post and Metrics 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 API. From this call we can retrieve the feed_hash 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 format=xml and NSXMLDocument.initWithContentsOfURL 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 id to access the 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 start and num parameters.

With the feed in hand we’re interested in the items 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 :postrank and :postrank_color and the post column will contain the :title, :link and :metrics.

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 friendfeed_comm and friendfeed_like as they’ve been renamed to ff_comments and ff_links and only remain as legacy.

Once we’ve got all the metrics source information packed into our post_data hash we call @table.reloadData so everything gets rendered properly.

Since the calls to DataRequest 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 NSCell and override the drawInteriorWithFrame(frame, inView:view) where we can layout our cell as desired.

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

The PostRankCell is pretty simple. We parse the provided PostRank colour, which comes as #ffffff into separate red, green and blue values. These values are passed to NSColor.colorWithCalibratedRed(red, green:green, blue:blue, alpha:alpha) in order to create a NSColor object representing our PostRank colour. We need to divided each value by 255 as colorWithCalibratedRed:green:blue:alpha: expects a value between 0.0 and 1.0. Once we’ve got our colour we call set to make that colour active and, using NSRectFill we fill then entire frame with the provided postrank_color.

I’m, kinda, sorta, centering the PostRank values in the column so we need to create a NSRect to specify the box where we want to draw the numbers. This is done by calling NSMakeRect and providing the x, y, width and height values for the rectange. Once we’ve got our NSRect in hand we call drawInRect(rank_frame, withAttributes:nil) 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 nil.

You’ll notice I’m using objectValue in a few places. objectValue is a NSCell method that will return the value assigned to this cell as retrieved based on the column key from our table data source.

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

PostRankCell is similar to PostCell in that we're basically creating bounding rectangles and drawing into them. The extra little bit we're doing here is loading up a NSImage which is our sprite set and using that to pull out all of the individual service icons. NSImage makes it easy to work with our sprite image by providing drawInRect(rect, fromRect:from_rect, operation:op, fraction:val). drawInRect:fromRect:operation:fraction: draws into the rectangle defined by rect retrieving the pixels in your NSImage that are inside the from_rect. I'm using NSCompositeSourceOver 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.

[Slashdot] [Digg] [Reddit] [del.icio.us] [Facebook] [Technorati] [Google] [StumbleUpon]

Next Page »