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
This commit is contained in:
Roopesh Chander 2019-01-16 01:00:42 +05:30
parent 02814ba546
commit b6d159ac96
28 changed files with 209 additions and 16 deletions

View File

@ -54,6 +54,7 @@
6F7774EF21722D97006A79B3 /* TunnelsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F7774EE21722D97006A79B3 /* TunnelsManager.swift */; }; 6F7774EF21722D97006A79B3 /* TunnelsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F7774EE21722D97006A79B3 /* TunnelsManager.swift */; };
6F7774F321774263006A79B3 /* TunnelEditTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F7774F221774263006A79B3 /* TunnelEditTableViewController.swift */; }; 6F7774F321774263006A79B3 /* TunnelEditTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F7774F221774263006A79B3 /* TunnelEditTableViewController.swift */; };
6F7F7E5F21C7D74B00527607 /* TunnelErrors.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F7F7E5E21C7D74B00527607 /* TunnelErrors.swift */; }; 6F7F7E5F21C7D74B00527607 /* TunnelErrors.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F7F7E5E21C7D74B00527607 /* TunnelErrors.swift */; };
6F89E17A21EDEB0E00C97BB9 /* StatusItemController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F89E17921EDEB0E00C97BB9 /* StatusItemController.swift */; };
6F919EC3218A2AE90023B400 /* ErrorPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F919EC2218A2AE90023B400 /* ErrorPresenter.swift */; }; 6F919EC3218A2AE90023B400 /* ErrorPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F919EC2218A2AE90023B400 /* ErrorPresenter.swift */; };
6F919ED9218C65C50023B400 /* wireguard_doc_logo_22x29.png in Resources */ = {isa = PBXBuildFile; fileRef = 6F919ED5218C65C50023B400 /* wireguard_doc_logo_22x29.png */; }; 6F919ED9218C65C50023B400 /* wireguard_doc_logo_22x29.png in Resources */ = {isa = PBXBuildFile; fileRef = 6F919ED5218C65C50023B400 /* wireguard_doc_logo_22x29.png */; };
6F919EDA218C65C50023B400 /* wireguard_doc_logo_44x58.png in Resources */ = {isa = PBXBuildFile; fileRef = 6F919ED6218C65C50023B400 /* wireguard_doc_logo_44x58.png */; }; 6F919EDA218C65C50023B400 /* wireguard_doc_logo_44x58.png in Resources */ = {isa = PBXBuildFile; fileRef = 6F919ED6218C65C50023B400 /* wireguard_doc_logo_44x58.png */; };
@ -263,6 +264,7 @@
6F7774EE21722D97006A79B3 /* TunnelsManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelsManager.swift; sourceTree = "<group>"; }; 6F7774EE21722D97006A79B3 /* TunnelsManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelsManager.swift; sourceTree = "<group>"; };
6F7774F221774263006A79B3 /* TunnelEditTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelEditTableViewController.swift; sourceTree = "<group>"; }; 6F7774F221774263006A79B3 /* TunnelEditTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelEditTableViewController.swift; sourceTree = "<group>"; };
6F7F7E5E21C7D74B00527607 /* TunnelErrors.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelErrors.swift; sourceTree = "<group>"; }; 6F7F7E5E21C7D74B00527607 /* TunnelErrors.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelErrors.swift; sourceTree = "<group>"; };
6F89E17921EDEB0E00C97BB9 /* StatusItemController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusItemController.swift; sourceTree = "<group>"; };
6F919EC2218A2AE90023B400 /* ErrorPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorPresenter.swift; sourceTree = "<group>"; }; 6F919EC2218A2AE90023B400 /* ErrorPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorPresenter.swift; sourceTree = "<group>"; };
6F919ED5218C65C50023B400 /* wireguard_doc_logo_22x29.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = wireguard_doc_logo_22x29.png; sourceTree = "<group>"; }; 6F919ED5218C65C50023B400 /* wireguard_doc_logo_22x29.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = wireguard_doc_logo_22x29.png; sourceTree = "<group>"; };
6F919ED6218C65C50023B400 /* wireguard_doc_logo_44x58.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = wireguard_doc_logo_44x58.png; sourceTree = "<group>"; }; 6F919ED6218C65C50023B400 /* wireguard_doc_logo_44x58.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = wireguard_doc_logo_44x58.png; sourceTree = "<group>"; };
@ -512,6 +514,7 @@
6FBA104421D7EA750051C35F /* ViewController */, 6FBA104421D7EA750051C35F /* ViewController */,
6FBA101321D613F30051C35F /* Application.swift */, 6FBA101321D613F30051C35F /* Application.swift */,
6FB1BD5F21D2607A00A991BF /* AppDelegate.swift */, 6FB1BD5F21D2607A00A991BF /* AppDelegate.swift */,
6F89E17921EDEB0E00C97BB9 /* StatusItemController.swift */,
6FBA101621D655340051C35F /* StatusMenu.swift */, 6FBA101621D655340051C35F /* StatusMenu.swift */,
6FBA104121D6BC210051C35F /* ErrorPresenter.swift */, 6FBA104121D6BC210051C35F /* ErrorPresenter.swift */,
6FCD99AE21E0EA1700BA4C82 /* ImportPanelPresenter.swift */, 6FCD99AE21E0EA1700BA4C82 /* ImportPanelPresenter.swift */,
@ -1111,6 +1114,7 @@
6FB1BDBC21D50F0200A991BF /* ringlogger.c in Sources */, 6FB1BDBC21D50F0200A991BF /* ringlogger.c in Sources */,
6FB1BDBD21D50F0200A991BF /* ringlogger.h in Sources */, 6FB1BDBD21D50F0200A991BF /* ringlogger.h in Sources */,
6FBA103F21D6B6FF0051C35F /* TunnelImporter.swift in Sources */, 6FBA103F21D6B6FF0051C35F /* TunnelImporter.swift in Sources */,
6F89E17A21EDEB0E00C97BB9 /* StatusItemController.swift in Sources */,
6F4DD16B21DA558800690EAE /* TunnelListRow.swift in Sources */, 6F4DD16B21DA558800690EAE /* TunnelListRow.swift in Sources */,
5F52D0BF21E3788900283CEA /* NSColor+Hex.swift in Sources */, 5F52D0BF21E3788900283CEA /* NSColor+Hex.swift in Sources */,
6FB1BDBE21D50F0200A991BF /* Logger.swift in Sources */, 6FB1BDBE21D50F0200A991BF /* Logger.swift in Sources */,

