Skip to content

Commit c7cb84a

Browse files
committed
Allow hiding the dock icon
Experimental
1 parent 3e18df2 commit c7cb84a

File tree

7 files changed

+112
-80
lines changed

7 files changed

+112
-80
lines changed

README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,11 @@ To set the radio station, paste the URL of the radio station in the
99
Soundtrack window. This is the HTTP link to the shoutcast playlist
1010
file, ending in ".pls" or ".m3u".
1111

12+
## Advanced
13+
14+
* If you choose to hide the dock icon, you can show the Soundtrack
15+
window by right or control clicking the status bar icon.
16+
1217
## Obtaining Soundtrack
1318

1419
### Building from Source

Soundtrack-macOS/AppDelegate.swift

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8,17 +8,8 @@ import Cocoa
88

99
@NSApplicationMain
1010
class AppDelegate: NSObject, NSApplicationDelegate {
11-
func applicationWillFinishLaunching(_ notification: Notification) {
12-
NSUserDefaultsController.shared.defaults.register(defaults: [
13-
"showNotifications": true,
14-
"showStatusBarIcon": true
15-
])
16-
}
17-
1811
func applicationShouldHandleReopen(_ sender: NSApplication, hasVisibleWindows: Bool) -> Bool {
19-
if !hasVisibleWindows {
20-
sender.windows.first?.makeKeyAndOrderFront(self)
21-
}
12+
sender.windows.first?.makeKeyAndOrderFront(self)
2213
return true
2314
}
2415
}

Soundtrack-macOS/Base.lproj/Main.storyboard

Lines changed: 20 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -88,13 +88,31 @@
8888
<menuItem title="Song Change Notifications" keyEquivalent="n" id="spa-G5-Q3m">
8989
<modifierMask key="keyEquivalentModifierMask" control="YES" command="YES"/>
9090
<connections>
91-
<binding destination="Bn8-jN-r8Z" name="value" keyPath="values.showNotifications" id="flV-lI-d30"/>
91+
<binding destination="Bn8-jN-r8Z" name="value" keyPath="values.hideNotifications" id="nIH-jg-cCL">
92+
<dictionary key="options">
93+
<string key="NSValueTransformerName">NSNegateBoolean</string>
94+
</dictionary>
95+
</binding>
9296
</connections>
9397
</menuItem>
9498
<menuItem title="Status Bar Icon" id="gBr-Cy-gE1">
9599
<modifierMask key="keyEquivalentModifierMask"/>
96100
<connections>
97-
<binding destination="Bn8-jN-r8Z" name="value" keyPath="values.showStatusBarIcon" id="vdZ-P2-fJA"/>
101+
<binding destination="Bn8-jN-r8Z" name="value" keyPath="values.hideStatusBarIcon" id="0my-ky-0Uc">
102+
<dictionary key="options">
103+
<string key="NSValueTransformerName">NSNegateBoolean</string>
104+
</dictionary>
105+
</binding>
106+
</connections>
107+
</menuItem>
108+
<menuItem title="Dock Icon" id="CYO-yh-o1i">
109+
<modifierMask key="keyEquivalentModifierMask"/>
110+
<connections>
111+
<binding destination="Bn8-jN-r8Z" name="value" keyPath="values.hideDockIcon" id="uJL-8e-e2w">
112+
<dictionary key="options">
113+
<string key="NSValueTransformerName">NSNegateBoolean</string>
114+
</dictionary>
115+
</binding>
98116
</connections>
99117
</menuItem>
100118
<menuItem isSeparatorItem="YES" id="rNn-zP-9Py"/>
@@ -132,18 +150,6 @@
132150
</items>
133151
</menu>
134152
</menuItem>
135-
<menuItem title="Help" id="wpr-3q-Mcd">
136-
<modifierMask key="keyEquivalentModifierMask"/>
137-
<menu key="submenu" title="Help" systemMenu="help" id="F2S-fz-NVQ">
138-
<items>
139-
<menuItem title="Soundtrack Help" keyEquivalent="?" id="FKE-Sm-Kum">
140-
<connections>
141-
<action selector="showHelp:" target="Ady-hI-5gd" id="y7X-2Q-9no"/>
142-
</connections>
143-
</menuItem>
144-
</items>
145-
</menu>
146-
</menuItem>
147153
</items>
148154
</menu>
149155
<connections>

Soundtrack-macOS/ViewController.swift

Lines changed: 50 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -22,14 +22,13 @@ class ViewController: NSViewController, NSUserInterfaceValidations, AudioControl
2222
private var lastTitle: String?
2323
private var togglePlaybackMenuItemTitle: String?
2424
private var togglePlaybackMenuItemIsEnabled: Bool?
25-
private var observationContext = 0
2625

2726
override func viewDidLoad() {
2827
super.viewDidLoad()
2928

3029
observeUserDefaultsController()
31-
3230
updateStatusBarItem()
31+
updateDockIcon()
3332

3433
indicateUnavailability()
3534

@@ -54,8 +53,6 @@ class ViewController: NSViewController, NSUserInterfaceValidations, AudioControl
5453
return AudioController(url: url, delegate: self, delegateQueue: DispatchQueue.main, makeSession: makeSession)
5554
}
5655

