EverydayMenu
Updates
- 0.2.0:
- Create
EverydayCommandto allow control of enablement of menu items
- Create
- 0.2.1:
- Fix a set method issue and resolve the error messages about missing methods
- 0.3.0:
- Add handling for
NSApp.servicesMenu,NSApp.windowsMenu, andNSApp.helpMenu
- Add handling for
- 0.4.0:
- Please see the "Introducing Presets!" section below for an awesome new feature!
- 1.0.0:
- Please see the "Introducing Statusbar Menus!" section below for another awesome new feature!
- 1.1.0:
- Added reference to parent
MenuIteminstance toEverydayCommand
- Added reference to parent
- 1.2.0:
- Added the ability to have individual ids for each command
- 1.3.0:
- Commands now get a random id if you don't give them one
- You can now access a command by id
- I now have a runtime dependency, the gem
rm-digest, but it has the necessary objective-c code built-in, so there shouldn't be any extra work for users ofeveryday-menu
- 1.3.1:
- Oops, I forgot to test outside the gem before releasing. The dependency issue should be fixed now.
- 1.3.2:
- Get tests working and add the missing
selectItemmethod inEverydayMenu::Menu
- Get tests working and add the missing
Credit
Please note that this gem is based off of Joe Fiorini's drink-menu gem (with a little code copy-paste and lots of test and readme copy-paste), which I couldn't get to work for me.
You can find his gem at https://github.com/joefiorini/drink-menu. He doesn't get all of the credit, but he gets a fair amount of it.
Installation
Add this line to your application's Gemfile:
gem 'everyday-menu'
And then execute:
$ bundle
Or install it yourself as:
$ gem install everyday-menu
Usage
Everyday Menu separates menu layout from menu definition. Menu definition looks like:
class MainMenu
extend EverydayMenu::MenuBuilder
:hide_others, 'Hide Others', key_equivalent: 'H', key_equivalent_modifier_mask: NSCommandKeyMask|NSAlternateKeyMask
:quit, 'Quit', key_equivalent: 'q'
:services, 'Services', services_menu: true
:services_item, 'Services', submenu: :services
:open, 'Open', key_equivalent: 'o'
:new, 'New'
:close, 'Close', key_equivalent: 'w'
:start_stop, 'Start'
end
Layout is as simple as:
class MainMenu
extend EverydayMenu::MenuBuilder
mainMenu(:app, 'Blah') {
hide_others
___
services_item
___
quit
}
mainMenu(:file, 'File') {
new
open
___
close
___
start_stop
}
end
And actions are as simple as:
class AppDelegate
def applicationDidFinishLaunching(notification)
@has_open = false
MainMenu.build!
MainMenu[:app].subscribe(:hide_others) { |_, _| NSApp.hideOtherApplications(self) }
MainMenu[:app].subscribe(:quit) { |_, _| NSApp.terminate(self) }
MainMenu[:file].subscribe(:start_stop, :start_stop_command_id) { |command, _|
@started = !@started
command.parent[:title] = @started ? 'Stop' : 'Start'
puts "subscribe 1 command id: #{command.command_id}"
}
MainMenu[:file].subscribe(:start_stop, :start_stop_command_id2) { |command, _|
puts "subscribe 2 command id: #{command.command_id}"
}
MainMenu[:file].subscribe(:new) { |_, _|
@has_open = true
puts 'new'
}
MainMenu[:file].subscribe(:close) { |_, _|
@has_open = false
puts 'close'
}.canExecuteBlock { |_| @has_open }
MainMenu[:file].subscribe(:open) { |command, _|
@has_open = true
puts 'open'
puts "open subscribe 1 command id: #{command.command_id}"
}
MainMenu[:file].subscribe(:open) { |command, _|
puts "open subscribe 2 command id: #{command.command_id}"
}
puts "start_stop subscribe 1 parent label: #{MainMenu[:file].items[:start_stop][:commands][:start_stop_command_id].label}"
end
end
You can even put multiple actions on a single item by calling subscribe multiple times.
The block passed to subscribe takes two parameters, the command instance and the sender. The command instance has knowledge of the label (command.label) and (as of version 1.1.0) the parent EverydayMenu::MenuItem instance (command.parent). In the above example, the parent instance is used to toggle the menu item text between 'Start' and 'Stop'.
Introducing Presets!
With version 0.4.0, I have added the capability to use some presets. Here is the above example with presets:
class MainMenu
extend EverydayMenu::MenuBuilder
:hide_others, 'Hide Others', preset: :hide_others
:show_all, 'Show All', preset: :show_all
:quit, 'Quit', preset: :quit
:services_item, 'Services', preset: :services
:open, 'Open', key_equivalent: 'o'
:new, 'New'
:close, 'Close', key_equivalent: 'w'
end
with actions defined as:
class AppDelegate
def applicationDidFinishLaunching(notification)
@has_open = false
MainMenu.build!
MainMenu[:file].subscribe(:new) { |_, _|
@has_open = true
puts 'new'
}
MainMenu[:file].subscribe(:close) { |_, _|
@has_open = false
puts 'close'
}.canExecuteBlock { |_| @has_open }
MainMenu[:file].subscribe(:open) { |_, _|
@has_open = true
puts 'open'
}
end
end
I didn't use a preset for close because there was special handling. Here are the presets and what they do:
| Preset | Settings | Action |
|---|---|---|
:hide |
key_equivalent: 'h' |
{ |_, _| NSApp.hide(self) } |
:hide_others |
key_equivalent: 'H' and :key_equivalent_modifier_mask: NSCommandKeyMask|NSAlternateKeyMask |
{ |_, _| NSApp.hideOtherApplications(self) } |
:show_all |
none | { |_, _| NSApp.unhideAllApplications(self) } |
:quit |
key_equivalent: 'q' |
{ |_, _| NSApp.terminate(self) } |
:close |
key_equivalent: 'w' |
{ |_, _| NSApp.keyWindow.performClose(self) } |
:services |
submenu: (menu :services, <item-title>, services_menu: true) |
none |
Let me know if you have any others you think I should add. If you want to add one of your own, I have included the ability to define presets. You will want to do this at the top of the file where you setup your menu items. Here is an example:
EverydayMenu::MenuItem.definePreset(:hide_others) { |item|
item[:key_equivalent] = 'H'
item[:key_equivalent_modifier_mask] = NSCommandKeyMask|NSAlternateKeyMask
item.subscribe { |_, _| NSApp.hideOtherApplications(item) }
}
Since the block is being run after the item instance is created, you have to use the other syntax, item[<key>]= in order to set the values. If you want to create a submenu in this, you can use EverydayMenu::Menu.create(label, title, options = {}), which accepts the same parameters as the menu method when building the menu normally.
If you set some application property (like NSApp.servicesMenu) in your method, you should probably have that delayed until the whole menu setup is built. You can do that like this:
EverydayMenu::MenuItem.definePreset(:services) { |item|
item[:submenu] = Menu.create(:services_menu, item[:title], services_menu: true)
item.registerOnBuild { NSApp.servicesMenu = item[:submenu] }
}
Any block you pass to item.registerOnBuild(&block) will be added to a list of blocks to be run when the menu setup is built.
Introducing Statusbar Menus!
As of version 1.0.0, everyday-menu now supports creating statusbar menus. With this addition, I believe I have finally matched all of the important features of drink-menu.
Here's how you can make a menu be for the statusbar icon:
class MainMenu
extend EverydayMenu::MenuBuilder
:status_open, 'Open', key_equivalent: 'o'
:status_new, 'New'
:status_close, 'Close', key_equivalent: 'w'
:status_quit, 'Quit', preset: :quit
(:statusbar, 'Statusbar Menu', status_item_icon: 'icon', status_item_view_class: ViewClass) {
status_new
status_open
___
status_close
___
status_quit
}
end
This will create a statusbar menu with the specified title, icon, and view class.
You can also create a statusbar menu by using the key status_item_title:, status_item_icon:, and/or status_item_view_class: in a regular (non-main) menu. Other than the addition of these parameters, a statusbar menu has all of the same parameters as a regular menu.
Known Issues
Here are known issues. If you encounter one, please log a bug ticket in the issue tracker (link above)
- Some methods in
NSMenuItemthat set values don't like being called withsend. I have to handle these on a case-by-case basis. Please log a bug in my issue tracker (link above) with any you find. It is possible thatNSMenumight have the same issue.
Running the Examples
To run our example apps:
- Clone this repo
- From within your clone's root, run
platform=osx example=basic_main_menu rake
You can replace the value of example with any folder under the examples directory to run that example.
Contributing
- Fork it
- Create your feature branch (
git checkout -b my-new-feature) - Commit your changes (
git commit -am 'Add some feature') - Push to the branch (
git push origin my-new-feature) - Create new Pull Request



