aboutsummaryrefslogblamecommitdiffstats
path: root/WireGuard/WireGuard/UI/macOS/StatusMenu.swift
blob: 39c0f32b3325691ca65ef47ff1af46c05c39dc3d (plain) (tree)
1
2
3
4
5
6
7
8
9
10







                                                        

                                             

                                     
                                         
                                        
 
                                                             
                                              

                                                                                             
                                                        



                                                                                                                            


                                                      









                                                                                                               




                                                
                                  





                                                           
                               
                                                                                  


































                                                                                                                                           
                                                                       










                                                                                               

                                                              

                                                             
                                                                          
         


                                  








                                                                                                                                  









                                                                                               
                                       
                                               
                                                      


                                       

                                                      
                                                                                                               


     





                                                                                                                   
                                                                                        
                                          
                                                                            








































                                                                                                                        


                                                     
                                                    
                                                                  


                                        

                                                                            
         
                                                                     


                                                            
                                                        
                                                                                     


                                       
                                       
                                                                    
     
 

























                                                                                                              
// SPDX-License-Identifier: MIT
// Copyright © 2018 WireGuard LLC. All Rights Reserved.

import Cocoa

class StatusMenu: NSMenu {

    let tunnelsManager: TunnelsManager
    var tunnelStatusObservers = [AnyObject]()

    var statusMenuItem: NSMenuItem?
    var networksMenuItem: NSMenuItem?
    var firstTunnelMenuItemIndex: Int = 0
    var numberOfTunnelMenuItems: Int = 0

    var manageTunnelsRootVC: ManageTunnelsRootViewController?
    lazy var manageTunnelsWindow: NSWindow = {
        manageTunnelsRootVC = ManageTunnelsRootViewController(tunnelsManager: tunnelsManager)
        let window = NSWindow(contentViewController: manageTunnelsRootVC!)
        window.title = tr("macWindowTitleManageTunnels")
        window.setFrameAutosaveName(NSWindow.FrameAutosaveName("ManageTunnelsWindow")) // Auto-save window position and size
        return window
    }()

    init(tunnelsManager: TunnelsManager) {
        self.tunnelsManager = tunnelsManager
        super.init(title: "WireGuard Status Bar Menu")

        addStatusMenuItems()
        addItem(NSMenuItem.separator())
        for index in 0 ..< tunnelsManager.numberOfTunnels() {
            let isUpdated = updateStatusMenuItems(with: tunnelsManager.tunnel(at: index), ignoreInactive: true)
            if isUpdated {
                break
            }
        }

        firstTunnelMenuItemIndex = numberOfItems
        let isAdded = addTunnelMenuItems()
        if isAdded {
            addItem(NSMenuItem.separator())
        }
        addTunnelManagementItems()
    }

