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>
@@ -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"
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 978 B |
|
After Width: | Height: | Size: 1.7 KiB |
|
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"
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 881 B |
|
After Width: | Height: | Size: 1.4 KiB |
|
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"
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 953 B |
|
After Width: | Height: | Size: 1.5 KiB |
|
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"
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 942 B |
|
After Width: | Height: | Size: 1.5 KiB |
|
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"
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 958 B |
|
After Width: | Height: | Size: 1.5 KiB |
|
After Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 990 B |
|
Before Width: | Height: | Size: 1.7 KiB |
|
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
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||