57-
// MARK: -
58-
5956
@IBAction func play(_ sender: NSButton) {
6057
log.info("User pressed play")
6158
prepareForPlaybackStart()
@@ -78,20 +75,11 @@ class ViewController: NSViewController, NSUserInterfaceValidations, AudioControl
7875
toggle()
7976
}
8077

81-
@objc func toggleStatus(_ sender: NSStatusBarButton) {
82-
log.info("User clicked status bar toggle")
83-
if togglePlaybackMenuItemIsEnabled == true {
84-
toggle()
85-
}
86-
}
87-
8878
private func toggle() {
8979
isPlaying ? prepareForPlaybackStop() : prepareForPlaybackStart()
9080
audioController?.playPause()
9181
}
9282

93-
// MARK: -
94-
9583
private func indicateUnavailability() {
9684
isPlaying = false
9785

@@ -223,8 +211,6 @@ class ViewController: NSViewController, NSUserInterfaceValidations, AudioControl
223211
fadeIn(titleStackView)
224212
}
225213

226-
// MARK: -
227-
228214
private func fadeOut(_ view: NSView, duration: TimeInterval = 2, then: (() -> Void)? = nil) {
229215
NSAnimationContext.runAnimationGroup({ context in
230216
context.duration = duration
@@ -239,8 +225,6 @@ class ViewController: NSViewController, NSUserInterfaceValidations, AudioControl
239225
}, completionHandler: then)
240226
}
241227

242-
// MARK: Music Menu
243-
244228
private func menuTitlePlay() -> String {
245229
return NSLocalizedString("Play", comment: "Menu Item - Music > Play")
246230
}
@@ -260,20 +244,29 @@ class ViewController: NSViewController, NSUserInterfaceValidations, AudioControl
260244
return true
261245
}
262246

263-
// MARK: Status Bar Button
264-
265247
private func makeStatusButtonItem() -> NSStatusItem {
266-
let statusBar = NSStatusBar.system
267-
268-
let item = statusBar.statusItem(withLength: NSStatusItem.squareLength)
269-
248+
let item = NSStatusBar.system.statusItem(withLength: NSStatusItem.squareLength)
270249
item.image = #imageLiteral(resourceName: "StatusBarButton")
271-
item.action = #selector(toggleStatus(_:))
250+
item.action = #selector(statusBarEvent)
272251
item.target = self
273-
252+
item.button?.sendAction(on: [.leftMouseUp, .rightMouseUp])
274253
return item
275254
}
276255

256+
@objc private func statusBarEvent(_ sender: NSStatusBarButton) {
257+
if let event = NSApp.currentEvent {
258+
if event.modifierFlags.contains(.control) || event.type == .rightMouseUp {
259+
configuration.hideDockIcon = false
260+
view.window?.makeKeyAndOrderFront(self)
261+
view.window?.makeMain()
262+
} else {
263+
if togglePlaybackMenuItemIsEnabled == true {
264+
toggle()
265+
}
266+
}
267+
}
268+
}
269+
277270
private func removeStatusBarItem() {
278271
if let item = statusItem {
279272
item.statusBar?.removeStatusItem(item)
@@ -299,15 +292,15 @@ class ViewController: NSViewController, NSUserInterfaceValidations, AudioControl
299292
}
300293

301294
private func updateStatusBarItem() {
302-
if showStatusBarIcon {
295+
if configuration.hideStatusBarIcon {
296+
if statusItem != nil {
297+
removeStatusBarItem()
298+
}
299+
} else {
303300
if statusItem == nil {
304301
statusItem = makeStatusButtonItem()
305302
gleanStatusItemState()
306303
}
307-
} else {
308-
if statusItem != nil {
309-
removeStatusBarItem()
310-
}
311304
}
312305
}
313306

@@ -320,43 +313,52 @@ class ViewController: NSViewController, NSUserInterfaceValidations, AudioControl
320313
statusItem?.toolTip = lastTitle
321314
}
322315

323-
// MARK: View Menu
316+
private func updateDockIcon() {
317+
if configuration.hideDockIcon {
318+
NSApp?.setActivationPolicy(.accessory)
319+
} else {
320+
NSApp?.setActivationPolicy(.regular)
321+
}
322+
}
324323

325-
let showStatusBarIconKVOPath = "values.showStatusBarIcon"
324+
private var userDefaultsController = NSUserDefaultsController.shared
325+
// Apparently, cannot use Swift 4 KVO with NSUserDefaultsController,
326+
// so do it the old way.
327+
private var observationContext = 0
328+
private let hideStatusBarIconKVOPath = "values.hideStatusBarIcon"
329+
private let hideDockIconKVOPath = "values.hideDockIcon"
326330

327331
private func observeUserDefaultsController() {
328-
NSUserDefaultsController.shared.addObserver(self, forKeyPath: showStatusBarIconKVOPath, options: [], context: &observationContext)
332+
userDefaultsController.addObserver(self, forKeyPath: hideStatusBarIconKVOPath, options: [], context: &observationContext)
333+
userDefaultsController.addObserver(self, forKeyPath: hideDockIconKVOPath, options: [], context: &observationContext)
329334
}
330335

331336
private func unobserveUserDefaultsController() {
332-
NSUserDefaultsController.shared.removeObserver(self, forKeyPath: showStatusBarIconKVOPath, context: &observationContext)
337+
userDefaultsController.removeObserver(self, forKeyPath: hideStatusBarIconKVOPath, context: &observationContext)
338+
userDefaultsController.removeObserver(self, forKeyPath: hideDockIconKVOPath, context: &observationContext)
333339
}
334340

335341
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
336342
if context == &observationContext {
337-
if keyPath == showStatusBarIconKVOPath {
343+
if keyPath == hideStatusBarIconKVOPath {
338344
updateStatusBarItem()
345+
} else if keyPath == hideDockIconKVOPath {
346+
updateDockIcon()
339347
}
340348
} else {
341349
super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context)
342350
}
343351
}
344352