View File

@ -222,6 +222,10 @@ class TunnelsManager {
return tunnels.first { $0.name == tunnelName } return tunnels.first { $0.name == tunnelName }
} }
func waitingTunnel() -> TunnelContainer? {
return tunnels.first { $0.status == .waiting }
}
func startActivation(of tunnel: TunnelContainer) { func startActivation(of tunnel: TunnelContainer) {
guard tunnels.contains(tunnel) else { return } // Ensure it's not deleted guard tunnels.contains(tunnel) else { return } // Ensure it's not deleted
guard tunnel.status == .inactive else { guard tunnel.status == .inactive else {

View File

@ -6,7 +6,8 @@ import Cocoa
@NSApplicationMain @NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate { class AppDelegate: NSObject, NSApplicationDelegate {
var statusItem: NSStatusItem? var statusItemController: StatusItemController?
var currentTunnelStatusObserver: AnyObject?
func applicationDidFinishLaunching(_ aNotification: Notification) { func applicationDidFinishLaunching(_ aNotification: Notification) {
Logger.configureGlobal(withFilePath: FileManager.appLogFileURL?.path) Logger.configureGlobal(withFilePath: FileManager.appLogFileURL?.path)
@ -19,21 +20,19 @@ class AppDelegate: NSObject, NSApplicationDelegate {
} }
let tunnelsManager: TunnelsManager = result.value! let tunnelsManager: TunnelsManager = result.value!
let statusItemController = StatusItemController()
let statusMenu = StatusMenu(tunnelsManager: tunnelsManager) 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.tunnelsListDelegate = statusMenu
tunnelsManager.activationDelegate = 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
}

View File

@ -2,22 +2,25 @@
"images" : [ "images" : [
{ {
"idiom" : "universal", "idiom" : "universal",
"filename" : "WireGuardMacStatusBarIcon@1x.png", "filename" : "StatusBarIcon@1x.png",
"scale" : "1x" "scale" : "1x"
}, },
{ {
"idiom" : "universal", "idiom" : "universal",
"filename" : "WireGuardMacStatusBarIcon@2x.png", "filename" : "StatusBarIcon@2x.png",
"scale" : "2x" "scale" : "2x"
}, },
{ {
"idiom" : "universal", "idiom" : "universal",
"filename" : "WireGuardMacStatusBarIcon@3x.png", "filename" : "StatusBarIcon@3x.png",
"scale" : "3x" "scale" : "3x"
} }
], ],
"info" : { "info" : {
"version" : 1, "version" : 1,
"author" : "xcode" "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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -13,6 +13,8 @@ class StatusMenu: NSMenu {
var firstTunnelMenuItemIndex = 0 var firstTunnelMenuItemIndex = 0
var numberOfTunnelMenuItems = 0 var numberOfTunnelMenuItems = 0
@objc dynamic var currentTunnel: TunnelContainer?
var manageTunnelsRootVC: ManageTunnelsRootViewController? var manageTunnelsRootVC: ManageTunnelsRootViewController?
lazy var manageTunnelsWindow: NSWindow = { lazy var manageTunnelsWindow: NSWindow = {
manageTunnelsRootVC = ManageTunnelsRootViewController(tunnelsManager: tunnelsManager) manageTunnelsRootVC = ManageTunnelsRootViewController(tunnelsManager: tunnelsManager)
@ -30,7 +32,11 @@ class StatusMenu: NSMenu {
addStatusMenuItems() addStatusMenuItems()
addItem(NSMenuItem.separator()) addItem(NSMenuItem.separator())
for index in 0 ..< tunnelsManager.numberOfTunnels() { 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 { if isUpdated {
break break
} }
@ -176,6 +182,13 @@ extension StatusMenu {
updateTunnelMenuItem(menuItem) updateTunnelMenuItem(menuItem)
let statusObservationToken = tunnel.observe(\.status) { [weak self] tunnel, _ in let statusObservationToken = tunnel.observe(\.status) { [weak self] tunnel, _ in
updateTunnelMenuItem(menuItem) 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) self?.updateStatusMenuItems(with: tunnel, ignoreInactive: false)
} }
tunnelStatusObservers.insert(statusObservationToken, at: tunnelIndex) tunnelStatusObservers.insert(statusObservationToken, at: tunnelIndex)