-
Notifications
You must be signed in to change notification settings - Fork 34
/
AppDelegate+Menu.swift
220 lines (188 loc) · 9.33 KB
/
AppDelegate+Menu.swift
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
//
// AppDelegate+Menu.swift
// PixelPicker
//
/**
* This file is responsible for managing PixelPicker's dropdown menu when
* the user clicks on the status bar item.
*/
import LaunchAtLogin
// The modifiers available to use to toggle "concentrationMode".
let concentrationModifiers: [(String, NSEvent.ModifierFlags)] = [
("fn Function", .function),
("⌘ Command", .command),
("⌃ Control", .control),
("⌥ Option", .option),
("⇧ Shift ", .shift)
]
extension AppDelegate: NSMenuDelegate {
// Unregister the activating shortcut when the menu is opened/closed so it can't be called when
// setting a new shortcut. Also start a run loop observer so we know when the modifierFlags have
// changed (used to dynamically update the menu).
func menuWillOpen(_ menu: NSMenu) {
unregisterActivatingShortcut()
if runLoopObserver == nil {
let activites = CFRunLoopActivity.beforeWaiting.rawValue
runLoopObserver = CFRunLoopObserverCreateWithHandler(nil, activites, true, 0, { [unowned self] (_, _) in
self.updateMenuItems()
})
CFRunLoopAddObserver(CFRunLoopGetCurrent(), runLoopObserver, CFRunLoopMode.commonModes)
}
}
// Re-register the activating shortcut, and remove the run loop observer.
func menuDidClose(_ menu: NSMenu) {
registerActivatingShortcut()
if (runLoopObserver != nil) {
CFRunLoopObserverInvalidate(runLoopObserver)
runLoopObserver = nil
}
}
// Updates the titles of the recently picked colors - if the `option` key is pressed, then
// the colors will be in the format they were *when* they were picked, otherwise they'll be
// in the currently chosen format.
private func updateMenuItems() {
// Update recent picks list with correct titles.
let alternate = NSEvent.modifierFlags.contains(.option)
for item in contextMenu.items {
if let pickedColor = item.representedObject as? PPPickedColor {
item.title = alternate
? pickedColor.asString
: PPState.shared.chosenFormat.asString(withColor: pickedColor.color)
}
}
}
// TODO: look into only updating the menu rather than rebuilding it each time.
// Might not be worth it - it doesn't seem expensive to build it every time it's opened...
func rebuildContextMenu() {
contextMenu.removeAllItems()
let pickItem = contextMenu.addItem(withTitle: "Pick a pixel!", action: #selector(showPicker), keyEquivalent: "")
pickItem.image = ICON
buildRecentPicks()
contextMenu.addItem(.separator())
buildColorFormatsMenu()
buildConcentrationMenu()
buildFloatPrecisionSlider()
buildShortcutMenuItem()
buildLaunchAtLoginItem()
contextMenu.addItem(.separator())
contextMenu.addItem(withTitle: "About", action: #selector(showAboutPanel), keyEquivalent: "")
contextMenu.addItem(withTitle: "Quit \(APP_NAME)", action: #selector(quitApplication), keyEquivalent: "")
}
private func buildLaunchAtLoginItem() {
let item = contextMenu.addItem(withTitle: "Launch \(APP_NAME) at Login", action: #selector(launchAtLogin(_:)), keyEquivalent: "")
item.state = LaunchAtLogin.isEnabled ? .on : .off
}
@objc private func launchAtLogin(_ sender: NSMenuItem) {
LaunchAtLogin.isEnabled = !LaunchAtLogin.isEnabled
}
// Show the user's recent picks in the menu.
private func buildRecentPicks() {
if PPState.shared.recentPicks.count > 0 {
contextMenu.addItem(.separator())
contextMenu.addItem(withTitle: "Recently Picked", action: nil, keyEquivalent: "")
let format = PPState.shared.chosenFormat
for pickedColor in PPState.shared.recentPicks {
let item = contextMenu.addItem(withTitle: format.asString(withColor: pickedColor.color), action: #selector(copyRecentPick(_:)), keyEquivalent: "")
item.representedObject = pickedColor
item.image = circleImage(withSize: 12, color: pickedColor.color)
}
}
}
// Copies the recently picked color (associated with the menu item) to the clipboard.
// If the `option` key is pressed, then it copies the color in the same format it was
// when it was picked (otherwise, it copies it in the currently chosen format).
@objc private func copyRecentPick(_ sender: NSMenuItem) {
if let pickedColor = sender.representedObject as? PPPickedColor {
let value = NSEvent.modifierFlags.contains(.option)
? pickedColor.asString
: PPState.shared.chosenFormat.asString(withColor: pickedColor.color)
copyToPasteboard(stringValue: value)
}
}
// Simply creates a circle NSImage with the given size and color.
private func circleImage(withSize size: CGFloat, color: NSColor) -> NSImage {
let image = NSImage(size: NSSize(width: size, height: size))
image.lockFocus()
color.set()
NSBezierPath(roundedRect: NSMakeRect(0, 0, size, size), xRadius: size, yRadius: size).fill()
image.unlockFocus()
return image
}
// A slider to change the float precision.
private func buildFloatPrecisionSlider() {
contextMenu.addItem(withTitle: "Float Precision (\(PPState.shared.floatPrecision))", action: nil, keyEquivalent: "")
let value = Double(PPState.shared.floatPrecision)
let maxValue = Double(PPState.maxFloatPrecision)
let slider = NSSlider(value: value, minValue: 1, maxValue: maxValue, target: self, action: #selector(sliderUpdate(_:)))
slider.allowsTickMarkValuesOnly = true
slider.autoresizingMask = .width
slider.tickMarkPosition = .above
slider.numberOfTickMarks = PPState.maxFloatPrecision
slider.frame = slider.frame.insetBy(dx: 20, dy: 0)
let item = contextMenu.addItem(withTitle: "Slider", action: nil, keyEquivalent: "")
item.view = NSView(frame: NSMakeRect(0, 0, 100, 20))
item.view!.autoresizingMask = .width
item.view!.addSubview(slider)
}
// Called when the slider is updated.
@objc private func sliderUpdate(_ sender: NSSlider) {
let newValue = UInt(sender.intValue)
// Update slider title.
if let item = contextMenu.item(withTitle: "Float Precision (\(PPState.shared.floatPrecision))") {
item.title = "Float Precision (\(newValue))"
}
// Update state.
PPState.shared.floatPrecision = newValue
// Update recent picks list with new precision.
updateMenuItems()
}
// Build a submenu with each case in the PPColor enum.
// TODO: with Swift 4.2, we shouldn't need to resort to the hacky "iterateEnum" approach.
private func buildColorFormatsMenu() {
let submenu = NSMenu()
for format in iterateEnum(PPColor.self) {
let formatItem = submenu.addItem(withTitle: format.rawValue, action: #selector(selectFormat(_:)), keyEquivalent: "")
formatItem.representedObject = format
if PPState.shared.chosenFormat == format { formatItem.state = .on }
}
let item = contextMenu.addItem(withTitle: "Color Format", action: nil, keyEquivalent: "")
item.submenu = submenu
}
// Set the selected format as the default.
@objc private func selectFormat(_ sender: NSMenuItem) {
if let format = sender.representedObject as? PPColor {
PPState.shared.chosenFormat = format
sender.menu?.items.forEach({ $0.state = .off })
sender.state = .on
}
}
// Builds and adds the concentration modifier submenu.
private func buildConcentrationMenu() {
let submenu = NSMenu()
for (name, modifier) in concentrationModifiers {
let modifierItem = submenu.addItem(withTitle: name, action: #selector(selectModifier(_:)), keyEquivalent: "")
modifierItem.representedObject = modifier
if PPState.shared.concentrationModeModifier == modifier { modifierItem.state = .on }
}
let item = contextMenu.addItem(withTitle: "Concentration Modifier", action: nil, keyEquivalent: "")
item.submenu = submenu
}
// Set the chosen modifier to toggle "concentrationMode".
@objc private func selectModifier(_ sender: NSMenuItem) {
if let modifier = sender.representedObject as? NSEvent.ModifierFlags {
PPState.shared.concentrationModeModifier = modifier
sender.menu?.items.forEach({ $0.state = .off })
sender.state = .on
}
}
// Builds and adds the MASShortcutView to be used in the menu.
// Uses a custom view to handle events correctly (since it's inside a NSMenu).
private func buildShortcutMenuItem() {
contextMenu.addItem(withTitle: "Picker Shortcut", action: nil, keyEquivalent: "")
let shortcutView = MASShortcutView()
shortcutView.shortcutValue = PPState.shared.activatingShortcut
shortcutView.shortcutValueChange = { PPState.shared.activatingShortcut = $0?.shortcutValue }
let item = contextMenu.addItem(withTitle: "Shortcut", action: nil, keyEquivalent: "")
item.view = PPMenuShortcutView(shortcut: shortcutView)
}
}