345-
var showNotifications: Bool {
346-
return NSUserDefaultsController.shared.defaults.bool(forKey: "showNotifications")
347-
}
348-
349-
var showStatusBarIcon: Bool {
350-
return NSUserDefaultsController.shared.defaults.bool(forKey: "showStatusBarIcon")
351-
}
352-
353353
private func maybeShowNotification(_ titleComponents: TitleComponents) {
354-
if showNotifications {
355-
let notification = NSUserNotification()
356-
notification.title = titleComponents.song
357-
notification.subtitle = titleComponents.artist
358-
NSUserNotificationCenter.default.deliver(notification)
354+
if configuration.hideNotifications {
355+
return
359356
}
357+
358+
let notification = NSUserNotification()
359+
notification.title = titleComponents.song
360+
notification.subtitle = titleComponents.artist
361+
NSUserNotificationCenter.default.deliver(notification)
360362
}
361363

362364
@objc func paste(_ sender: Any) {

Soundtrack-macOS/WindowController.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@ class WindowController: NSWindowController, NSWindowDelegate {
1010
override func windowDidLoad() {
1111
super.windowDidLoad()
1212

13-
window!.titlebarAppearsTransparent = true
14-
window!.isMovableByWindowBackground = true
13+
window?.titlebarAppearsTransparent = true
14+
window?.isMovableByWindowBackground = true
1515
}
1616

1717
func windowWillUseStandardFrame(_ window: NSWindow, defaultFrame newFrame: NSRect) -> NSRect {

Soundtrack/Configuration.swift

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,16 +8,47 @@ import Foundation
88

99
class Configuration {
1010
static let shared = Configuration()
11+
private let userDefaults = UserDefaults.standard
1112

1213
private(set) var shoutcastURL: URL? {
1314
get {
14-
if let string = UserDefaults.standard.string(forKey: "shoutcastURL") {
15+
if let string = userDefaults.string(forKey: "shoutcastURL") {
1516
return URL(string: string)
1617
}
1718
return nil
1819
}
1920
set {
20-
UserDefaults.standard.set(newValue?.absoluteString, forKey: "shoutcastURL")
21+
userDefaults.set(newValue?.absoluteString, forKey: "shoutcastURL")
22+
}
23+
}
24+
25+
var hideNotifications: Bool {
26+
get {
27+
return userDefaults.bool(forKey: "hideNotifications")
28+
}
29+
set {
30+
userDefaults.set(newValue, forKey: "hideNotifications")
31+
}
32+
}
33+
34+
var hideStatusBarIcon: Bool {
35+
get {
36+
return userDefaults.bool(forKey: "hideStatusBarIcon")
37+
}
38+
set {
39+
return userDefaults.set(newValue, forKey: "hideStatusBarIcon")
40+
}
41+
}
42+
43+
var hideDockIcon: Bool {
44+
get {
45+
if hideStatusBarIcon {
46+
return true
47+
}
48+
return userDefaults.bool(forKey: "hideDockIcon")
49+
}
50+
set {
51+
return userDefaults.set(newValue, forKey: "hideDockIcon")
2152
}
2253
}
2354

Soundtrack/NSObjectExtensions.swift

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,12 @@
77
import Foundation
88

99
extension NSObject {
10-
1110
/// Invoke `selector` when any object posts a notification with the given
1211
/// name to the default notification center.
1312
///
1413
/// If your app targets iOS 9.0 and later or macOS 10.11 and later,
1514
/// you don't need to unregister an observer in its deallocation method.
16-
17-
func observe(_ name: Notification.Name, with selector: Selector) {
15+
func observeNotification(_ name: Notification.Name, with selector: Selector) {
1816
NotificationCenter.default.addObserver(self, selector: selector, name: name, object: nil)
1917
}
20-
2118
}

0 commit comments

Comments
 (0)