diff --git a/WireGuard/WireGuard.xcodeproj/project.pbxproj b/WireGuard/WireGuard.xcodeproj/project.pbxproj index 70ff8b1..5118ea2 100644 --- a/WireGuard/WireGuard.xcodeproj/project.pbxproj +++ b/WireGuard/WireGuard.xcodeproj/project.pbxproj @@ -53,6 +53,7 @@ 6F1075642258AE9800D78929 /* DeleteTunnelsConfirmationAlert.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F1075632258AE9800D78929 /* DeleteTunnelsConfirmationAlert.swift */; }; 6F19D30422402B8700A126F2 /* ConfirmationAlertPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F19D30322402B8700A126F2 /* ConfirmationAlertPresenter.swift */; }; 6F2449E8226587B90047B9E9 /* MacAppStoreUpdateDetector.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F2449E7226587B80047B9E9 /* MacAppStoreUpdateDetector.swift */; }; + 6F3E02E9228000F6001FE7E3 /* MainMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F3E02E8228000F6001FE7E3 /* MainMenu.swift */; }; 6F4DD16B21DA558800690EAE /* TunnelListRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F4DD16A21DA558800690EAE /* TunnelListRow.swift */; }; 6F4DD16C21DA558F00690EAE /* NSTableView+Reuse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F4DD16721DA552B00690EAE /* NSTableView+Reuse.swift */; }; 6F4DD16E21DBEA0700690EAE /* ManageTunnelsRootViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F4DD16D21DBEA0700690EAE /* ManageTunnelsRootViewController.swift */; }; @@ -300,6 +301,7 @@ 6F1075632258AE9800D78929 /* DeleteTunnelsConfirmationAlert.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeleteTunnelsConfirmationAlert.swift; sourceTree = ""; }; 6F19D30322402B8700A126F2 /* ConfirmationAlertPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfirmationAlertPresenter.swift; sourceTree = ""; }; 6F2449E7226587B80047B9E9 /* MacAppStoreUpdateDetector.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MacAppStoreUpdateDetector.swift; sourceTree = ""; }; + 6F3E02E8228000F6001FE7E3 /* MainMenu.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MainMenu.swift; sourceTree = ""; }; 6F4DD16721DA552B00690EAE /* NSTableView+Reuse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSTableView+Reuse.swift"; sourceTree = ""; }; 6F4DD16A21DA558800690EAE /* TunnelListRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelListRow.swift; sourceTree = ""; }; 6F4DD16D21DBEA0700690EAE /* ManageTunnelsRootViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ManageTunnelsRootViewController.swift; sourceTree = ""; }; @@ -631,6 +633,7 @@ 6FB1BD5F21D2607A00A991BF /* AppDelegate.swift */, 6F89E17921EDEB0E00C97BB9 /* StatusItemController.swift */, 6FBA101621D655340051C35F /* StatusMenu.swift */, + 6F3E02E8228000F6001FE7E3 /* MainMenu.swift */, 6F89E17B21F090CC00C97BB9 /* TunnelsTracker.swift */, 6FBA104121D6BC210051C35F /* ErrorPresenter.swift */, 6FCD99AE21E0EA1700BA4C82 /* ImportPanelPresenter.swift */, @@ -1289,6 +1292,7 @@ 6F4DD16B21DA558800690EAE /* TunnelListRow.swift in Sources */, 6FDB6D15224CB2CE00EE4BC3 /* LogViewCell.swift in Sources */, 6FE3661D21F64F6B00F78C7D /* ConfTextColorTheme.swift in Sources */, + 6F3E02E9228000F6001FE7E3 /* MainMenu.swift in Sources */, 5F52D0BF21E3788900283CEA /* NSColor+Hex.swift in Sources */, 6FB1BDBE21D50F0200A991BF /* Logger.swift in Sources */, 6FB1BDBF21D50F0200A991BF /* TunnelConfiguration+WgQuickConfig.swift in Sources */, diff --git a/WireGuard/WireGuard/Base.lproj/Localizable.strings b/WireGuard/WireGuard/Base.lproj/Localizable.strings index 9cfaef1..1d05a45 100644 --- a/WireGuard/WireGuard/Base.lproj/Localizable.strings +++ b/WireGuard/WireGuard/Base.lproj/Localizable.strings @@ -291,7 +291,7 @@ "alertSystemErrorMessageTunnelConfigurationReadWriteFailed" = "Reading or writing the configuration failed."; "alertSystemErrorMessageTunnelConfigurationUnknown" = "Unknown system error."; -// Mac status bar menu / pulldown menu +// Mac status bar menu / pulldown menu / main menu "macMenuNetworks (%@)" = "Networks: %@"; "macMenuNetworksNone" = "Networks: None"; @@ -305,6 +305,31 @@ "macMenuAbout" = "About WireGuard"; "macMenuQuit" = "Quit"; +"macMenuHideApp" = "Hide WireGuard"; +"macMenuHideOtherApps" = "Hide Others"; +"macMenuShowAllApps" = "Show All"; + +"macMenuFile" = "File"; +"macMenuCloseWindow" = "Close Window"; + +"macMenuEdit" = "Edit"; +"macMenuUndo" = "Undo"; +"macMenuRedo" = "Redo"; +"macMenuCut" = "Cut"; +"macMenuCopy" = "Copy"; +"macMenuPaste" = "Paste"; +"macMenuSelectAll" = "Select All"; + +"macMenuTunnel" = "Tunnel"; +"macMenuToggleStatus" = "Toggle Status"; +"macMenuEditTunnel" = "Edit…"; +"macMenuDeleteSelected" = "Delete Selected"; + +"macMenuWindow" = "Window"; +"macMenuMinimize" = "Minimize"; +"macMenuZoom" = "Zoom"; +"macMenuFullScreen" = "Full Screen"; + // Mac manage tunnels window "macWindowTitleManageTunnels" = "Manage WireGuard Tunnels"; diff --git a/WireGuard/WireGuard/UI/macOS/AppDelegate.swift b/WireGuard/WireGuard/UI/macOS/AppDelegate.swift index 3f69ea3..8dc5825 100644 --- a/WireGuard/WireGuard/UI/macOS/AppDelegate.swift +++ b/WireGuard/WireGuard/UI/macOS/AppDelegate.swift @@ -17,6 +17,7 @@ class AppDelegate: NSObject, NSApplicationDelegate { func applicationDidFinishLaunching(_ aNotification: Notification) { Logger.configureGlobal(tagged: "APP", withFilePath: FileManager.logFileURL?.path) registerLoginItem(shouldLaunchAtLogin: true) + NSApp.mainMenu = MainMenu() TunnelsManager.create { [weak self] result in guard let self = self else { return } @@ -104,6 +105,24 @@ class AppDelegate: NSObject, NSApplicationDelegate { } } +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: "" + ]) + } +} + extension AppDelegate: StatusMenuWindowDelegate { func manageTunnelsWindow() -> NSWindow { if manageTunnelsWindowObject == nil { diff --git a/WireGuard/WireGuard/UI/macOS/MainMenu.swift b/WireGuard/WireGuard/UI/macOS/MainMenu.swift new file mode 100644 index 0000000..72bba71 --- /dev/null +++ b/WireGuard/WireGuard/UI/macOS/MainMenu.swift @@ -0,0 +1,129 @@ +// SPDX-License-Identifier: MIT +// Copyright © 2018 WireGuard LLC. All Rights Reserved. + +import Cocoa + +class MainMenu: NSMenu { + init() { + super.init(title: "") + addSubmenu(createApplicationMenu()) + addSubmenu(createFileMenu()) + addSubmenu(createEditMenu()) + addSubmenu(createTunnelMenu()) + addSubmenu(createWindowMenu()) + } + + required init(coder decoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private func addSubmenu(_ menu: NSMenu) { + let menuItem = self.addItem(withTitle: "", action: nil, keyEquivalent: "") + self.setSubmenu(menu, for: menuItem) + } + + private func createApplicationMenu() -> NSMenu { + let menu = NSMenu() + + let aboutMenuItem = menu.addItem(withTitle: tr("macMenuAbout"), + action: #selector(AppDelegate.aboutClicked), keyEquivalent: "") + aboutMenuItem.target = NSApp.delegate + + menu.addItem(NSMenuItem.separator()) + + menu.addItem(withTitle: tr("macMenuViewLog"), + action: #selector(TunnelsListTableViewController.handleViewLogAction), keyEquivalent: "") + + menu.addItem(NSMenuItem.separator()) + + let hideMenuItem = menu.addItem(withTitle: tr("macMenuHideApp"), + action: #selector(NSApplication.hide), keyEquivalent: "h") + hideMenuItem.target = NSApp + let hideOthersMenuItem = menu.addItem(withTitle: tr("macMenuHideOtherApps"), + action: #selector(NSApplication.hideOtherApplications), keyEquivalent: "h") + hideOthersMenuItem.keyEquivalentModifierMask = [.command, .option] + hideOthersMenuItem.target = NSApp + let showAllMenuItem = menu.addItem(withTitle: tr("macMenuShowAllApps"), + action: #selector(NSApplication.unhideAllApplications), keyEquivalent: "") + showAllMenuItem.target = NSApp + + menu.addItem(NSMenuItem.separator()) + + let quitMenuItem = menu.addItem(withTitle: tr("macMenuQuit"), + action: #selector(AppDelegate.quit), keyEquivalent: "q") + quitMenuItem.target = NSApp.delegate + + return menu + } + + private func createFileMenu() -> NSMenu { + let menu = NSMenu(title: tr("macMenuFile")) + + menu.addItem(withTitle: tr("macMenuAddEmptyTunnel"), + action: #selector(TunnelsListTableViewController.handleAddEmptyTunnelAction), keyEquivalent: "n") + menu.addItem(withTitle: tr("macMenuImportTunnels"), + action: #selector(TunnelsListTableViewController.handleImportTunnelAction), keyEquivalent: "o") + + menu.addItem(NSMenuItem.separator()) + + menu.addItem(withTitle: tr("macMenuExportTunnels"), + action: #selector(TunnelsListTableViewController.handleExportTunnelsAction), keyEquivalent: "") + + menu.addItem(NSMenuItem.separator()) + + menu.addItem(withTitle: tr("macMenuCloseWindow"), action: #selector(NSWindow.performClose(_:)), keyEquivalent:"w") + + return menu + } + + private func createEditMenu() -> NSMenu { + let menu = NSMenu(title: tr("macMenuEdit")) + + menu.addItem(withTitle: tr("macMenuUndo"), action: #selector(UndoActionRespondable.undo(_:)), keyEquivalent:"z") + menu.addItem(withTitle: tr("macMenuRedo"), action: #selector(UndoActionRespondable.redo(_:)), keyEquivalent:"Z") + + menu.addItem(NSMenuItem.separator()) + + menu.addItem(withTitle: tr("macMenuCut"), action: #selector(NSText.cut(_:)), keyEquivalent:"x") + menu.addItem(withTitle: tr("macMenuCopy"), action: #selector(NSText.copy(_:)), keyEquivalent:"c") + menu.addItem(withTitle: tr("macMenuPaste"), action: #selector(NSText.paste(_:)), keyEquivalent:"v") + + menu.addItem(NSMenuItem.separator()) + + menu.addItem(withTitle: tr("macMenuSelectAll"), action: #selector(NSText.selectAll(_:)), keyEquivalent:"a") + + return menu + } + + private func createTunnelMenu() -> NSMenu { + let menu = NSMenu(title: tr("macMenuTunnel")) + + menu.addItem(withTitle: tr("macMenuToggleStatus"), action: #selector(TunnelDetailTableViewController.handleToggleActiveStatusAction), keyEquivalent:"t") + + menu.addItem(NSMenuItem.separator()) + + menu.addItem(withTitle: tr("macMenuEditTunnel"), action: #selector(TunnelDetailTableViewController.handleEditTunnelAction), keyEquivalent:"e") + menu.addItem(withTitle: tr("macMenuDeleteSelected"), action: #selector(TunnelsListTableViewController.handleRemoveTunnelAction), keyEquivalent: "") + + return menu + } + + private func createWindowMenu() -> NSMenu { + let menu = NSMenu(title: tr("macMenuWindow")) + + menu.addItem(withTitle: tr("macMenuMinimize"), action: #selector(NSWindow.performMiniaturize(_:)), keyEquivalent:"m") + menu.addItem(withTitle: tr("macMenuZoom"), action: #selector(NSWindow.performZoom(_:)), keyEquivalent:"") + + menu.addItem(NSMenuItem.separator()) + + let fullScreenMenuItem = menu.addItem(withTitle: tr("macMenuFullScreen"), action: #selector(NSWindow.toggleFullScreen(_:)), keyEquivalent:"f") + fullScreenMenuItem.keyEquivalentModifierMask = [.command, .control] + + return menu + } +} + +@objc protocol UndoActionRespondable { + func undo(_ sender: AnyObject) + func redo(_ sender: AnyObject) +} diff --git a/WireGuard/WireGuard/UI/macOS/StatusMenu.swift b/WireGuard/WireGuard/UI/macOS/StatusMenu.swift index 150c55e..4044d4e 100644 --- a/WireGuard/WireGuard/UI/macOS/StatusMenu.swift +++ b/WireGuard/WireGuard/UI/macOS/StatusMenu.swift @@ -127,8 +127,8 @@ class StatusMenu: NSMenu { } func addApplicationItems() { - let aboutItem = NSMenuItem(title: tr("macMenuAbout"), action: #selector(aboutClicked), keyEquivalent: "") - aboutItem.target = self + let aboutItem = NSMenuItem(title: tr("macMenuAbout"), action: #selector(AppDelegate.aboutClicked), keyEquivalent: "") + aboutItem.target = NSApp.delegate addItem(aboutItem) let quitItem = NSMenuItem(title: tr("macMenuQuit"), action: #selector(AppDelegate.quit), keyEquivalent: "") quitItem.target = NSApp.delegate @@ -164,22 +164,6 @@ class StatusMenu: NSMenu { manageTunnelsWindow.makeKeyAndOrderFront(self) ImportPanelPresenter.presentImportPanel(tunnelsManager: tunnelsManager, sourceVC: manageTunnelsWindow.contentViewController) } - - @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: "" - ]) - } } extension StatusMenu {