5. Make your own menus
5.1 A Ruby menu definition
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:
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.
5.2 A menu-making DSL
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.
# 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
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
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?
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.
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.