aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorAndrej Mihajlov <and@mullvad.net>2020-12-22 16:40:28 +0100
committerJason A. Donenfeld <Jason@zx2c4.com>2020-12-23 14:40:54 +0100
commite54a5d9a13dc3eff90db6c19c738a93c92429174 (patch)
tree0a9fcc995d3a338681efb9c2e5fe22a48abc9572
parentUI: Avoid force unwrap when checking for errors (diff)
downloadwireguard-apple-e54a5d9a13dc3eff90db6c19c738a93c92429174.tar.xz
wireguard-apple-e54a5d9a13dc3eff90db6c19c738a93c92429174.zip
UI: macOS: Group more than 10 tunnels into submenu
Signed-off-by: Andrej Mihajlov <and@mullvad.net>
-rw-r--r--Sources/WireGuardApp/Base.lproj/Localizable.strings1
-rw-r--r--Sources/WireGuardApp/UI/macOS/StatusMenu.swift169
2 files changed, 138 insertions, 32 deletions
diff --git a/Sources/WireGuardApp/Base.lproj/Localizable.strings b/Sources/WireGuardApp/Base.lproj/Localizable.strings
index 4ef9540..a2cc455 100644
--- a/Sources/WireGuardApp/Base.lproj/Localizable.strings
+++ b/Sources/WireGuardApp/Base.lproj/Localizable.strings
@@ -297,6 +297,7 @@
"macMenuNetworksNone" = "Networks: None";
"macMenuTitle" = "WireGuard";
+"macTunnelsMenuTitle" = "Tunnels";
"macMenuManageTunnels" = "Manage Tunnels";
"macMenuImportTunnels" = "Import Tunnel(s) from File…";
"macMenuAddEmptyTunnel" = "Add Empty Tunnel…";
diff --git a/Sources/WireGuardApp/UI/macOS/StatusMenu.swift b/Sources/WireGuardApp/UI/macOS/StatusMenu.swift
index 5630fdc..d30ed88 100644
--- a/Sources/WireGuardApp/UI/macOS/StatusMenu.swift
+++ b/Sources/WireGuardApp/UI/macOS/StatusMenu.swift
@@ -14,8 +14,14 @@ class StatusMenu: NSMenu {
var statusMenuItem: NSMenuItem?
var networksMenuItem: NSMenuItem?
var deactivateMenuItem: NSMenuItem?
- var firstTunnelMenuItemIndex = 0
- var numberOfTunnelMenuItems = 0
+
+ private let tunnelsBreakdownMenu = NSMenu()
+ private let tunnelsMenuItem = NSMenuItem(title: tr("macTunnelsMenuTitle"), action: nil, keyEquivalent: "")
+ private let tunnelsMenuSeparatorItem = NSMenuItem.separator()
+
+ private var firstTunnelMenuItemIndex = 0
+ private var numberOfTunnelMenuItems = 0
+ private var tunnelsPresentationStyle = StatusMenuTunnelsPresentationStyle.inline
var currentTunnel: TunnelContainer? {
didSet {
@@ -26,16 +32,20 @@ class StatusMenu: NSMenu {
init(tunnelsManager: TunnelsManager) {
self.tunnelsManager = tunnelsManager
+
super.init(title: tr("macMenuTitle"))
addStatusMenuItems()
addItem(NSMenuItem.separator())
+ tunnelsMenuItem.submenu = tunnelsBreakdownMenu
+ addItem(tunnelsMenuItem)
+
firstTunnelMenuItemIndex = numberOfItems
- let isAdded = addTunnelMenuItems()
- if isAdded {
- addItem(NSMenuItem.separator())
- }
+ populateInitialTunnelMenuItems()
+
+ addItem(tunnelsMenuSeparatorItem)
+
addTunnelManagementItems()
addItem(NSMenuItem.separator())
addApplicationItems()
@@ -108,15 +118,6 @@ class StatusMenu: NSMenu {
deactivateMenuItem.isHidden = tunnel.status != .active
}
- 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
@@ -166,34 +167,121 @@ class StatusMenu: NSMenu {
extension StatusMenu {
func insertTunnelMenuItem(for tunnel: TunnelContainer, at tunnelIndex: Int) {
+ let nextNumberOfTunnels = numberOfTunnelMenuItems + 1
+
+ guard !reparentTunnelMenuItems(nextNumberOfTunnels: nextNumberOfTunnels) else {
+ return
+ }
+
+ let menuItem = makeTunnelItem(tunnel: tunnel)
+ switch tunnelsPresentationStyle {
+ case .submenu:
+ tunnelsBreakdownMenu.insertItem(menuItem, at: tunnelIndex)
+ case .inline:
+ insertItem(menuItem, at: firstTunnelMenuItemIndex + tunnelIndex)
+ }
+
+ numberOfTunnelMenuItems = nextNumberOfTunnels
+ updateTunnelsMenuItemVisibility()
+ }
+
+ func removeTunnelMenuItem(at tunnelIndex: Int) {
+ let nextNumberOfTunnels = numberOfTunnelMenuItems - 1
+
+ guard !reparentTunnelMenuItems(nextNumberOfTunnels: nextNumberOfTunnels) else {
+ return
+ }
+
+ switch tunnelsPresentationStyle {
+ case .submenu:
+ tunnelsBreakdownMenu.removeItem(at: tunnelIndex)
+ case .inline:
+ removeItem(at: firstTunnelMenuItemIndex + tunnelIndex)
+ }
+
+ numberOfTunnelMenuItems = nextNumberOfTunnels
+ updateTunnelsMenuItemVisibility()
+ }
+
+ func moveTunnelMenuItem(from oldTunnelIndex: Int, to newTunnelIndex: Int) {
+ let tunnel = tunnelsManager.tunnel(at: newTunnelIndex)
+ let menuItem = makeTunnelItem(tunnel: tunnel)
+
+ switch tunnelsPresentationStyle {
+ case .submenu:
+ tunnelsBreakdownMenu.removeItem(at: oldTunnelIndex)
+ tunnelsBreakdownMenu.insertItem(menuItem, at: newTunnelIndex)
+ case .inline:
+ removeItem(at: firstTunnelMenuItemIndex + oldTunnelIndex)
+ insertItem(menuItem, at: firstTunnelMenuItemIndex + newTunnelIndex)
+ }
+ }
+
+ private func makeTunnelItem(tunnel: TunnelContainer) -> TunnelMenuItem {
let menuItem = TunnelMenuItem(tunnel: tunnel, action: #selector(tunnelClicked(sender:)))
menuItem.target = self
menuItem.isHidden = !tunnel.isTunnelAvailableToUser
- insertItem(menuItem, at: firstTunnelMenuItemIndex + tunnelIndex)
- if numberOfTunnelMenuItems == 0 {
- insertItem(NSMenuItem.separator(), at: firstTunnelMenuItemIndex + tunnelIndex + 1)
+ return menuItem
+ }
+
+ private func populateInitialTunnelMenuItems() {
+ let numberOfTunnels = tunnelsManager.numberOfTunnels()
+ let initialStyle = tunnelsPresentationStyle.preferredPresentationStyle(numberOfTunnels: numberOfTunnels)
+
+ tunnelsPresentationStyle = initialStyle
+ switch initialStyle {
+ case .inline:
+ numberOfTunnelMenuItems = addTunnelMenuItems(into: self, at: firstTunnelMenuItemIndex)
+ case .submenu:
+ numberOfTunnelMenuItems = addTunnelMenuItems(into: tunnelsBreakdownMenu, at: 0)
}
- numberOfTunnelMenuItems += 1
+
+ updateTunnelsMenuItemVisibility()
}
- func removeTunnelMenuItem(at tunnelIndex: Int) {
- removeItem(at: firstTunnelMenuItemIndex + tunnelIndex)
- numberOfTunnelMenuItems -= 1
- if numberOfTunnelMenuItems == 0 {
- if let firstItem = item(at: firstTunnelMenuItemIndex), firstItem.isSeparatorItem {
- removeItem(at: firstTunnelMenuItemIndex)
+ private func reparentTunnelMenuItems(nextNumberOfTunnels: Int) -> Bool {
+ let nextStyle = tunnelsPresentationStyle.preferredPresentationStyle(numberOfTunnels: nextNumberOfTunnels)
+
+ switch (tunnelsPresentationStyle, nextStyle) {
+ case (.inline, .submenu):
+ tunnelsPresentationStyle = nextStyle
+ for index in (0..<numberOfTunnelMenuItems).reversed() {
+ removeItem(at: firstTunnelMenuItemIndex + index)
}
+ numberOfTunnelMenuItems = addTunnelMenuItems(into: tunnelsBreakdownMenu, at: 0)
+ updateTunnelsMenuItemVisibility()
+ return true
+
+ case (.submenu, .inline):
+ tunnelsPresentationStyle = nextStyle
+ tunnelsBreakdownMenu.removeAllItems()
+ numberOfTunnelMenuItems = addTunnelMenuItems(into: self, at: firstTunnelMenuItemIndex)
+ updateTunnelsMenuItemVisibility()
+ return true
+
+ case (.submenu, .submenu), (.inline, .inline):
+ return false
}
}
- func moveTunnelMenuItem(from oldTunnelIndex: Int, to newTunnelIndex: Int) {
- guard let oldMenuItem = item(at: firstTunnelMenuItemIndex + oldTunnelIndex) as? TunnelMenuItem else { return }
- let oldMenuItemTunnel = oldMenuItem.tunnel
- removeItem(at: firstTunnelMenuItemIndex + oldTunnelIndex)
- let menuItem = TunnelMenuItem(tunnel: oldMenuItemTunnel, action: #selector(tunnelClicked(sender:)))
- menuItem.target = self
- insertItem(menuItem, at: firstTunnelMenuItemIndex + newTunnelIndex)
+ private func addTunnelMenuItems(into menu: NSMenu, at startIndex: Int) -> Int {
+ let numberOfTunnels = tunnelsManager.numberOfTunnels()
+ for tunnelIndex in 0..<numberOfTunnels {
+ let tunnel = tunnelsManager.tunnel(at: tunnelIndex)
+ let menuItem = makeTunnelItem(tunnel: tunnel)
+ menu.insertItem(menuItem, at: startIndex + tunnelIndex)
+ }
+ return numberOfTunnels
+ }
+ private func updateTunnelsMenuItemVisibility() {
+ switch tunnelsPresentationStyle {
+ case .inline:
+ tunnelsMenuItem.isHidden = true
+ case .submenu:
+ tunnelsMenuItem.isHidden = false
+ }
+ tunnelsMenuSeparatorItem.isHidden = numberOfTunnelMenuItems == 0
}
}
@@ -232,3 +320,20 @@ class TunnelMenuItem: NSMenuItem {
state = shouldShowCheckmark ? .on : .off
}
}
+
+private enum StatusMenuTunnelsPresentationStyle {
+ case inline
+ case submenu
+
+ func preferredPresentationStyle(numberOfTunnels: Int) -> StatusMenuTunnelsPresentationStyle {
+ let maxInlineTunnels = 10
+
+ if case .inline = self, numberOfTunnels > maxInlineTunnels {
+ return .submenu
+ } else if case .submenu = self, numberOfTunnels <= maxInlineTunnels {
+ return .inline
+ } else {
+ return self
+ }
+ }
+}