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]
Posted in Computers, Programming | Tagged , , , | Leave a comment

EventMachine Screencast — EM-HTTP-Request

IMG_6037I decided to try my hand at creating a screencast the other day. I took a look at the EventMachine EM-HTTP-Request library and created a simple shell program to do single and multi requests. Take a look and let me know what you think.

require ‘rubygems’
require ‘eventmachine’
require ‘em-http’
require ‘pp’

$stdout.sync = true

class KeyboardHandler < EM::Connection
  include EM::Protocols::LineText2
 
  def post_init
    print "> "
  end
 
  def receive_line(line)
    line.chomp!
    line.gsub!(/^\s+/, )
   
    case(line)
    when /^get (.*)$/ then
      site = $1.chomp
      sites = site.split(‘,’)
     
      multi = EM::MultiRequest.new
      sites.each do |s|
        multi.add(EM::HttpRequest.new(s).get)
      end
      multi.callback {
        puts ""
        multi.responses[:succeeded].each do |h|
          pp h.response_header.status
          pp h.response_header
        end
        multi.responses[:failed].each do |h|
          puts "#{h.inspect} failed"
        end
        print "> "
      }
      print "> "

    when /^exit$/ then
      EM.stop

    when /^help$/ then
      puts "get URL[,URL]*   – gets a URL"
      puts "exit      - exits the app"
      puts "help      - this help"
      print "> "
    end
  end
end

EM::run {
  EM.open_keyboard(KeyboardHandler)
}
puts "Finished"

[Slashdot] [Digg] [Reddit] [del.icio.us] [Facebook] [Technorati] [Google] [StumbleUpon]
Posted in Computers, Programming, Screencasts | Tagged , , | 5 Comments

Google Analytics, OAuth and Ruby. Oh, my.

IMG_6658I recently had the opportunity to take a peek at accessing Google Analytics data using their OAuth endpoint. We did this in Ruby using the Ruby OAuth gem. There were a few snags along the way so I figured I’d put up some notes for other people to take a peek.

First, you’re going to need a newer version of the OAuth gem (0.3.5 seems to work). Google uses some features from the 1.0a version of the OAuth spec, the oauth_verifier specifically, that aren’t available in older versions of the OAuth gem. That took a while to track down.

The second thing you’re going to need is a consumer token and secret from google. You can retrieve this from your domain management page (here as it’s a bit difficult to find.)

require ‘oauth’
con = OAuth::Consumer.new(CONSUMER_TOKEN, CONSUMER_SECRET,
                              {:site => ‘https://www.google.com’,
                               :request_token_path => ‘/accounts/OAuthGetRequestToken’,
                               :access_token_path => ‘/accounts/OAuthGetAccessToken’,
                               :authorize_path => ‘/accounts/OAuthAuthorizeToken’})

To start we create our OAuth connection. You’ll need to substitute your token and secret for the consumer constants above. We need to specify all of the OAuth paths to match the endpoints provided by Google.

