Echographia

making things better, making better things

Sunday, February 8, 2009

dynamic Dock menus in MacRuby

I’ve been wanting to write a Mac application since I got this computer a year or so ago. Still haven’t, but today I ran through a MacRuby/Cocoa tutorial, and I’m hoping to find the time to code the app I have in mind soon, before I forget whatever I just learned. More about that if it happens.

My original plan was to make the app accessible from the global menu bar, but then I learned that Apple’s Human Interface Guidelines discourage it, and you know how I love guidelines. And humans. So instead I’m going to use a Dock menu. The main advantage of both, for my purposes, is that after you take some action, control returns immediately to whatever application you were using; perfect for programs you use for a moment now and then, but don’t really spend time in.

Anyway, I thought I’d share what I learned about implementing Dock menus in MacRuby. The instructions in this post assume you’ve already implemented the Friends app in the aforementioned tutorial. Friends has a single window with a list of, yes, friends; a button adds a new friend named John Smith, and you can edit your new friend’s name by clicking on it. Pretty fun.

For the sake of learning, I added a Dock menu that allows you to remove friends by name. This – adding with a button, editing in place, and removing all the way over in the Dock – is a terrible user interface. But here’s how I made it.

There’s a nice Interface Builder way to create Dock menus. But it only works if you know in advance what’s going to be in the menu. In this case the menu depends on your list of friends. So we have to build it dynamically, in code.

To provide a Dock menu from code, we have to create an application delegate that responds to the applicationDockMenu: selector. I don’t know if it’s good practice, but I chose to make the Controller class my application delegate. This is simple: In Interface Builder, control-drag from File’s Owner to Controller, and then select the delegate outlet. Back in Xcode, add the required method to Controller.rb:

  def applicationDockMenu(sender)
    @dock_menu
  end

So what’s @dock_menu? It’s an NSMenu. I instantiated it in awakeFromNib:

  def awakeFromNib
    ...
    @dock_menu = NSMenu.new
  end

Now we’ve got an empty menu. Let’s fill it with stuff! We can add to it every time the Add button is pressed, by extending the existing method:

  def addFriend(sender)
    ...
    item = NSMenuItem.new.initWithTitle(
      "Remove #{new_friend.first_name} #{new_friend.last_name}",
      action: 'removeFriend:',
      keyEquivalent: ""
    )
    item.representedObject = new_friend
    @dock_menu.addItem(item)
  end

The hardest thing for me to figure out, as a Cocoa newbie, was the proper value for action:. It should be the name of a method on the calling class; with a colon at the end, signifying that it takes a single argument. Note also the use of representedObject, which lets us connect the Friend object to the new menu item.

That comes in handy in the removeFriend action mentioned above:

  def removeFriend(sender)
    friend = sender.representedObject
    @friends.delete(friend)
    @friendsTableView.reloadData
    @dock_menu.removeItem(sender)
  end

removeFriend is called from the menu item; we ask it for that Friend object we previously told it to represent, and delete the friend from the data model. Then we update the table view, and remove the menu item from the Dock menu.

Voila! Now we can delete friends.

Unfortunately, the menu item won’t be updated if you rename your friend, which means you’ll only ever have a list of John Smiths in the Dock menu. We could hook into the tableView method that handles updates. Another approach would be just to dynamically generate a new NSMenu each time a Dock menu is requested. I’m not too sure how deallocation is handled in MacRuby, so I don’t know if that’s a reasonable thing to do. In any case, it’s an exercise left for someone who cares more about their Friends app than I do.

(Aside from the MacRuby/Cocoa tutorial, my biggest source of insight in this process was Brian Christensen’s 2001 tutorial on Dock menus in Objective C; and Johannes Fahrenkrug’s MacRuby and Core Data tutorial clued me in on how to set up the app delegate. Thanks!)

posted by erik at 2:48 am  

No Comments

No comments yet.

RSS feed for comments on this post.

Sorry, the comment form is closed at this time.

Powered by WordPress