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