rt = con.get_request_token({}, {:scope => ‘https://www.google.com/analytics/feeds’})

We use the connection to create a request token. get_request_token takes two parameters. The token parameters and other parameters. Above, I’m not specifying any request parameters but, if you want Google to redirect the user to a site of your choice you would provide {:oauth_callback => URL} as the token parameter. After authenticating the user would be redirected to the provided URL and the parameters oauth_token=TOKEN and oauth_verifier=VERIFIER will be passed to the URL. You’ll need to use those parameters when creating the access token.

The second parameter when creating the request token is used by Google to restrict the users access to specific portions of Google. In our case we’re requesting access to the Analytics data. A list of available scopes is here. You can specify multiple scope values by putting a space between them. e.g. "http://www.google.com/calendar/feeds/%20http://docs.google.com/feeds/".

rt.authorize_url       => "https://www.google.com/accounts/OAuthAuthorizeToken?oauth_token=4%2F-1o2aZgHxJmjhaExv9htRsGWLlwy"

google-1With our request token in hand we can use it to generate the authorization URL. You’ll need to redirect the user to this URL to do the authentication.

Since we aren’t providing a callback URL our users will just get redirected to a standard Google page and you’ll need to copy their verifier code into the application.

at = rt.get_access_token(:oauth_verifier => ‘vQH9bpXateNpsISpEDZax’)

google-2When the user has authorized we either get them to paste the verifier code or we can retrieve it from the parameters passed to our oauth_callback. We use the verifier code to create an access token. This is the token we’ll be using to access the Google resources. We can save the at.token and at.secret values into our database to create new access tokens next time the user needs data from Google. We don’t need to authenticate each time.

at = OAuth::AccessToken.new(con, USER_TOKEN, USER_SECRET)

We can also, if we’re using the oauth_callback store some of our request token information to the session. If we store rt.token and rt.secret we can re-build our request token in a simlar fashion to the access token.

rt = OAuth::RequestToken.new(con, REQUEST_TOKEN, REQUEST_SECRET)

With the authentication out of the way we can get on with accessing the analytics data.

puts at.get("https://www.google.com/analytics/feeds/accounts/default?prettyprint=true").body

We grab our profile data first. You’ll need the dxp:tableId value in order to make calls to get profile specific analytic data. (The prettyprint=true is a handy little flag to get Google to nicely indent the XML returned. Makes for easier reading/debugging.)

< ?xml version=’1.0′ encoding=‘UTF-8′?>
<feed xmlns=‘http://www.w3.org/2005/Atom’ xmlns:openSearch=‘http://a9.com/-/spec/opensearchrss/1.0/’ xmlns:dxp=‘http://schemas.google.com/analytics/2009′>
  <id>http://www.google.com/analytics/feeds/accounts/dan.sinclair@gmail.com</id>
  <updated>2009-07-06T11:00:09.000-07:00</updated>
  <title type=‘text’>Profile list for dan.sinclair@gmail.com</title>
  <link rel=‘self’ type=‘application/atom+xml’ href=‘http://www.google.com/analytics/feeds/accounts/default’/>
  <author>
    <name>Google Analytics</name>
  </author>
  <generator version=’1.0′>Google Analytics</generator>
  <opensearch :totalResults>1</opensearch>
  <opensearch :startIndex>1</opensearch>
  <opensearch :itemsPerPage>1</opensearch>
  <entry>
    <id>http://www.google.com/analytics/feeds/accounts/ga:123xxxx</id>
    <updated>2009-07-06T11:00:09.000-07:00</updated>
    <title type=‘text’>everburning</title>
    <link rel=‘alternate’ type=‘text/html’ href=‘http://www.google.com/analytics’/>
    <dxp :tableId>ga:123xxxx</dxp>
    <dxp :property name=‘ga:accountId’ value=’73xxxx’/>
    <dxp :property name=‘ga:accountName’ value=‘everburning’/>
    <dxp :property name=‘ga:profileId’ value=’123xxxx’/>
    <dxp :property name=‘ga:webPropertyId’ value=‘UA-73xxxx-1′/>
    <dxp :property name=‘ga:currency’ value=‘USD’/>
    <dxp :property name=‘ga:timezone’ value=‘America/Toronto’/>
  </entry>
</feed>

puts at.get("https://www.google.com/analytics/feeds/data?ids=ga:123xxxx&start-date=2009-06-05&end-date=2009-06-06&dimensions=ga:pagePath&metrics=ga:pageviews,ga:uniquePageviews&prettyprint=true").body

The data I want from Google Analytics is the number of ga:pageviews and ga:uniquePageviews for each ga:pagePath between June 5th and 6th.

< ?xml version=’1.0′ encoding=‘UTF-8′?>
<feed xmlns=‘http://www.w3.org/2005/Atom’ xmlns:openSearch=‘http://a9.com/-/spec/opensearchrss/1.0/’ xmlns:dxp=‘http://schemas.google.com/analytics/2009′>
  <id>http://www.google.com/analytics/feeds/data?ids=ga:123xxxx&amp;dimensions=ga:pagePath&amp;metrics=ga:pageviews,ga:uniquePageviews&amp;start-date=2009-06-05&amp;end-date=2009-06-06</id>
  <updated>2009-06-06T16:59:59.999-07:00</updated>
  <title type=‘text’>Google Analytics Data for Profile 123xxxx</title>
  <link rel=‘self’ type=‘application/atom+xml’ href=‘http://www.google.com/analytics/feeds/data?end-date=2009-06-06&amp;start-date=2009-06-05&amp;metrics=ga%3Apageviews%2Cga%3AuniquePageviews&amp;ids=ga%3A123xxxx&amp;dimensions=ga%3ApagePath’/>
  <author>
    <name>Google Analytics</name>
  </author>
  <generator version=’1.0′>Google Analytics</generator>
  <opensearch :totalResults>52</opensearch>
  <opensearch :startIndex>1</opensearch>
  <opensearch :itemsPerPage>52</opensearch>
  <dxp :startDate>2009-06-05</dxp>
  <dxp :endDate>2009-06-06</dxp>
  <dxp :aggregates>
    <dxp :metric confidenceInterval=’0.0′ name=‘ga:pageviews’ type=‘integer’ value=’162′/>
    <dxp :metric confidenceInterval=’0.0′ name=‘ga:uniquePageviews’ type=‘integer’ value=’152′/>
  </dxp>
  <dxp :dataSource>
    </dxp><dxp :tableId>ga:1239xxxx</dxp>
    <dxp :tableName>everburning</dxp>
    <dxp :property name=‘ga:profileId’ value=’123xxxx5′/>
    <dxp :property name=‘ga:webPropertyId’ value=‘UA-73xxxx-1′/>
    <dxp :property name=‘ga:accountName’ value=‘everburning’/>
 
  <entry>
    <id>http://www.google.com/analytics/feeds/data?ids=ga:123xxxx&amp;ga:pagePath=/&amp;start-date=2009-06-05&amp;end-date=2009-06-06</id>
    <updated>2009-06-05T17:00:00.001-07:00</updated>
    <title type=‘text’>ga:pagePath=/</title>
    <link rel=‘alternate’ type=‘text/html’ href=‘http://www.google.com/analytics’/>
    <dxp :dimension name=‘ga:pagePath’ value=‘/’/>
    <dxp :metric confidenceInterval=’0.0′ name=‘ga:pageviews’ type=‘integer’ value=’9′/>
    <dxp :metric confidenceInterval=’0.0′ name=‘ga:uniquePageviews’ type=‘integer’ value=’8′/>
  </entry>
  <entry>
    <id>http://www.google.com/analytics/feeds/data?ids=ga:123xxxx&amp;ga:pagePath=/quotes/&amp;start-date=2009-06-05&amp;end-date=2009-06-06</id>
    <updated>2009-06-05T17:00:00.001-07:00</updated>
    <title type=‘text’>ga:pagePath=/quotes/</title>
    <link rel=‘alternate’ type=‘text/html’ href=‘http://www.google.com/analytics’/>
    <dxp :dimension name=‘ga:pagePath’ value=‘/quotes/’/>
    <dxp :metric confidenceInterval=’0.0′ name=‘ga:pageviews’ type=‘integer’ value=’6′/>
    <dxp :metric confidenceInterval=’0.0′ name=‘ga:uniquePageviews’ type=‘integer’ value=’6′/>
  </entry>
</feed>

You may notice that all of the paths returned are relative to some base URL. I haven’t been able to figure out how to get Google Analytics to tell me what that base URL happens to be. If you figure it out, please leave a comment so I can update my code.

That’s it. You can take a deeper look at the Google Analytics API documentation to learn about the different dimensions and metrics that can be queried.

If you’re doing this in Ruby you may also want to take a look at the Happy Mapper gem to make your XML using life easier.

Have fun.

[Slashdot] [Digg] [Reddit] [del.icio.us] [Facebook] [Technorati] [Google] [StumbleUpon]
Posted in Computers, Programming | Tagged , , | 6 Comments

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]
Posted in Computers, Programming | Tagged , , | 2 Comments

MacRuby and NSError

Just a note for other people that might be cracking their heads trying to meld MacRuby and NSError ** parameters. The answer was already sitting on my computer, I just never through to look at the MacRuby Core Data XCode templates, but that’s beside the point. For those of you searching, you just need to do the following.

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

The NSLog error[0].inspect will output #<NSError:0x80057a640>. Exactly what we want.

[Slashdot] [Digg] [Reddit] [del.icio.us] [Facebook] [Technorati] [Google] [StumbleUpon]
Posted in Computers, Programming | Tagged , | 1 Comment