From e54a5d9a13dc3eff90db6c19c738a93c92429174 Mon Sep 17 00:00:00 2001 From: Andrej Mihajlov Date: Tue, 22 Dec 2020 16:40:28 +0100 Subject: [PATCH] UI: macOS: Group more than 10 tunnels into submenu Signed-off-by: Andrej Mihajlov --- .../Base.lproj/Localizable.strings | 1 + .../WireGuardApp/UI/macOS/StatusMenu.swift | 173 ++++++++++++++---- 2 files changed, 140 insertions(+), 34 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 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) + let nextNumberOfTunnels = numberOfTunnelMenuItems + 1 + + guard !reparentTunnelMenuItems(nextNumberOfTunnels: nextNumberOfTunnels) else { + return } - numberOfTunnelMenuItems += 1 + + 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) { - removeItem(at: firstTunnelMenuItemIndex + tunnelIndex) - numberOfTunnelMenuItems -= 1 - if numberOfTunnelMenuItems == 0 { - if let firstItem = item(at: firstTunnelMenuItemIndex), firstItem.isSeparatorItem { - removeItem(at: firstTunnelMenuItemIndex) - } + 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) { - 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) + 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 + 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) + } + + updateTunnelsMenuItemVisibility() + } + + private func reparentTunnelMenuItems(nextNumberOfTunnels: Int) -> Bool { + let nextStyle = tunnelsPresentationStyle.preferredPresentationStyle(numberOfTunnels: nextNumberOfTunnels) + + switch (tunnelsPresentationStyle, nextStyle) { + case (.inline, .submenu): + tunnelsPresentationStyle = nextStyle + for index in (0.. Int { + let numberOfTunnels = tunnelsManager.numberOfTunnels() + for tunnelIndex in 0.. StatusMenuTunnelsPresentationStyle { + let maxInlineTunnels = 10 + + if case .inline = self, numberOfTunnels > maxInlineTunnels { + return .submenu + } else if case .submenu = self, numberOfTunnels <= maxInlineTunnels { + return .inline + } else { + return self + } + } +}