drink-menu 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -0,0 +1,19 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
18
+ build
19
+ vendor
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in drink-menu.gemspec
4
+ gemspec
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
@@ -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,3 @@
1
+ module DrinkMenu
2
+ VERSION = "0.1.0"
3
+ 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