macOS: Different status bar icon looks for different states

- Looks dimmed when no tunnel is active
- Looks normal when a tunnel is active
- Animates when a tunnel is activating

Signed-off-by: Roopesh Chander <roop@roopc.net>
This commit is contained in:
Roopesh Chander
2019-01-16 01:00:42 +05:30
parent 26ea353933
commit e29cf19fdd
28 changed files with 209 additions and 16 deletions
+11 -12
View File
@@ -6,7 +6,8 @@ import Cocoa
@NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate {
var statusItem: NSStatusItem?
var statusItemController: StatusItemController?
var currentTunnelStatusObserver: AnyObject?
func applicationDidFinishLaunching(_ aNotification: Notification) {
Logger.configureGlobal(withFilePath: FileManager.appLogFileURL?.path)
@@ -19,21 +20,19 @@ class AppDelegate: NSObject, NSApplicationDelegate {
}
let tunnelsManager: TunnelsManager = result.value!
let statusItemController = StatusItemController()
let statusMenu = StatusMenu(tunnelsManager: tunnelsManager)
self.statusItem = createStatusBarItem(with: statusMenu)
statusItemController.statusItem.menu = statusMenu
statusItemController.currentTunnel = statusMenu.currentTunnel
self.currentTunnelStatusObserver = statusMenu.observe(\.currentTunnel) { statusMenu, _ in
statusItemController.currentTunnel = statusMenu.currentTunnel
}
self.statusItemController = statusItemController
tunnelsManager.tunnelsListDelegate = statusMenu
tunnelsManager.activationDelegate = statusMenu
}
}
}
func createStatusBarItem(with statusMenu: StatusMenu) -> NSStatusItem {
let statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.squareLength)
if let statusBarImage = NSImage(named: "WireGuardMacStatusBarIcon") {
statusBarImage.isTemplate = true
statusItem.button?.image = statusBarImage
}
statusItem.menu = statusMenu
return statusItem
}
@@ -2,22 +2,25 @@
"images" : [
{
"idiom" : "universal",
"filename" : "WireGuardMacStatusBarIcon@1x.png",
"filename" : "StatusBarIcon@1x.png",
"scale" : "1x"
},
{
"idiom" : "universal",
"filename" : "WireGuardMacStatusBarIcon@2x.png",
"filename" : "StatusBarIcon@2x.png",
"scale" : "2x"
},
{
"idiom" : "universal",
"filename" : "WireGuardMacStatusBarIcon@3x.png",
"filename" : "StatusBarIcon@3x.png",
"scale" : "3x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
},
"properties" : {
"template-rendering-intent" : "template"
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 978 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

@@ -0,0 +1,26 @@
{
"images" : [
{
"idiom" : "universal",
"filename" : "StatusBarIconDimmed@1x.png",
"scale" : "1x"
},
{
"idiom" : "universal",
"filename" : "StatusBarIconDimmed@2x.png",
"scale" : "2x"
},
{
"idiom" : "universal",
"filename" : "StatusBarIconDimmed@3x.png",
"scale" : "3x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
},
"properties" : {
"template-rendering-intent" : "template"
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 881 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

@@ -0,0 +1,26 @@
{
"images" : [
{
"idiom" : "universal",
"filename" : "StatusBarIconDot1@1x.png",
"scale" : "1x"
},
{
"idiom" : "universal",
"filename" : "StatusBarIconDot1@2x.png",
"scale" : "2x"
},
{
"idiom" : "universal",
"filename" : "StatusBarIconDot1@3x.png",
"scale" : "3x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
},
"properties" : {
"template-rendering-intent" : "template"
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 953 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

@@ -0,0 +1,26 @@
{
"images" : [
{
"idiom" : "universal",
"filename" : "StatusBarIconDot2@1x.png",
"scale" : "1x"
},
{
"idiom" : "universal",
"filename" : "StatusBarIconDot2@2x.png",
"scale" : "2x"
},
{
"idiom" : "universal",
"filename" : "StatusBarIconDot2@3x.png",
"scale" : "3x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
},
"properties" : {
"template-rendering-intent" : "template"
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 942 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

@@ -0,0 +1,26 @@
{
"images" : [
{
"idiom" : "universal",
"filename" : "StatusBarIconDot3@1x.png",
"scale" : "1x"
},
{
"idiom" : "universal",
"filename" : "StatusBarIconDot3@2x.png",
"scale" : "2x"
},
{
"idiom" : "universal",
"filename" : "StatusBarIconDot3@3x.png",
"scale" : "3x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
},
"properties" : {
"template-rendering-intent" : "template"
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 958 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 990 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

@@ -0,0 +1,66 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
import Cocoa
class StatusItemController {
var currentTunnel: TunnelContainer? {
didSet {
updateStatusItemImage()
}
}
let statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.squareLength)
private let statusBarImageWhenActive = NSImage(named: "StatusBarIcon")!
private let statusBarImageWhenInactive = NSImage(named: "StatusBarIconDimmed")!
private let animationImages = [
NSImage(named: "StatusBarIconDot1")!,
NSImage(named: "StatusBarIconDot2")!,
NSImage(named: "StatusBarIconDot3")!
]
private var animationImageIndex: Int = 0
private var animationTimer: Timer?
init() {
updateStatusItemImage()
}
func updateStatusItemImage() {
guard let currentTunnel = currentTunnel else {
stopActivatingAnimation()
statusItem.button?.image = statusBarImageWhenInactive
return
}
switch currentTunnel.status {
case .inactive:
stopActivatingAnimation()
statusItem.button?.image = statusBarImageWhenInactive
case .active:
stopActivatingAnimation()
statusItem.button?.image = statusBarImageWhenActive
case .activating, .waiting, .reasserting, .restarting:
startActivatingAnimation()
case .deactivating:
break
}
}
func startActivatingAnimation() {
guard animationTimer == nil else { return }
let timer = Timer(timeInterval: 0.3, repeats: true) { [weak self] _ in
guard let self = self else { return }
self.statusItem.button?.image = self.animationImages[self.animationImageIndex]
self.animationImageIndex = (self.animationImageIndex + 1) % self.animationImages.count
}
RunLoop.main.add(timer, forMode: .default)
animationTimer = timer
}
func stopActivatingAnimation() {
guard let timer = self.animationTimer else { return }
timer.invalidate()
animationTimer = nil
animationImageIndex = 0
}
}
+14 -1
View File
@@ -13,6 +13,8 @@ class StatusMenu: NSMenu {
var firstTunnelMenuItemIndex = 0
var numberOfTunnelMenuItems = 0
@objc dynamic var currentTunnel: TunnelContainer?
var manageTunnelsRootVC: ManageTunnelsRootViewController?
lazy var manageTunnelsWindow: NSWindow = {
manageTunnelsRootVC = ManageTunnelsRootViewController(tunnelsManager: tunnelsManager)
@@ -30,7 +32,11 @@ class StatusMenu: NSMenu {
addStatusMenuItems()
addItem(NSMenuItem.separator())
for index in 0 ..< tunnelsManager.numberOfTunnels() {
let isUpdated = updateStatusMenuItems(with: tunnelsManager.tunnel(at: index), ignoreInactive: true)
let tunnel = tunnelsManager.tunnel(at: index)
if tunnel.status != .inactive {
currentTunnel = tunnel
}
let isUpdated = updateStatusMenuItems(with: tunnel, ignoreInactive: true)
if isUpdated {
break
}
@@ -176,6 +182,13 @@ extension StatusMenu {
updateTunnelMenuItem(menuItem)
let statusObservationToken = tunnel.observe(\.status) { [weak self] tunnel, _ in
updateTunnelMenuItem(menuItem)
if tunnel.status == .deactivating || tunnel.status == .inactive {
if self?.currentTunnel == tunnel {
self?.currentTunnel = self?.tunnelsManager.waitingTunnel()
}
} else {
self?.currentTunnel = tunnel
}
self?.updateStatusMenuItems(with: tunnel, ignoreInactive: false)
}
tunnelStatusObservers.insert(statusObservationToken, at: tunnelIndex)