]> git.ipfire.org Git - thirdparty/wireguard-apple.git/commitdiff
UI: macOS: Group more than 10 tunnels into submenu
authorAndrej Mihajlov <and@mullvad.net>
Tue, 22 Dec 2020 15:40:28 +0000 (16:40 +0100)
committerJason A. Donenfeld <Jason@zx2c4.com>
Wed, 23 Dec 2020 13:40:54 +0000 (14:40 +0100)
Signed-off-by: Andrej Mihajlov <and@mullvad.net>
Sources/WireGuardApp/Base.lproj/Localizable.strings
Sources/WireGuardApp/UI/macOS/StatusMenu.swift

index 4ef9540391f91ad2c9d9259f2db28b56e85c394b..a2cc45587e5d07753fae62f0cb1cd41d83ce8449 100644 (file)
 "macMenuNetworksNone" = "Networks: None";
 
 "macMenuTitle" = "WireGuard";
+"macTunnelsMenuTitle" = "Tunnels";
 "macMenuManageTunnels" = "Manage Tunnels";
 "macMenuImportTunnels" = "Import Tunnel(s) from File…";
 "macMenuAddEmptyTunnel" = "Add Empty Tunnel…";
index 5630fdcc408464419fd2d6d1100b6ca3d508e432..d30ed88e4e56797927139b890a45833f356db900 100644 (file)
@@ -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
+        }
+    }
+}