5. Make your own menus

Do you miss copy-and-paste? It’s time to get our menus and keyboard shortcuts back. But now, instead of reading them from a nib file, we will define them in Ruby. To show you how, here’s the Ruby definition for the menus that were in our original project template. These menus are defined in a simple Ruby domain specific language (DSL) that I’ll show you soon. You should find it all quite self-explanatory:

Menu definition [ruby]
include MenuMaker

def make_menu(appname = "MyApplication")
  menu("Main") do
    menu("#{appname}") do
      item("About #{appname}").withAction("orderFrontStandardAboutPanel:")
      item("Preferences...").withKey(",")
      separator
      menu("Services")
      separator
      item("Hide #{appname}").withAction("hide:").withKey("h")
      item("Hide Others").withAction("hideOtherApplications:").withKey("h").
        withModifier(OSX::NSAlternateKeyMask + OSX::NSCommandKeyMask)
      item("Show All").withAction("unhideAllApplications:")
      separator
      item("Quit #{appname}").withAction("terminate:").withKey("q")
    end
    menu("File") do
      item("New")
      item("Open...").withKey("o")
      menu("Open Recent") do
        item("Clear Menu").withAction("clearRecentDocuments:")
      end
      separator
      item("Close").withAction("performClose:").withKey("w")
      item("Save").withKey("s")
      item("Save As...").withKey("S")
      item("Revert")
      separator
      item("Page Setup...").withAction("runPageLayout:").withKey("P")
      item("Print...").withAction("print:").withKey("p")
    end
    menu("Edit") do
      item("Undo").withAction("undo:").withKey("z")
      item("Redo").withAction("redo:").withKey("Z")
      separator
      item("Cut").withAction("cut:").withKey("x")
      item("Copy").withAction("copy:").withKey("c")
      item("Paste").withAction("paste:").withKey("v")
      item("Delete").withAction("delete:")
      item("Select All").withAction("selectAll:").withKey("a")
      separator
      menu("Find") do
        item("Find...").withKey("f")
        item("Find Next").withKey("g")
        item("Find Previous").withKey("d")
        item("Use Selection for Find").withKey("e")
        item("Scroll to Selection").withKey("j")
      end
      menu("Spelling") do
        item("Spelling...").withAction("showGuessPanel:")
        item("Check Spelling").withAction("checkSpelling:")
        item("Check Spelling as You Type").
          withAction("toggleContinuousSpellChecking:")
      end
    end
    menu("Window") do
      item("Minimize").withAction("performMiniaturize:").withKey("m")
      separator
      item("Bring All to Front").withAction("arrangeInFront:")
    end
    menu("Help") do
      item("#{appname} Help").withAction("showHelp:").withKey("?")
    end
  end
end

Notice that this code accepts an application name as a parameter, and that it uses it to insert that name in all the appropriate menu items. The menu, item, and separator functions are all defined in a module called MenuMaker. The include statement at the top of the listing lets us use them without the MenuMaker qualifier.

Here’s the definition of the MenuMaker domain specific language. I wrote it while listening to talks at the Silicon Valley Ruby Conference last April. It was inspired by a DSL that Rich Kilmer showed us there. The new methods that I added to NSMenuItem are simply to allow me to chain them together into a single line of code. Of course, you could probably do something like this in Objective-C, but I’ll bet it would be a lot more verbose, and it would be easy to miss something. With Ruby I can put a lot more in front of you at once.

MenuMaker [ruby]
# The following line is temporarily necessary if you are using 
# recent RubyCocoa builds from the subversion archives
OSX.ns_import :NSMenuItem

module MenuMaker
  @@context = []
  def _context
    @@context[-1]
  end
  def separator
    _context.insertItem_atIndex(
       OSX::NSMenuItem.separatorItem, _context.numberOfItems)
  end
  def item(name)
    i = OSX::NSMenuItem.alloc.
         initWithTitle_action_keyEquivalent(name, nil, "")
    _context.insertItem_atIndex(i, _context.numberOfItems)
    i
  end
  def menu(name)
    m = OSX::NSMenu.alloc.initWithTitle name
    if _context
      i = OSX::NSMenuItem.alloc.
            initWithTitle_action_keyEquivalent(name, nil, "")
      _context.insertItem_atIndex(i, _context.numberOfItems)
      i.setSubmenu m
    end
    if block_given?
      @@context.push(m)
      yield
      @@context.pop
    end
    m.setAutoenablesItems true
    if not _context
      OSX::NSApplication.sharedApplication.setMainMenu m
    elsif name == "Window"
      OSX::NSApplication.sharedApplication.setWindowsMenu m
    elsif name == "Services"
      OSX::NSApplication.sharedApplication.setServicesMenu m
    end
    m
  end
  class OSX::NSMenuItem
    def withAction(action)
      self.setAction(action)
      self
    end
    def withTarget(target)
      self.setTarget(target)
      self
    end
    def withKey(key)
      self.setKeyEquivalent(key)
      self
    end
    def withModifier(modifier)
      self.setKeyEquivalentModifierMask(modifier)
      self
    end
  end
end
To use this, create a file called menu.rb in your project directory. Add the MenuMaker code to the top and put the menu definition from the previous section at the bottom. Then, while your application is still running, type “require menu” at the console, followed by
make_menu('Ruby Made Cocoa My Servant')
Do you see your new menu? You can make that call again and set the title to something more or less polite (suit yourself). Add it to your ApplicationDelegate’s applicationDidFinishLaunching method and it will run every time your application starts up (but be sure to put it before the call to $consoleWindowController.run).

Did you find an error? Is something missing? Post your comment or suggestion below!

Comments (3) post
  1. Steve Mon Aug 21 21:42:09 +0000 2006

    I tried to put the MenuMaker module and make_menu() method in different files so that the MenuMaker module could be reused. This worked fine within RubyApp when I did “included MenuMaker” then “make_menu( ‘Ruby Roolz!’)” works.

    But I couldn’t hit on the right combination of includes or requires to make it work when make_menu() was called from the ApplicationDelegate. If MenuMaker were in menumaker.rb and make_menu() were in menu.rb, and make_menu( “RubyApp”) were called in the applicationDidFinishLaunching method of the ApplicationDelegate in console.rb, what needs to be included or required where to make it work?

  2. Tim Burks Tue Aug 22 20:27:07 +0000 2006

    You might want to add some puts or OSX.NSLog statements in each of your source files to confirm that they are all being loaded.  If they are all loaded before the applicationDidFinishLaunching method is called, you should be fine.  But watch out for name conflicts.  I once tried to use a file named “delegate.rb”, but whenever I tried to load it with “require ‘delegate’”, Ruby would instead load delegate.rb from the standard Ruby library.

    If you’re still having problems, send me a tar file of your sources and I’ll take a look.

  3. Matt Sun Nov 19 00:15:25 +0000 2006

    This is definately cool stuff. I did have a little trouble with the fact that the interpreter (IRB) is started up with the working directory set to ”/” and the project directory isn’t included in $LOAD_PATH, so I had to type a full path to menu.rb in the require statement.