    required init(coder decoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    func addStatusMenuItems() {
        let statusTitle = tr(format: "macStatus (%@)", tr("tunnelStatusInactive"))
        let statusMenuItem = NSMenuItem(title: statusTitle, action: #selector(manageTunnelsClicked), keyEquivalent: "")
        statusMenuItem.isEnabled = false
        addItem(statusMenuItem)
        let networksMenuItem = NSMenuItem(title: tr("macMenuNetworksInactive"), action: #selector(manageTunnelsClicked), keyEquivalent: "")
        networksMenuItem.isEnabled = false
        addItem(networksMenuItem)
        self.statusMenuItem = statusMenuItem
        self.networksMenuItem = networksMenuItem
    }

    @discardableResult
    func updateStatusMenuItems(with tunnel: TunnelContainer, ignoreInactive: Bool) -> Bool {
        guard let statusMenuItem = statusMenuItem, let networksMenuItem = networksMenuItem else { return false }
        var statusText: String

        switch tunnel.status {
        case .waiting:
            return false
        case .inactive:
            if ignoreInactive {
                return false
            }
            statusText = tr("tunnelStatusInactive")
        case .activating:
            statusText = tr("tunnelStatusActivating")
        case .active:
            statusText = tr("tunnelStatusActive")
        case .deactivating:
            statusText = tr("tunnelStatusDeactivating")
        case .reasserting:
            statusText = tr("tunnelStatusReasserting")
        case .restarting:
            statusText = tr("tunnelStatusRestarting")
        }

        statusMenuItem.title = tr(format: "macStatus (%@)", statusText)

        let addresses = tunnel.tunnelConfiguration?.interface.addresses ?? []
        let addressesString = addresses.map { $0.stringRepresentation }.joined(separator: ", ")
        if addressesString.isEmpty {
            networksMenuItem.title = tr("macMenuNetworksNone")
        } else {
            networksMenuItem.title = tr(format: "macMenuNetworks (%@)", addressesString)
        }
        return true
    }

    func addTunnelMenuItems() -> Bool {
        let numberOfTunnels = tunnelsManager.numberOfTunnels()
        for index in 0 ..< tunnelsManager.numberOfTunnels() {
            let tunnel = tunnelsManager.tunnel(at: index)
            insertTunnelMenuItem(for: tunnel, at: numberOfTunnelMenuItems)
        }
        return numberOfTunnels > 0
    }

    func addTunnelManagementItems() {
        let manageItem = NSMenuItem(title: tr("macMenuManageTunnels"), action: #selector(manageTunnelsClicked), keyEquivalent: "")
        manageItem.target = self
        addItem(manageItem)
        let importItem = NSMenuItem(title: tr("macMenuImportTunnels"), action: #selector(importTunnelsClicked), keyEquivalent: "")
        importItem.target = self
        addItem(importItem)
    }

    @objc func tunnelClicked(sender: AnyObject) {
        guard let tunnelMenuItem = sender as? NSMenuItem else { return }
        guard let tunnel = tunnelMenuItem.representedObject as? TunnelContainer else { return }
        if tunnelMenuItem.state == .off {
            tunnelsManager.startActivation(of: tunnel)
        } else {
            tunnelsManager.startDeactivation(of: tunnel)
        }
    }

    @objc func manageTunnelsClicked() {
        NSApp.activate(ignoringOtherApps: true)
        manageTunnelsWindow.makeKeyAndOrderFront(self)
    }

    @objc func importTunnelsClicked() {
        NSApp.activate(ignoringOtherApps: true)
        manageTunnelsWindow.makeKeyAndOrderFront(self)
        ImportPanelPresenter.presentImportPanel(tunnelsManager: tunnelsManager, sourceVC: manageTunnelsRootVC!)
    }
}

extension StatusMenu {
    func insertTunnelMenuItem(for tunnel: TunnelContainer, at tunnelIndex: Int) {
        let menuItem = NSMenuItem(title: tunnel.name, action: #selector(tunnelClicked(sender:)), keyEquivalent: "")
        menuItem.target = self
        menuItem.representedObject = tunnel
        updateTunnelMenuItem(menuItem)
        let statusObservationToken = tunnel.observe(\.status) { [weak self] tunnel, _ in
            updateTunnelMenuItem(menuItem)
            self?.updateStatusMenuItems(with: tunnel, ignoreInactive: false)
        }
        tunnelStatusObservers.insert(statusObservationToken, at: tunnelIndex)
        insertItem(menuItem, at: firstTunnelMenuItemIndex + tunnelIndex)
        if numberOfTunnelMenuItems == 0 {
            insertItem(NSMenuItem.separator(), at: firstTunnelMenuItemIndex + tunnelIndex + 1)
        }
        numberOfTunnelMenuItems += 1
    }

    func removeTunnelMenuItem(at tunnelIndex: Int) {
        removeItem(at: firstTunnelMenuItemIndex + tunnelIndex)
        tunnelStatusObservers.remove(at: tunnelIndex)
        numberOfTunnelMenuItems -= 1
        if numberOfTunnelMenuItems == 0 {
            if let firstItem = item(at: firstTunnelMenuItemIndex), firstItem.isSeparatorItem {
                removeItem(at: firstTunnelMenuItemIndex)
            }
        }
    }

    func moveTunnelMenuItem(from oldTunnelIndex: Int, to newTunnelIndex: Int) {
        let oldMenuItem = item(at: firstTunnelMenuItemIndex + oldTunnelIndex)!
        let oldMenuItemTitle = oldMenuItem.title
        let oldMenuItemTunnel = oldMenuItem.representedObject
        removeItem(at: firstTunnelMenuItemIndex + oldTunnelIndex)
        let menuItem = NSMenuItem(title: oldMenuItemTitle, action: #selector(tunnelClicked(sender:)), keyEquivalent: "")
        menuItem.target = self
        menuItem.representedObject = oldMenuItemTunnel
        insertItem(menuItem, at: firstTunnelMenuItemIndex + newTunnelIndex)
        let statusObserver = tunnelStatusObservers.remove(at: oldTunnelIndex)
        tunnelStatusObservers.insert(statusObserver, at: newTunnelIndex)
    }
}

private func updateTunnelMenuItem(_ tunnelMenuItem: NSMenuItem) {
    guard let tunnel = tunnelMenuItem.representedObject as? TunnelContainer else { return }
    tunnelMenuItem.title = tunnel.name
    let shouldShowCheckmark = (tunnel.status != .inactive && tunnel.status != .deactivating)
    tunnelMenuItem.state = shouldShowCheckmark ? .on : .off
}

extension StatusMenu: TunnelsManagerListDelegate {
    func tunnelAdded(at index: Int) {
        let tunnel = tunnelsManager.tunnel(at: index)
        insertTunnelMenuItem(for: tunnel, at: index)
        manageTunnelsRootVC?.tunnelsListVC?.tunnelAdded(at: index)
    }

    func tunnelModified(at index: Int) {
        if let tunnelMenuItem = item(at: firstTunnelMenuItemIndex + index) {
            updateTunnelMenuItem(tunnelMenuItem)
        }
        manageTunnelsRootVC?.tunnelsListVC?.tunnelModified(at: index)
    }

    func tunnelMoved(from oldIndex: Int, to newIndex: Int) {
        moveTunnelMenuItem(from: oldIndex, to: newIndex)
        manageTunnelsRootVC?.tunnelsListVC?.tunnelMoved(from: oldIndex, to: newIndex)
    }

    func tunnelRemoved(at index: Int) {
        removeTunnelMenuItem(at: index)
        manageTunnelsRootVC?.tunnelsListVC?.tunnelRemoved(at: index)
    }
}

extension StatusMenu: TunnelsManagerActivationDelegate {
    func tunnelActivationAttemptFailed(tunnel: TunnelContainer, error: TunnelsManagerActivationAttemptError) {
        if let manageTunnelsRootVC = manageTunnelsRootVC, manageTunnelsWindow.isVisible {
            ErrorPresenter.showErrorAlert(error: error, from: manageTunnelsRootVC)
        } else {
            ErrorPresenter.showErrorAlert(error: error, from: nil)
        }
    }

    func tunnelActivationAttemptSucceeded(tunnel: TunnelContainer) {
        // Nothing to do
    }

    func tunnelActivationFailed(tunnel: TunnelContainer, error: TunnelsManagerActivationError) {
        if let manageTunnelsRootVC = manageTunnelsRootVC, manageTunnelsWindow.isVisible {
            ErrorPresenter.showErrorAlert(error: error, from: manageTunnelsRootVC)
        } else {
            ErrorPresenter.showErrorAlert(error: error, from: nil)
        }
    }

    func tunnelActivationSucceeded(tunnel: TunnelContainer) {
        // Nothing to do
    }
}