Updates
- 0.2.0:
- Create
EverydayCommand
to allow control of enablement of menu items
- 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
, and NSApp.helpMenu
- 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
MenuItem
instance to EverydayCommand
- 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 of everyday-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
selectItem
method in EverydayMenu::Menu
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
menuItem :hide_others, 'Hide Others', key_equivalent: 'H', key_equivalent_modifier_mask: NSCommandKeyMask|NSAlternateKeyMask
menuItem :quit, 'Quit', key_equivalent: 'q'
menu :services, 'Services', services_menu: true
menuItem :services_item, 'Services', submenu: :services
menuItem :open, 'Open', key_equivalent: 'o'
menuItem :new, 'New'
menuItem :close, 'Close', key_equivalent: 'w'
menuItem :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
menuItem :hide_others, 'Hide Others', preset: :hide_others
menuItem :show_all, 'Show All', preset: :show_all
menuItem :quit, 'Quit', preset: :quit
menuItem :services_item, 'Services', preset: :services
menuItem :open, 'Open', key_equivalent: 'o'
menuItem :new, 'New'
menuItem :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.
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
menuItem :status_open, 'Open', key_equivalent: 'o'
menuItem :status_new, 'New'
menuItem :status_close, 'Close', key_equivalent: 'w'
menuItem :status_quit, 'Quit', preset: :quit
statusbarMenu(: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
NSMenuItem
that set values don't like being called with send
. 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 that NSMenu
might 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