/everyday-menu

An easy way to define menu items and visually lay out menus for your OSX apps. Based strongly on the drink-menu gem that I couldn't get to work for me.

Primary LanguageRubyOtherNOASSERTION

EverydayMenu

Gem Version Build Status Dependency Status Code Climate

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.

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
  
  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)

  1. 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:

  1. Clone this repo
  2. 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

  1. Fork it
  2. Create your feature branch (git checkout -b my-new-feature)
  3. Commit your changes (git commit -am 'Add some feature')
  4. Push to the branch (git push origin my-new-feature)
  5. Create new Pull Request