237 lines
9.8 KiB
Swift
237 lines
9.8 KiB
Swift
// SPDX-License-Identifier: MIT
|
|
// Copyright © 2018-2023 WireGuard LLC. All Rights Reserved.
|
|
|
|
import Cocoa
|
|
import ServiceManagement
|
|
|
|
@NSApplicationMain
|
|
class AppDelegate: NSObject, NSApplicationDelegate {
|
|
|
|
var tunnelsManager: TunnelsManager?
|
|
var tunnelsTracker: TunnelsTracker?
|
|
var statusItemController: StatusItemController?
|
|
|
|
var manageTunnelsRootVC: ManageTunnelsRootViewController?
|
|
var manageTunnelsWindowObject: NSWindow?
|
|
var onAppDeactivation: (() -> Void)?
|
|
|
|
func applicationWillFinishLaunching(_ notification: Notification) {
|
|
// To workaround a possible AppKit bug that causes the main menu to become unresponsive sometimes
|
|
// (especially when launched through Xcode) if we call setActivationPolicy(.regular) in
|
|
// in applicationDidFinishLaunching, we set it to .prohibited here.
|
|
// Setting it to .regular would fix that problem too, but at this point, we don't know
|
|
// whether the app was launched at login or not, so we're not sure whether we should
|
|
// show the app icon in the dock or not.
|
|
NSApp.setActivationPolicy(.prohibited)
|
|
}
|
|
|
|
func applicationDidFinishLaunching(_ aNotification: Notification) {
|
|
Logger.configureGlobal(tagged: "APP", withFilePath: FileManager.logFileURL?.path)
|
|
registerLoginItem(shouldLaunchAtLogin: true)
|
|
|
|
var isLaunchedAtLogin = false
|
|
if let appleEvent = NSAppleEventManager.shared().currentAppleEvent {
|
|
isLaunchedAtLogin = LaunchedAtLoginDetector.isLaunchedAtLogin(openAppleEvent: appleEvent)
|
|
}
|
|
|
|
NSApp.mainMenu = MainMenu()
|
|
setDockIconAndMainMenuVisibility(isVisible: !isLaunchedAtLogin)
|
|
|
|
TunnelsManager.create { [weak self] result in
|
|
guard let self = self else { return }
|
|
|
|
switch result {
|
|
case .failure(let error):
|
|
ErrorPresenter.showErrorAlert(error: error, from: nil)
|
|
case .success(let tunnelsManager):
|
|
let statusMenu = StatusMenu(tunnelsManager: tunnelsManager)
|
|
statusMenu.windowDelegate = self
|
|
|
|
let statusItemController = StatusItemController()
|
|
statusItemController.statusItem.menu = statusMenu
|
|
|
|
let tunnelsTracker = TunnelsTracker(tunnelsManager: tunnelsManager)
|
|
tunnelsTracker.statusMenu = statusMenu
|
|
tunnelsTracker.statusItemController = statusItemController
|
|
|
|
self.tunnelsManager = tunnelsManager
|
|
self.tunnelsTracker = tunnelsTracker
|
|
self.statusItemController = statusItemController
|
|
|
|
if !isLaunchedAtLogin {
|
|
self.showManageTunnelsWindow(completion: nil)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
@objc func confirmAndQuit() {
|
|
let alert = NSAlert()
|
|
alert.messageText = tr("macConfirmAndQuitAlertMessage")
|
|
if let currentTunnel = tunnelsTracker?.currentTunnel, currentTunnel.status == .active || currentTunnel.status == .activating {
|
|
alert.informativeText = tr(format: "macConfirmAndQuitInfoWithActiveTunnel (%@)", currentTunnel.name)
|
|
} else {
|
|
alert.informativeText = tr("macConfirmAndQuitAlertInfo")
|
|
}
|
|
alert.addButton(withTitle: tr("macConfirmAndQuitAlertCloseWindow"))
|
|
alert.addButton(withTitle: tr("macConfirmAndQuitAlertQuitWireGuard"))
|
|
|
|
NSApp.activate(ignoringOtherApps: true)
|
|
if let manageWindow = manageTunnelsWindowObject {
|
|
manageWindow.orderFront(self)
|
|
alert.beginSheetModal(for: manageWindow) { response in
|
|
switch response {
|
|
case .alertFirstButtonReturn:
|
|
manageWindow.close()
|
|
case .alertSecondButtonReturn:
|
|
NSApp.terminate(nil)
|
|
default:
|
|
break
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
@objc func quit() {
|
|
if let manageWindow = manageTunnelsWindowObject, manageWindow.attachedSheet != nil {
|
|
NSApp.activate(ignoringOtherApps: true)
|
|
manageWindow.orderFront(self)
|
|
return
|
|
}
|
|
registerLoginItem(shouldLaunchAtLogin: false)
|
|
guard let currentTunnel = tunnelsTracker?.currentTunnel, currentTunnel.status == .active || currentTunnel.status == .activating else {
|
|
NSApp.terminate(nil)
|
|
return
|
|
}
|
|
let alert = NSAlert()
|
|
alert.messageText = tr("macAppExitingWithActiveTunnelMessage")
|
|
alert.informativeText = tr("macAppExitingWithActiveTunnelInfo")
|
|
NSApp.activate(ignoringOtherApps: true)
|
|
if let manageWindow = manageTunnelsWindowObject {
|
|
manageWindow.orderFront(self)
|
|
alert.beginSheetModal(for: manageWindow) { _ in
|
|
NSApp.terminate(nil)
|
|
}
|
|
} else {
|
|
alert.runModal()
|
|
NSApp.terminate(nil)
|
|
}
|
|
}
|
|
|
|
func applicationShouldTerminate(_ sender: NSApplication) -> NSApplication.TerminateReply {
|
|
guard let currentTunnel = tunnelsTracker?.currentTunnel, currentTunnel.status == .active || currentTunnel.status == .activating else {
|
|
return .terminateNow
|
|
}
|
|
guard let appleEvent = NSAppleEventManager.shared().currentAppleEvent else {
|
|
return .terminateNow
|
|
}
|
|
guard MacAppStoreUpdateDetector.isUpdatingFromMacAppStore(quitAppleEvent: appleEvent) else {
|
|
return .terminateNow
|
|
}
|
|
let alert = NSAlert()
|
|
alert.messageText = tr("macAppStoreUpdatingAlertMessage")
|
|
if currentTunnel.isActivateOnDemandEnabled {
|
|
alert.informativeText = tr(format: "macAppStoreUpdatingAlertInfoWithOnDemand (%@)", currentTunnel.name)
|
|
} else {
|
|
alert.informativeText = tr(format: "macAppStoreUpdatingAlertInfoWithoutOnDemand (%@)", currentTunnel.name)
|
|
}
|
|
NSApp.activate(ignoringOtherApps: true)
|
|
if let manageWindow = manageTunnelsWindowObject {
|
|
alert.beginSheetModal(for: manageWindow) { _ in }
|
|
} else {
|
|
alert.runModal()
|
|
}
|
|
return .terminateCancel
|
|
}
|
|
|
|
func applicationShouldTerminateAfterLastWindowClosed(_ application: NSApplication) -> Bool {
|
|
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(200)) { [weak self] in
|
|
self?.setDockIconAndMainMenuVisibility(isVisible: false)
|
|
}
|
|
return false
|
|
}
|
|
|
|
private func setDockIconAndMainMenuVisibility(isVisible: Bool, completion: (() -> Void)? = nil) {
|
|
let currentActivationPolicy = NSApp.activationPolicy()
|
|
let newActivationPolicy: NSApplication.ActivationPolicy = isVisible ? .regular : .accessory
|
|
guard currentActivationPolicy != newActivationPolicy else {
|
|
if newActivationPolicy == .regular {
|
|
NSApp.activate(ignoringOtherApps: true)
|
|
}
|
|
completion?()
|
|
return
|
|
}
|
|
if newActivationPolicy == .regular && NSApp.isActive {
|
|
// To workaround a possible AppKit bug that causes the main menu to become unresponsive,
|
|
// we should deactivate the app first and then set the activation policy.
|
|
// NSApp.deactivate() doesn't always deactivate the app, so we instead use
|
|
// setActivationPolicy(.prohibited).
|
|
onAppDeactivation = {
|
|
NSApp.setActivationPolicy(.regular)
|
|
NSApp.activate(ignoringOtherApps: true)
|
|
completion?()
|
|
}
|
|
NSApp.setActivationPolicy(.prohibited)
|
|
} else {
|
|
NSApp.setActivationPolicy(newActivationPolicy)
|
|
if newActivationPolicy == .regular {
|
|
NSApp.activate(ignoringOtherApps: true)
|
|
}
|
|
completion?()
|
|
}
|
|
}
|
|
|
|
func applicationDidResignActive(_ notification: Notification) {
|
|
onAppDeactivation?()
|
|
onAppDeactivation = nil
|
|
}
|
|
}
|
|
|
|
extension AppDelegate {
|
|
@objc func aboutClicked() {
|
|
var appVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "Unknown"
|
|
if let appBuild = Bundle.main.infoDictionary?["CFBundleVersion"] as? String {
|
|
appVersion += " (\(appBuild))"
|
|
}
|
|
let appVersionString = [
|
|
tr(format: "macAppVersion (%@)", appVersion),
|
|
tr(format: "macGoBackendVersion (%@)", WIREGUARD_GO_VERSION)
|
|
].joined(separator: "\n")
|
|
NSApp.activate(ignoringOtherApps: true)
|
|
NSApp.orderFrontStandardAboutPanel(options: [
|
|
.applicationVersion: appVersionString,
|
|
.version: "",
|
|
.credits: ""
|
|
])
|
|
}
|
|
}
|
|
|
|
extension AppDelegate: StatusMenuWindowDelegate {
|
|
func showManageTunnelsWindow(completion: ((NSWindow?) -> Void)?) {
|
|
guard let tunnelsManager = tunnelsManager else {
|
|
completion?(nil)
|
|
return
|
|
}
|
|
if manageTunnelsWindowObject == nil {
|
|
manageTunnelsRootVC = ManageTunnelsRootViewController(tunnelsManager: tunnelsManager)
|
|
let window = NSWindow(contentViewController: manageTunnelsRootVC!)
|
|
window.title = tr("macWindowTitleManageTunnels")
|
|
window.setContentSize(NSSize(width: 800, height: 480))
|
|
window.setFrameAutosaveName(NSWindow.FrameAutosaveName("ManageTunnelsWindow")) // Auto-save window position and size
|
|
manageTunnelsWindowObject = window
|
|
tunnelsTracker?.manageTunnelsRootVC = manageTunnelsRootVC
|
|
}
|
|
setDockIconAndMainMenuVisibility(isVisible: true) { [weak manageTunnelsWindowObject] in
|
|
manageTunnelsWindowObject?.makeKeyAndOrderFront(self)
|
|
completion?(manageTunnelsWindowObject)
|
|
}
|
|
}
|
|
}
|
|
|
|
@discardableResult
|
|
func registerLoginItem(shouldLaunchAtLogin: Bool) -> Bool {
|
|
let appId = Bundle.main.bundleIdentifier!
|
|
let helperBundleId = "\(appId).login-item-helper"
|
|
return SMLoginItemSetEnabled(helperBundleId as CFString, shouldLaunchAtLogin)
|
|
}
|