drink-menu 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +19 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +80 -0
- data/Rakefile +23 -0
- data/drink-menu.gemspec +26 -0
- data/lib/drink-menu.rb +9 -0
- data/lib/drink-menu/ext/forwardable.rb +131 -0
- data/lib/drink-menu/ext/ns_menu_item.rb +55 -0
- data/lib/drink-menu/menu.rb +174 -0
- data/lib/drink-menu/menu_builder.rb +83 -0
- data/lib/drink-menu/menu_item.rb +92 -0
- data/lib/drink-menu/version.rb +3 -0
- data/spec/menu_builder_spec.rb +87 -0
- data/spec/menu_item_spec.rb +82 -0
- data/spec/menu_spec.rb +195 -0
- metadata +122 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: aa0caded737aae5b8e19f02d234a62bec5532129
|
4
|
+
data.tar.gz: 3670d72c16d59197c56304cfe94992ab2218e71f
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 30a6f653df7169b157e7424c5be84b35c34c3f339a88a3361f057f3e3822092005c2df993a1e91fe6dca366b2b70bf2a83a975f83fc719f849750ed8b301b892
|
7
|
+
data.tar.gz: f1d041f9171d4c9cd4ffe7671d60a82383c9d3b7d9fcf9868dd3fdf90eab3d7cc183adc37993931be0793b57bc91cdc2f496ec49c4483f455a33ae977d8710e3
|
data/.gitignore
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2013 Joe Fiorini
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,80 @@
|
|
1
|
+
# Drink::Menu
|
2
|
+
|
3
|
+
TODO: Write a gem description
|
4
|
+
|
5
|
+
## Installation
|
6
|
+
|
7
|
+
Add this line to your application's Gemfile:
|
8
|
+
|
9
|
+
gem 'drink-menu'
|
10
|
+
|
11
|
+
And then execute:
|
12
|
+
|
13
|
+
$ bundle
|
14
|
+
|
15
|
+
Or install it yourself as:
|
16
|
+
|
17
|
+
$ gem install drink-menu
|
18
|
+
|
19
|
+
## Usage
|
20
|
+
|
21
|
+
Drink Menu separates menu layout from menu definition. Menu definition looks like:
|
22
|
+
|
23
|
+
|
24
|
+
```ruby
|
25
|
+
class MainMenu
|
26
|
+
extend DrinkMenu::MenuBuilder
|
27
|
+
|
28
|
+
menuItem :progress do |item|
|
29
|
+
end
|
30
|
+
|
31
|
+
menu :sites_list, itemsFromCollection: Staticly.sitesList, titleProperty: :name
|
32
|
+
|
33
|
+
menuItem :open_site, title: 'Open Site', submenu: :sites_list
|
34
|
+
|
35
|
+
menuItem :create_site, title: 'Create Site'
|
36
|
+
menuItem :export, title: 'Export to Folder...'
|
37
|
+
menuItem :import, title: 'Import Folder as Site...'
|
38
|
+
menuItem :force_rebuild, title: 'Force Rebuild'
|
39
|
+
menuItem :about, title: 'About Staticly'
|
40
|
+
menuItem :quit, title: 'Quit'
|
41
|
+
|
42
|
+
|
43
|
+
iconImage = NSImage.imageNamed "status-icon-off"
|
44
|
+
iconImage.template = true
|
45
|
+
|
46
|
+
end
|
47
|
+
```
|
48
|
+
|
49
|
+
and then layout is as simple as:
|
50
|
+
|
51
|
+
```ruby
|
52
|
+
|
53
|
+
class MainMenu
|
54
|
+
extend DrinkMenu::MenuBuilder
|
55
|
+
|
56
|
+
statusBarMenu :main_menu, icon: iconImage, statusItemViewClass: StatusItemView do
|
57
|
+
open_site
|
58
|
+
create_site
|
59
|
+
___
|
60
|
+
export
|
61
|
+
import
|
62
|
+
force_rebuild
|
63
|
+
___
|
64
|
+
about
|
65
|
+
quit
|
66
|
+
end
|
67
|
+
|
68
|
+
end
|
69
|
+
```
|
70
|
+
|
71
|
+
More detailed documentation coming soon.
|
72
|
+
|
73
|
+
|
74
|
+
## Contributing
|
75
|
+
|
76
|
+
1. Fork it
|
77
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
78
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
79
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
80
|
+
5. Create new Pull Request
|
data/Rakefile
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
2
|
+
$:.unshift("/Library/RubyMotion/lib")
|
3
|
+
|
4
|
+
if ENV['platform'] == 'osx'
|
5
|
+
require 'motion/project/template/osx'
|
6
|
+
else
|
7
|
+
raise "The drink-menu gem must be used within an OSX project."
|
8
|
+
end
|
9
|
+
|
10
|
+
Bundler.setup
|
11
|
+
Bundler.require
|
12
|
+
|
13
|
+
require 'motion-cocoapods'
|
14
|
+
|
15
|
+
Motion::Project::App.setup do |app|
|
16
|
+
app.name = 'drink-menu'
|
17
|
+
app.identifier = 'com.densitypop.drink-menu'
|
18
|
+
app.specs_dir = "spec/"
|
19
|
+
|
20
|
+
app.pods do
|
21
|
+
pod 'ReactiveCocoa'
|
22
|
+
end
|
23
|
+
end
|
data/drink-menu.gemspec
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'drink-menu/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "drink-menu"
|
8
|
+
spec.version = DrinkMenu::VERSION
|
9
|
+
spec.authors = ["Joe Fiorini"]
|
10
|
+
spec.email = ["joe@joefiorini.com"]
|
11
|
+
spec.description = %q{An easy way to define menu items and visually lay out menus for your OSX apps. Uses ReactiveCocoa to provide a nice syntax for responding to menu interactions. Also provides live-binding to collections, which makes keeping menus up-to-date a breeze!}
|
12
|
+
spec.summary = %q{OSX menus - the ruby way}
|
13
|
+
spec.homepage = "https://github.com/joefiorini/drink-menu"
|
14
|
+
spec.license = "MIT"
|
15
|
+
|
16
|
+
spec.files = `git ls-files`.split($/)
|
17
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
18
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
19
|
+
spec.require_paths = ["lib"]
|
20
|
+
|
21
|
+
spec.add_development_dependency "bundler", "~> 1.3"
|
22
|
+
spec.add_development_dependency "rake", "~> 10.0.0"
|
23
|
+
|
24
|
+
spec.add_dependency "motion-cocoapods"
|
25
|
+
spec.add_dependency "cocoapods", "= 0.20.2"
|
26
|
+
end
|
data/lib/drink-menu.rb
ADDED
@@ -0,0 +1,9 @@
|
|
1
|
+
unless defined?(Motion::Project::Config)
|
2
|
+
raise "The drink-menu gem must be required within a RubyMotion project Rakefile."
|
3
|
+
end
|
4
|
+
|
5
|
+
Motion::Project::App.setup do |app|
|
6
|
+
Dir.glob(File.join(File.dirname(__FILE__), 'drink-menu/**/*.rb')).each do |file|
|
7
|
+
app.files.unshift(file)
|
8
|
+
end
|
9
|
+
end
|
@@ -0,0 +1,131 @@
|
|
1
|
+
module Forwardable
|
2
|
+
FORWARDABLE_VERSION = "1.1.0"
|
3
|
+
|
4
|
+
@debug = nil
|
5
|
+
class << self
|
6
|
+
attr_accessor :debug
|
7
|
+
end
|
8
|
+
|
9
|
+
# Takes a hash as its argument. The key is a symbol or an array of
|
10
|
+
# symbols. These symbols correspond to method names. The value is
|
11
|
+
# the accessor to which the methods will be delegated.
|
12
|
+
#
|
13
|
+
# :call-seq:
|
14
|
+
# delegate method => accessor
|
15
|
+
# delegate [method, method, ...] => accessor
|
16
|
+
#
|
17
|
+
def instance_delegate(hash)
|
18
|
+
hash.each{ |methods, accessor|
|
19
|
+
methods = [methods] unless methods.respond_to?(:each)
|
20
|
+
methods.each{ |method|
|
21
|
+
def_instance_delegator(accessor, method)
|
22
|
+
}
|
23
|
+
}
|
24
|
+
end
|
25
|
+
|
26
|
+
#
|
27
|
+
# Shortcut for defining multiple delegator methods, but with no
|
28
|
+
# provision for using a different name. The following two code
|
29
|
+
# samples have the same effect:
|
30
|
+
#
|
31
|
+
# def_delegators :@records, :size, :<<, :map
|
32
|
+
#
|
33
|
+
# def_delegator :@records, :size
|
34
|
+
# def_delegator :@records, :<<
|
35
|
+
# def_delegator :@records, :map
|
36
|
+
#
|
37
|
+
def def_instance_delegators(accessor, *methods)
|
38
|
+
methods.delete("__send__")
|
39
|
+
methods.delete("__id__")
|
40
|
+
for method in methods
|
41
|
+
def_instance_delegator(accessor, method)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def def_instance_delegator(accessor, method, ali = method)
|
46
|
+
accessor = accessor.id2name if accessor.kind_of?(Integer)
|
47
|
+
method = method.id2name if method.kind_of?(Integer)
|
48
|
+
ali = ali.id2name if ali.kind_of?(Integer)
|
49
|
+
activity = Proc.new do
|
50
|
+
define_method("#{ali}") do |*args, &block|
|
51
|
+
begin
|
52
|
+
instance_variable_get(accessor).__send__(method, *args, &block)
|
53
|
+
rescue Exception
|
54
|
+
$@.delete_if{|s| %r"#{Regexp.quote(__FILE__)}"o =~ s} unless Forwardable::debug
|
55
|
+
Kernel::raise
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
# If it's not a class or module, it's an instance
|
61
|
+
begin
|
62
|
+
module_eval(&activity)
|
63
|
+
rescue
|
64
|
+
instance_eval(&activity)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
alias delegate instance_delegate
|
69
|
+
alias def_delegators def_instance_delegators
|
70
|
+
alias def_delegator def_instance_delegator
|
71
|
+
end
|
72
|
+
|
73
|
+
module SingleForwardable
|
74
|
+
# Takes a hash as its argument. The key is a symbol or an array of
|
75
|
+
# symbols. These symbols correspond to method names. The value is
|
76
|
+
# the accessor to which the methods will be delegated.
|
77
|
+
#
|
78
|
+
# :call-seq:
|
79
|
+
# delegate method => accessor
|
80
|
+
# delegate [method, method, ...] => accessor
|
81
|
+
#
|
82
|
+
def single_delegate(hash)
|
83
|
+
hash.each{ |methods, accessor|
|
84
|
+
methods = [methods] unless methods.respond_to?(:each)
|
85
|
+
methods.each{ |method|
|
86
|
+
def_single_delegator(accessor, method)
|
87
|
+
}
|
88
|
+
}
|
89
|
+
end
|
90
|
+
|
91
|
+
#
|
92
|
+
# Shortcut for defining multiple delegator methods, but with no
|
93
|
+
# provision for using a different name. The following two code
|
94
|
+
# samples have the same effect:
|
95
|
+
#
|
96
|
+
# def_delegators :@records, :size, :<<, :map
|
97
|
+
#
|
98
|
+
# def_delegator :@records, :size
|
99
|
+
# def_delegator :@records, :<<
|
100
|
+
# def_delegator :@records, :map
|
101
|
+
#
|
102
|
+
def def_single_delegators(accessor, *methods)
|
103
|
+
methods.delete("__send__")
|
104
|
+
methods.delete("__id__")
|
105
|
+
for method in methods
|
106
|
+
def_single_delegator(accessor, method)
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
#
|
111
|
+
# Defines a method _method_ which delegates to _obj_ (i.e. it calls
|
112
|
+
# the method of the same name in _obj_). If _new_name_ is
|
113
|
+
# provided, it is used as the name for the delegate method.
|
114
|
+
#
|
115
|
+
def def_single_delegator(accessor, method, ali = method)
|
116
|
+
instance_eval do
|
117
|
+
define_method("#{ali}") do |*args, &block|
|
118
|
+
begin
|
119
|
+
instance_variable_get(accessor).__send__(method, *args, &block)
|
120
|
+
rescue Exception
|
121
|
+
$@.delete_if{|s| %r"#{Regexp.quote(__FILE__)}"o =~ s} unless Forwardable::debug
|
122
|
+
::Kernel::raise
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
alias delegate single_delegate
|
129
|
+
alias def_delegators def_single_delegators
|
130
|
+
alias def_delegator def_single_delegator
|
131
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
class NSMenuItem
|
2
|
+
attr_accessor :rac_command
|
3
|
+
attr_accessor :rac_originalTarget
|
4
|
+
attr_accessor :rac_stateSignal
|
5
|
+
|
6
|
+
def rac_command=(command)
|
7
|
+
|
8
|
+
@rac_command = command
|
9
|
+
|
10
|
+
self.enabled = (command && command.canExecute) || true
|
11
|
+
|
12
|
+
return unless command
|
13
|
+
|
14
|
+
# TODO: Set state via binding rather than validateMenuItem
|
15
|
+
# Look at https://github.com/ReactiveCocoa/ReactiveCocoa/blob/2.0-development/ReactiveCocoaFramework/ReactiveCocoa/NSObject%2BRACAppKitBindings.m#L23
|
16
|
+
#
|
17
|
+
self.bind(NSEnabledBinding, toObject: self.rac_command, withKeyPath: "canExecute", options: nil)
|
18
|
+
|
19
|
+
self.rac_hijackActionAndTargetIfNeeded
|
20
|
+
|
21
|
+
@rac_command
|
22
|
+
end
|
23
|
+
|
24
|
+
def rac_stateSignal=(signal)
|
25
|
+
@rac_stateSignal = signal
|
26
|
+
@rac_stateSignal.subscribeNext ->(value){
|
27
|
+
self.state = value
|
28
|
+
}
|
29
|
+
end
|
30
|
+
|
31
|
+
def rac_hijackActionAndTargetIfNeeded
|
32
|
+
hijackSelector = :"rac_commandPerformAction:"
|
33
|
+
|
34
|
+
return if target == self and action == hijackSelector
|
35
|
+
|
36
|
+
NSLog("WARNING: NSControl.rac_command hijacks the control's existing target and action. You can access the original target via the rac_originalTarget property.") if target
|
37
|
+
|
38
|
+
self.rac_originalTarget = target
|
39
|
+
|
40
|
+
self.target = self
|
41
|
+
self.action = hijackSelector
|
42
|
+
end
|
43
|
+
|
44
|
+
def rac_commandPerformAction(sender)
|
45
|
+
rac_command.execute(sender)
|
46
|
+
end
|
47
|
+
|
48
|
+
def validateMenuItem(item)
|
49
|
+
return rac_originalTarget.validateMenuItem(item) if rac_originalTarget and rac_originalTarget.respondsToSelector(:"validateMenuItem:")
|
50
|
+
|
51
|
+
rac_command.canExecute
|
52
|
+
end
|
53
|
+
|
54
|
+
end
|
55
|
+
|
@@ -0,0 +1,174 @@
|
|
1
|
+
module DrinkMenu
|
2
|
+
class Menu
|
3
|
+
extend Forwardable
|
4
|
+
|
5
|
+
def_delegators :@menu,
|
6
|
+
:menuBarHeight, :numberOfItems,
|
7
|
+
:itemArray, :isTornOff,
|
8
|
+
:supermenu, :setSupermenu, :supermenu=,
|
9
|
+
:autoenablesItems, :setAutoenablesItems, :autoenablesItems=,
|
10
|
+
:font, :setFont, :font=,
|
11
|
+
:title, :setTitle, :title=,
|
12
|
+
:minimumWidth, :setMinimumWidth, :minimumWidth=,
|
13
|
+
:size, :propertiesToUpdate,
|
14
|
+
:menuChangedMessagesEnabled, :setMenuChangedMessagesEnabled, :menuChangedMessagesEnabled=,
|
15
|
+
:allowsContextMenuPlugins, :setAllowsContextMenuPlugins, :allowsContextMenuPlugins=,
|
16
|
+
:highlightedItem, :delegate, :setDelegate, :delegate=
|
17
|
+
|
18
|
+
attr_reader :menuItems, :menu, :builder, :label
|
19
|
+
attr_accessor :statusItem, :statusItemIcon, :memberCommand, :statusItemTitle, :memberTitleProperty, :itemAddedSubject
|
20
|
+
|
21
|
+
def self.statusMenuWithLabel(label, icon: image, statusItemViewClass: statusItemViewClass, &block)
|
22
|
+
new(label, &block).tap do |menu|
|
23
|
+
menu.createStatusItemWithIcon image, viewClass: statusItemViewClass
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def self.statusMenuWithLabel(label, icon: image, &block)
|
28
|
+
new(label, &block).tap do |menu|
|
29
|
+
menu.createStatusItemWithIcon image
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def self.statusMenuWithLabel(label, title: title, &block)
|
34
|
+
new(label, &block).tap do |menu|
|
35
|
+
menu.createStatusItemWithTitle title
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def self.menuWithLabel(label, title: title, &block)
|
40
|
+
new(label, &block).tap do |menu|
|
41
|
+
menu.title = title
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def self.menuWithLabel(label, itemsFromCollection: collection, titleProperty: titleProperty)
|
46
|
+
new(label).tap do |menu|
|
47
|
+
menu.memberTitleProperty = titleProperty
|
48
|
+
|
49
|
+
menu.itemAddedSubject = RACSubject.subject
|
50
|
+
|
51
|
+
@allItemsSignal =
|
52
|
+
menu.itemAddedSubject.flattenMap(->(value){
|
53
|
+
value.command.map(->(v){
|
54
|
+
[label, menu[v.tag]]
|
55
|
+
})
|
56
|
+
})
|
57
|
+
|
58
|
+
menu.memberCommand = @allItemsSignal.multicast(RACReplaySubject.subject)
|
59
|
+
menu.memberCommand.connect
|
60
|
+
|
61
|
+
collection.arrangedObjects.each do |member|
|
62
|
+
menu.addMenuItemForMember member
|
63
|
+
end
|
64
|
+
|
65
|
+
collection.addObserver menu, forKeyPath: "arrangedObjects", options: (NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld), context: nil
|
66
|
+
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def initialize(label, &block)
|
71
|
+
@menuItems = {}
|
72
|
+
@label = label
|
73
|
+
@builder = block
|
74
|
+
@menu = NSMenu.alloc.init
|
75
|
+
end
|
76
|
+
|
77
|
+
def <<(item)
|
78
|
+
previousItemTag = @menuItems.keys.last || 0
|
79
|
+
item.tag = previousItemTag + 1
|
80
|
+
@menuItems[item.tag] = item
|
81
|
+
@menu.addItem item.menuItem
|
82
|
+
end
|
83
|
+
|
84
|
+
def addMenuItemForMember(member)
|
85
|
+
item = MenuItem.itemWithLabel(member.hash.to_s, title: member.send(memberTitleProperty))
|
86
|
+
item.representedObject = member
|
87
|
+
item.rac_stateSignal = memberCommand.signal.map ->((_,value)){
|
88
|
+
value.representedObject == item.representedObject
|
89
|
+
}
|
90
|
+
|
91
|
+
self << item
|
92
|
+
itemAddedSubject.sendNext(item)
|
93
|
+
|
94
|
+
end
|
95
|
+
|
96
|
+
def observeValueForKeyPath(keyPath, ofObject: object, change: change, context: context)
|
97
|
+
return unless keyPath == "arrangedObjects"
|
98
|
+
if itemArray.length < object.arrangedObjects.length
|
99
|
+
member = object.arrangedObjects.lastObject
|
100
|
+
addMenuItemForMember(member)
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
def subscribeToMembers(&block)
|
105
|
+
memberCommand.signal.subscribeNext(block)
|
106
|
+
end
|
107
|
+
|
108
|
+
def subscribe(itemLabel, &block)
|
109
|
+
self[itemLabel].subscribe(&block)
|
110
|
+
end
|
111
|
+
|
112
|
+
def createStatusItemWithTitle(title)
|
113
|
+
@needsStatusItem = true
|
114
|
+
@statusItemTitle = title
|
115
|
+
end
|
116
|
+
|
117
|
+
def createStatusItemWithIcon(image)
|
118
|
+
@needsStatusItem = true
|
119
|
+
@statusItemIcon = image
|
120
|
+
end
|
121
|
+
|
122
|
+
def createStatusItemWithIcon(image, viewClass: viewClass)
|
123
|
+
@needsStatusItem = true
|
124
|
+
@statusItemViewClass = viewClass
|
125
|
+
@statusItemIcon = image
|
126
|
+
end
|
127
|
+
|
128
|
+
def selectItem(label)
|
129
|
+
item = self[label]
|
130
|
+
item.command.execute(item)
|
131
|
+
end
|
132
|
+
|
133
|
+
def selectItemByMember(member)
|
134
|
+
item = @menuItems.values.find do |i|
|
135
|
+
i.representedObject == member
|
136
|
+
end
|
137
|
+
item.command.execute(item)
|
138
|
+
end
|
139
|
+
|
140
|
+
def [](labelOrTag)
|
141
|
+
if labelOrTag.is_a? Fixnum
|
142
|
+
@menuItems[labelOrTag]
|
143
|
+
else
|
144
|
+
@menuItems.values.find do |item|
|
145
|
+
item.label == labelOrTag
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
def needsStatusItem?
|
151
|
+
@needsStatusItem
|
152
|
+
end
|
153
|
+
|
154
|
+
def createStatusItem!
|
155
|
+
statusBar = NSStatusBar.systemStatusBar
|
156
|
+
@statusItem = statusBar.statusItemWithLength(NSSquareStatusItemLength)
|
157
|
+
@statusItem.highlightMode = true
|
158
|
+
|
159
|
+
@statusItem.menu = menu
|
160
|
+
|
161
|
+
if @statusItemViewClass
|
162
|
+
statusItemView = @statusItemViewClass.viewWithStatusItem(@statusItem)
|
163
|
+
@statusItem.menu.delegate = @statusItemView
|
164
|
+
@statusItem.view = statusItemView
|
165
|
+
end
|
166
|
+
|
167
|
+
@statusItem.title = @statusItemTitle
|
168
|
+
@statusItem.image = @statusItemIcon
|
169
|
+
|
170
|
+
@statusItem
|
171
|
+
end
|
172
|
+
|
173
|
+
end
|
174
|
+
end
|
@@ -0,0 +1,83 @@
|
|
1
|
+
module DrinkMenu
|
2
|
+
module MenuBuilder
|
3
|
+
|
4
|
+
class Context
|
5
|
+
def initialize(menu, menuItems={})
|
6
|
+
@menu = menu
|
7
|
+
@menuItems = menuItems
|
8
|
+
end
|
9
|
+
|
10
|
+
def ___
|
11
|
+
@menu << MenuItem.separatorItem
|
12
|
+
end
|
13
|
+
|
14
|
+
def method_missing(meth, *args)
|
15
|
+
if @menuItems.key?(meth)
|
16
|
+
@menu << @menuItems[meth]
|
17
|
+
else
|
18
|
+
super
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def <<(item)
|
24
|
+
@menuItems ||= {}
|
25
|
+
@menuItems[item.label] = item
|
26
|
+
end
|
27
|
+
|
28
|
+
def menuItem(label, title: title)
|
29
|
+
@menuItems ||= {}
|
30
|
+
@menuItems[label] = MenuItem.itemWithLabel label, title: title
|
31
|
+
end
|
32
|
+
|
33
|
+
def menuItem(label, title: title, submenu: submenu)
|
34
|
+
@menuItems ||= {}
|
35
|
+
@menuItems[label] = MenuItem.itemWithLabel label, title: title, submenu: @menus[submenu]
|
36
|
+
end
|
37
|
+
|
38
|
+
def menuItem(label, &block)
|
39
|
+
@menuItems ||= {}
|
40
|
+
@menuItems[label] = MenuItem.itemWithLabel label, &block
|
41
|
+
end
|
42
|
+
|
43
|
+
def statusBarMenu(label, title: title, &block)
|
44
|
+
@menus ||= {}
|
45
|
+
@menus[label] = Menu.statusMenuWithLabel label, title: title, &block
|
46
|
+
end
|
47
|
+
|
48
|
+
def statusBarMenu(label, icon: image, statusItemViewClass: statusItemViewClass, &block)
|
49
|
+
@menus ||= {}
|
50
|
+
@menus[label] = Menu.statusMenuWithLabel label, icon: image, statusItemViewClass: statusItemViewClass, &block
|
51
|
+
end
|
52
|
+
|
53
|
+
def statusBarMenu(label, icon: image, &block)
|
54
|
+
@menus ||= {}
|
55
|
+
@menus[label] = Menu.statusMenuWithLabel label, icon: image, &block
|
56
|
+
end
|
57
|
+
|
58
|
+
def menu(label, title: title, &block)
|
59
|
+
@menus ||= {}
|
60
|
+
@menus[label] = Menu.menuWithLabel label, title: title, &block
|
61
|
+
end
|
62
|
+
|
63
|
+
def menu(label, itemsFromCollection: collection, titleProperty: property)
|
64
|
+
@menus ||= {}
|
65
|
+
@menus[label] = Menu.menuWithLabel label, itemsFromCollection: collection, titleProperty: property
|
66
|
+
end
|
67
|
+
|
68
|
+
def [](label)
|
69
|
+
@menus[label]
|
70
|
+
end
|
71
|
+
|
72
|
+
def build!
|
73
|
+
@menus.values.each do |menu|
|
74
|
+
context = Context.new(menu, @menuItems.dup)
|
75
|
+
context.instance_eval(&menu.builder) if menu.builder
|
76
|
+
if menu.needsStatusItem?
|
77
|
+
menu.createStatusItem!
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
end
|
83
|
+
end
|
@@ -0,0 +1,92 @@
|
|
1
|
+
module DrinkMenu
|
2
|
+
class MenuItem
|
3
|
+
extend Forwardable
|
4
|
+
|
5
|
+
attr_accessor :label, :menuItem, :command, :canExecuteSignal
|
6
|
+
|
7
|
+
def_delegators :@menuItem, :isEnabled, :setEnabled, :enabled=,
|
8
|
+
:tag, :setTag, :tag=,
|
9
|
+
:image, :setImage, :image=,
|
10
|
+
:state, :setState, :state=,
|
11
|
+
:title, :setTitle, :title=,
|
12
|
+
:isHidden, :setHidden, :hidden=,
|
13
|
+
:isHiddenOrHasHiddenAncestor,
|
14
|
+
:target, :setTarget, :target=,
|
15
|
+
:action, :setAction, :action=,
|
16
|
+
:onStateImage, :setOnStateImage, :onStateImage=,
|
17
|
+
:offStateImage, :setOffStateImage, :offStateImage=,
|
18
|
+
:mixedStateImage, :setMixedStateImage, :mixedStateImage=,
|
19
|
+
:submenu, :setSubmenu, :submenu=,
|
20
|
+
:hasSubmenu,
|
21
|
+
:parentItem,
|
22
|
+
:isSeparatorItem,
|
23
|
+
:menu, :setMenu, :menu=,
|
24
|
+
:keyEquivalent, :setKeyEquivalent, :keyEquivalent=,
|
25
|
+
:keyEquivalentModifierMask, :setKeyEquivalentModifierMask, :keyEquivalentModifierMask=,
|
26
|
+
:isAlternate, :setAlternate, :alternate=,
|
27
|
+
:indentationLevel, :setIndentationLevel, :indentationLevel=,
|
28
|
+
:toolTip, :setToolTip, :toolTip=,
|
29
|
+
:representedObject, :setRepresentedObject, :representedObject=,
|
30
|
+
:view, :setView, :view=,
|
31
|
+
:rac_command, :rac_command=,
|
32
|
+
:rac_stateSignal, :rac_stateSignal=,
|
33
|
+
:isHighlighted
|
34
|
+
|
35
|
+
def self.itemWithLabel(label, title: title)
|
36
|
+
new.tap do |item|
|
37
|
+
item.label = label
|
38
|
+
item.title = title
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def self.itemWithLabel(label, title: title, submenu: submenu)
|
43
|
+
new.tap do |item|
|
44
|
+
item.label = label
|
45
|
+
item.title = title
|
46
|
+
item.submenu = submenu.menu
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def self.itemWithLabel(label, &block)
|
51
|
+
new.tap do |item|
|
52
|
+
item.label = label
|
53
|
+
block.call item
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def self.separatorItem
|
58
|
+
@@separatorId ||= 0
|
59
|
+
@@separatorId += 1
|
60
|
+
label = :"separator#{@@separatorId}"
|
61
|
+
new(NSMenuItem.separatorItem).tap do |item|
|
62
|
+
item.label = label
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
def initialize(menuItem=nil)
|
67
|
+
@menuItem = menuItem || NSMenuItem.alloc.init
|
68
|
+
end
|
69
|
+
|
70
|
+
def command=(command)
|
71
|
+
self.rac_command = command
|
72
|
+
end
|
73
|
+
|
74
|
+
def command
|
75
|
+
self.rac_command ||= if canExecuteSignal
|
76
|
+
RACCommand.commandWithCanExecuteSignal canExecuteSignal
|
77
|
+
else
|
78
|
+
RACCommand.command
|
79
|
+
end
|
80
|
+
|
81
|
+
self.rac_command
|
82
|
+
end
|
83
|
+
|
84
|
+
def subscribe(&block)
|
85
|
+
command.map(->(value){
|
86
|
+
[label, self]
|
87
|
+
}).subscribeNext(block)
|
88
|
+
end
|
89
|
+
|
90
|
+
end
|
91
|
+
|
92
|
+
end
|
@@ -0,0 +1,87 @@
|
|
1
|
+
class TestMenu; extend DrinkMenu::MenuBuilder; end
|
2
|
+
|
3
|
+
describe "sugar for creating menus" do
|
4
|
+
|
5
|
+
it "supports defining menu items" do
|
6
|
+
menuItem = TestMenu.menuItem :create_site, title: 'Create Site'
|
7
|
+
menuItem.label.should.equal :create_site
|
8
|
+
menuItem.title.should.equal 'Create Site'
|
9
|
+
end
|
10
|
+
|
11
|
+
it "supports defining menu items with a block" do
|
12
|
+
menuItem = TestMenu.menuItem :create_site do |item|
|
13
|
+
item.title = 'Progress Item'
|
14
|
+
end
|
15
|
+
|
16
|
+
menuItem.title.should.equal 'Progress Item'
|
17
|
+
end
|
18
|
+
|
19
|
+
it "builds gives access to menu instance" do
|
20
|
+
TestMenu.menuItem :create_site, title: 'Create Site'
|
21
|
+
TestMenu.menu :main_menu, title: "Main"
|
22
|
+
menu = TestMenu[:main_menu]
|
23
|
+
menu.should.be.an.instance_of DrinkMenu::Menu
|
24
|
+
end
|
25
|
+
|
26
|
+
it "allows creating a top-level menu" do
|
27
|
+
menu = TestMenu.menu :main_menu, title: "Blah"
|
28
|
+
menu.menu.should.be.an.instance_of NSMenu
|
29
|
+
menu.title.should.equal "Blah"
|
30
|
+
end
|
31
|
+
|
32
|
+
it "allows creating a status bar menu item" do
|
33
|
+
menu = TestMenu.statusBarMenu :main_menu, title: "B"
|
34
|
+
menu.statusItemTitle.should.equal "B"
|
35
|
+
|
36
|
+
image = NSImage.imageNamed "NSMenuRadio"
|
37
|
+
menu = TestMenu.statusBarMenu :main_menu, icon: image
|
38
|
+
menu.statusItemIcon.should.equal image
|
39
|
+
end
|
40
|
+
|
41
|
+
it "evaluates menu's block to add items to menu" do
|
42
|
+
item1 = TestMenu.menuItem :test_item1, title: "Blah"
|
43
|
+
item2 = TestMenu.menuItem :test_item2, title: "Blah"
|
44
|
+
|
45
|
+
TestMenu.menu :main_menu, title: "Main" do
|
46
|
+
test_item1
|
47
|
+
___
|
48
|
+
test_item2
|
49
|
+
end
|
50
|
+
|
51
|
+
TestMenu.build!
|
52
|
+
|
53
|
+
TestMenu[:main_menu][:test_item1].should.equal item1
|
54
|
+
TestMenu[:main_menu][:test_item2].should.equal item2
|
55
|
+
|
56
|
+
TestMenu[:main_menu][2].isSeparatorItem.should.be.true
|
57
|
+
end
|
58
|
+
|
59
|
+
describe "builder's context class" do
|
60
|
+
|
61
|
+
Context = DrinkMenu::MenuBuilder::Context
|
62
|
+
|
63
|
+
it "adds item to menu by calling method named after item's label" do
|
64
|
+
menu = DrinkMenu::Menu.new(:test)
|
65
|
+
item = DrinkMenu::MenuItem.itemWithLabel :test_item, title: "Blah"
|
66
|
+
context = Context.new(menu, {test_item: item})
|
67
|
+
|
68
|
+
menu[:test_item].should.be.nil
|
69
|
+
|
70
|
+
context.test_item
|
71
|
+
|
72
|
+
menu[:test_item].should.equal item
|
73
|
+
end
|
74
|
+
|
75
|
+
it "creates separators from ___" do
|
76
|
+
menu = DrinkMenu::Menu.new(:test)
|
77
|
+
|
78
|
+
menu[1].should.be.nil
|
79
|
+
|
80
|
+
Context.new(menu).send :"___"
|
81
|
+
|
82
|
+
puts menu.menuItems.inspect
|
83
|
+
menu[1].isSeparatorItem.should.be.true
|
84
|
+
end
|
85
|
+
|
86
|
+
end
|
87
|
+
end
|
@@ -0,0 +1,82 @@
|
|
1
|
+
describe "creating menu items" do
|
2
|
+
|
3
|
+
it "keeps the title and label" do
|
4
|
+
m = DrinkMenu::MenuItem.itemWithLabel :create_site, title: 'Create Site'
|
5
|
+
m.label.should.equal :create_site
|
6
|
+
m.title.should.equal 'Create Site'
|
7
|
+
end
|
8
|
+
|
9
|
+
it "allows passing a block for further configuration" do
|
10
|
+
m = DrinkMenu::MenuItem.itemWithLabel :export do |item|
|
11
|
+
item.title = 'Export...'
|
12
|
+
end
|
13
|
+
|
14
|
+
m.label.should.equal :export
|
15
|
+
m.title.should.equal 'Export...'
|
16
|
+
end
|
17
|
+
|
18
|
+
it "has an instance of NSMenuItem" do
|
19
|
+
m = DrinkMenu::MenuItem.itemWithLabel :create_site, title: 'Create Site'
|
20
|
+
m.menuItem.should.be.a.instance_of NSMenuItem
|
21
|
+
end
|
22
|
+
|
23
|
+
it "delegates NSMenuItem methods to NSMenuItem instance" do
|
24
|
+
m = DrinkMenu::MenuItem.itemWithLabel :create_site, title: 'Create Site'
|
25
|
+
m.enabled = false
|
26
|
+
m.tag = 1
|
27
|
+
m.image = NSImage.imageNamed "NSMenuRadio"
|
28
|
+
m.state = NSOnState
|
29
|
+
|
30
|
+
m.menuItem.title.should.equal m.title
|
31
|
+
m.menuItem.isEnabled.should.equal m.isEnabled
|
32
|
+
m.menuItem.tag.should.equal m.tag
|
33
|
+
m.menuItem.image.should.equal m.image
|
34
|
+
m.menuItem.state.should.equal m.state
|
35
|
+
end
|
36
|
+
|
37
|
+
it "exposes an RACCommand instance for subscribing" do
|
38
|
+
m = DrinkMenu::MenuItem.itemWithLabel :create_site, title: 'Create Site'
|
39
|
+
m.should.respond_to :subscribe
|
40
|
+
m.command.should.respond_to :execute
|
41
|
+
m.command.should.respond_to :subscribeNext
|
42
|
+
end
|
43
|
+
|
44
|
+
it "delegates subscribing to RACCommand" do
|
45
|
+
m = DrinkMenu::MenuItem.itemWithLabel :create_site, title: 'Create Site'
|
46
|
+
called = false
|
47
|
+
m.subscribe do |value|
|
48
|
+
value.should.equal [:create_site, m]
|
49
|
+
called = true
|
50
|
+
end
|
51
|
+
m.rac_command.execute(m.menuItem)
|
52
|
+
called.should.be.true
|
53
|
+
end
|
54
|
+
|
55
|
+
it "allows setting a canExecuteSignal on the RACCommand instance" do
|
56
|
+
m = DrinkMenu::MenuItem.itemWithLabel :create_site, title: 'Create Site'
|
57
|
+
signal = RACSubject.subject
|
58
|
+
m.canExecuteSignal = signal
|
59
|
+
m.subscribe do
|
60
|
+
end
|
61
|
+
signal.sendNext true
|
62
|
+
m.isEnabled.should.be.true
|
63
|
+
signal.sendNext false
|
64
|
+
m.isEnabled.should.be.false
|
65
|
+
end
|
66
|
+
|
67
|
+
it "allows creating separator items" do
|
68
|
+
item = DrinkMenu::MenuItem.separatorItem
|
69
|
+
item.isSeparatorItem.should.be.true
|
70
|
+
end
|
71
|
+
|
72
|
+
def separator_id(item)
|
73
|
+
item.label[/\d+$/].to_i
|
74
|
+
end
|
75
|
+
|
76
|
+
it "generates unique label for separators" do
|
77
|
+
item1 = DrinkMenu::MenuItem.separatorItem
|
78
|
+
item2 = DrinkMenu::MenuItem.separatorItem
|
79
|
+
separator_id(item2).should.equal separator_id(item1) + 1
|
80
|
+
end
|
81
|
+
|
82
|
+
end
|
data/spec/menu_spec.rb
ADDED
@@ -0,0 +1,195 @@
|
|
1
|
+
describe "creating menus" do
|
2
|
+
|
3
|
+
|
4
|
+
it "creates an NSMenu instance" do
|
5
|
+
menu = DrinkMenu::Menu.new(:test)
|
6
|
+
menu.menu.should.be.an.instance_of NSMenu
|
7
|
+
end
|
8
|
+
|
9
|
+
it "forwards title to NSMenu instance" do
|
10
|
+
menu = DrinkMenu::Menu.menuWithLabel(:test_menu, title: "title")
|
11
|
+
menu.title.should.equal "title"
|
12
|
+
menu.menu.title.should.equal menu.title
|
13
|
+
end
|
14
|
+
|
15
|
+
it "automatically adds NSMenuItem instance to NSMenu" do
|
16
|
+
menu = DrinkMenu::Menu.new(:test)
|
17
|
+
item = DrinkMenu::MenuItem.itemWithLabel :test_item, title: "Blah"
|
18
|
+
menu << item
|
19
|
+
|
20
|
+
menu.itemArray.should.include item.menuItem
|
21
|
+
|
22
|
+
end
|
23
|
+
|
24
|
+
it "sets a unique tag on each menu item as it's added" do
|
25
|
+
menu = DrinkMenu::Menu.new(:test)
|
26
|
+
item1 = DrinkMenu::MenuItem.itemWithLabel :test_item1, title: "Blah"
|
27
|
+
item2 = DrinkMenu::MenuItem.itemWithLabel :test_item2, title: "Diddy"
|
28
|
+
|
29
|
+
menu << item1
|
30
|
+
menu << item2
|
31
|
+
|
32
|
+
item1.menuItem.tag.should.equal 1
|
33
|
+
item2.menuItem.tag.should.equal 2
|
34
|
+
|
35
|
+
end
|
36
|
+
|
37
|
+
describe "creating menu from collection of objects" do
|
38
|
+
|
39
|
+
class Person
|
40
|
+
attr_reader :firstName, :lastName
|
41
|
+
|
42
|
+
def initialize(firstName, lastName)
|
43
|
+
@firstName = firstName
|
44
|
+
@lastName = lastName
|
45
|
+
end
|
46
|
+
|
47
|
+
def fullName
|
48
|
+
"#{@firstName} #{@lastName}"
|
49
|
+
end
|
50
|
+
|
51
|
+
def inspect
|
52
|
+
"<#Person:#{hash} fullName=\"#{fullName}\">"
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
before do
|
57
|
+
@people = [Person.new("Joe", "Fiorini"), Person.new("Josh", "Walsh")]
|
58
|
+
@controller = NSArrayController.alloc.initWithContent(@people)
|
59
|
+
@menu = DrinkMenu::Menu.menuWithLabel :test, itemsFromCollection: @controller, titleProperty: :fullName
|
60
|
+
end
|
61
|
+
|
62
|
+
it "automatically creates menu items from each item" do
|
63
|
+
@menu.itemArray.length.should.equal 2
|
64
|
+
end
|
65
|
+
|
66
|
+
it "sets menu item titles using the property specified in titleProperty" do
|
67
|
+
@menu.itemArray.each_with_index do |item, idx|
|
68
|
+
item.title.should.equal @people[idx].fullName
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
it "supports arrays or array controllers"
|
73
|
+
|
74
|
+
it "sets menu item representedObject using the member" do
|
75
|
+
@menu.itemArray.each_with_index do |item, idx|
|
76
|
+
item.representedObject.should.equal @people[idx]
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
it "delegates menu command to menu items" do
|
81
|
+
handled = false
|
82
|
+
|
83
|
+
@menu.subscribeToMembers do |(label, _)|
|
84
|
+
label.should.equal :test
|
85
|
+
handled = true
|
86
|
+
end
|
87
|
+
|
88
|
+
@menu[1].command.execute @menu[1].menuItem
|
89
|
+
handled.should.be.true
|
90
|
+
end
|
91
|
+
|
92
|
+
it "automatically rebuilds the menu to reflect new members" do
|
93
|
+
@menu.itemArray.length.should.equal 2
|
94
|
+
newPerson = Person.new("Michael", "Bluth")
|
95
|
+
@controller.addObject(newPerson)
|
96
|
+
@menu.itemArray.length.should.equal 3
|
97
|
+
@menu.itemArray.should.any(&->(item){
|
98
|
+
item.representedObject == newPerson
|
99
|
+
})
|
100
|
+
end
|
101
|
+
|
102
|
+
it "sets up a subscriber for the new item" do
|
103
|
+
handled = false
|
104
|
+
newPerson = Person.new("Lucille", "Two")
|
105
|
+
@controller.addObject(newPerson)
|
106
|
+
@menu.subscribe newPerson.hash.to_s do |item|
|
107
|
+
handled = true
|
108
|
+
end
|
109
|
+
newItem = @menu[newPerson.hash.to_s]
|
110
|
+
newItem.command.execute(newItem)
|
111
|
+
handled.should.be.true
|
112
|
+
end
|
113
|
+
|
114
|
+
it "allows selecting an item by its member object" do
|
115
|
+
handled = false
|
116
|
+
@menu.subscribeToMembers do |(_,item)|
|
117
|
+
handled = true
|
118
|
+
item.representedObject.should.equal @people[0]
|
119
|
+
end
|
120
|
+
|
121
|
+
@menu.selectItemByMember @people[0]
|
122
|
+
handled.should.be.true
|
123
|
+
end
|
124
|
+
|
125
|
+
end
|
126
|
+
|
127
|
+
it "allows looking up menu items by label" do
|
128
|
+
menu = DrinkMenu::Menu.new(:test)
|
129
|
+
item = DrinkMenu::MenuItem.itemWithLabel :test_item, title: "Blah"
|
130
|
+
|
131
|
+
menu << item
|
132
|
+
|
133
|
+
menu[item.label].should.equal item
|
134
|
+
end
|
135
|
+
|
136
|
+
it "allows selecting an item by its label" do
|
137
|
+
handled = false
|
138
|
+
menu = DrinkMenu::Menu.new(:test)
|
139
|
+
item = DrinkMenu::MenuItem.itemWithLabel :test_item, title: "Blah"
|
140
|
+
|
141
|
+
menu << item
|
142
|
+
|
143
|
+
menu.subscribe :test_item do |_|
|
144
|
+
handled = true
|
145
|
+
end
|
146
|
+
|
147
|
+
menu.selectItem :test_item
|
148
|
+
|
149
|
+
handled.should.be.true
|
150
|
+
end
|
151
|
+
|
152
|
+
it "allows looking up menu items by tag" do
|
153
|
+
menu = DrinkMenu::Menu.new(:test)
|
154
|
+
item = DrinkMenu::MenuItem.itemWithLabel :test_item, title: "Blah"
|
155
|
+
|
156
|
+
menu << item
|
157
|
+
|
158
|
+
menu[1].should.equal item
|
159
|
+
end
|
160
|
+
|
161
|
+
it "supports wrapping menu in a status bar item" do
|
162
|
+
menu = DrinkMenu::Menu.statusMenuWithLabel :test, title: "S"
|
163
|
+
menu.createStatusItem!
|
164
|
+
menu.statusItem.should.be.an.instance_of NSStatusItem
|
165
|
+
menu.statusItem.highlightMode.should.be.true
|
166
|
+
menu.statusItem.title.should.equal "S"
|
167
|
+
menu.statusItem.menu.should.equal menu.menu
|
168
|
+
end
|
169
|
+
|
170
|
+
it "supports icon for status bar item" do
|
171
|
+
image = NSImage.imageNamed "NSMenuRadio"
|
172
|
+
menu = DrinkMenu::Menu.statusMenuWithLabel :test, icon: image
|
173
|
+
menu.createStatusItem!
|
174
|
+
menu.statusItem.image.should.equal image
|
175
|
+
end
|
176
|
+
|
177
|
+
it "supports subscribing to menu item commands" do
|
178
|
+
called = false
|
179
|
+
|
180
|
+
item = DrinkMenu::MenuItem.itemWithLabel :test_item, title: "Blah"
|
181
|
+
menu = DrinkMenu::Menu.new(:test)
|
182
|
+
|
183
|
+
menu << item
|
184
|
+
|
185
|
+
menu.subscribe :test_item do |(label, value)|
|
186
|
+
label.should.equal item.label
|
187
|
+
value.should.equal item
|
188
|
+
called = true
|
189
|
+
end
|
190
|
+
|
191
|
+
item.command.execute(item)
|
192
|
+
called.should.be.true
|
193
|
+
end
|
194
|
+
|
195
|
+
end
|
metadata
ADDED
@@ -0,0 +1,122 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: drink-menu
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Joe Fiorini
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2013-07-10 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: bundler
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ~>
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '1.3'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ~>
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '1.3'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rake
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ~>
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: 10.0.0
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ~>
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: 10.0.0
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: motion-cocoapods
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - '>='
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - '>='
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: cocoapods
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - '='
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: 0.20.2
|
62
|
+
type: :runtime
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - '='
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: 0.20.2
|
69
|
+
description: An easy way to define menu items and visually lay out menus for your
|
70
|
+
OSX apps. Uses ReactiveCocoa to provide a nice syntax for responding to menu interactions.
|
71
|
+
Also provides live-binding to collections, which makes keeping menus up-to-date
|
72
|
+
a breeze!
|
73
|
+
email:
|
74
|
+
- joe@joefiorini.com
|
75
|
+
executables: []
|
76
|
+
extensions: []
|
77
|
+
extra_rdoc_files: []
|
78
|
+
files:
|
79
|
+
- .gitignore
|
80
|
+
- Gemfile
|
81
|
+
- LICENSE.txt
|
82
|
+
- README.md
|
83
|
+
- Rakefile
|
84
|
+
- drink-menu.gemspec
|
85
|
+
- lib/drink-menu.rb
|
86
|
+
- lib/drink-menu/ext/forwardable.rb
|
87
|
+
- lib/drink-menu/ext/ns_menu_item.rb
|
88
|
+
- lib/drink-menu/menu.rb
|
89
|
+
- lib/drink-menu/menu_builder.rb
|
90
|
+
- lib/drink-menu/menu_item.rb
|
91
|
+
- lib/drink-menu/version.rb
|
92
|
+
- spec/menu_builder_spec.rb
|
93
|
+
- spec/menu_item_spec.rb
|
94
|
+
- spec/menu_spec.rb
|
95
|
+
homepage: https://github.com/joefiorini/drink-menu
|
96
|
+
licenses:
|
97
|
+
- MIT
|
98
|
+
metadata: {}
|
99
|
+
post_install_message:
|
100
|
+
rdoc_options: []
|
101
|
+
require_paths:
|
102
|
+
- lib
|
103
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
104
|
+
requirements:
|
105
|
+
- - '>='
|
106
|
+
- !ruby/object:Gem::Version
|
107
|
+
version: '0'
|
108
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
109
|
+
requirements:
|
110
|
+
- - '>='
|
111
|
+
- !ruby/object:Gem::Version
|
112
|
+
version: '0'
|
113
|
+
requirements: []
|
114
|
+
rubyforge_project:
|
115
|
+
rubygems_version: 2.0.2
|
116
|
+
signing_key:
|
117
|
+
specification_version: 4
|
118
|
+
summary: OSX menus - the ruby way
|
119
|
+
test_files:
|
120
|
+
- spec/menu_builder_spec.rb
|
121
|
+
- spec/menu_item_spec.rb
|
122
|
+
- spec/menu_spec.rb
|