UI: macOS: Group more than 10 tunnels into submenu
Signed-off-by: Andrej Mihajlov <and@mullvad.net>
This commit is contained in:
parent
6d57c8b6f9
commit
e54a5d9a13
|
@ -297,6 +297,7 @@
|
||||||
"macMenuNetworksNone" = "Networks: None";
|
"macMenuNetworksNone" = "Networks: None";
|
||||||
|
|
||||||
"macMenuTitle" = "WireGuard";
|
"macMenuTitle" = "WireGuard";
|
||||||
|
"macTunnelsMenuTitle" = "Tunnels";
|
||||||
"macMenuManageTunnels" = "Manage Tunnels";
|
"macMenuManageTunnels" = "Manage Tunnels";
|
||||||
"macMenuImportTunnels" = "Import Tunnel(s) from File…";
|
"macMenuImportTunnels" = "Import Tunnel(s) from File…";
|
||||||
"macMenuAddEmptyTunnel" = "Add Empty Tunnel…";
|
"macMenuAddEmptyTunnel" = "Add Empty Tunnel…";
|
||||||
|
|
|
@ -14,8 +14,14 @@ class StatusMenu: NSMenu {
|
||||||
var statusMenuItem: NSMenuItem?
|
var statusMenuItem: NSMenuItem?
|
||||||
var networksMenuItem: NSMenuItem?
|
var networksMenuItem: NSMenuItem?
|
||||||
var deactivateMenuItem: 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? {
|
var currentTunnel: TunnelContainer? {
|
||||||
didSet {
|
didSet {
|
||||||
|
@ -26,16 +32,20 @@ class StatusMenu: NSMenu {
|
||||||
|
|
||||||
init(tunnelsManager: TunnelsManager) {
|
init(tunnelsManager: TunnelsManager) {
|
||||||
self.tunnelsManager = tunnelsManager
|
self.tunnelsManager = tunnelsManager
|
||||||
|
|
||||||
super.init(title: tr("macMenuTitle"))
|
super.init(title: tr("macMenuTitle"))
|
||||||
|
|
||||||
addStatusMenuItems()
|
addStatusMenuItems()
|
||||||
addItem(NSMenuItem.separator())
|
addItem(NSMenuItem.separator())
|
||||||
|
|
||||||
|
tunnelsMenuItem.submenu = tunnelsBreakdownMenu
|
||||||
|
addItem(tunnelsMenuItem)
|
||||||
|
|
||||||
firstTunnelMenuItemIndex = numberOfItems
|
firstTunnelMenuItemIndex = numberOfItems
|
||||||
let isAdded = addTunnelMenuItems()
|
populateInitialTunnelMenuItems()
|
||||||
if isAdded {
|
|
||||||
addItem(NSMenuItem.separator())
|
addItem(tunnelsMenuSeparatorItem)
|
||||||
}
|
|
||||||
addTunnelManagementItems()
|
addTunnelManagementItems()
|
||||||
addItem(NSMenuItem.separator())
|
addItem(NSMenuItem.separator())
|
||||||
addApplicationItems()
|
addApplicationItems()
|
||||||
|
@ -108,15 +118,6 @@ class StatusMenu: NSMenu {
|
||||||
deactivateMenuItem.isHidden = tunnel.status != .active
|
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() {
|
func addTunnelManagementItems() {
|
||||||
let manageItem = NSMenuItem(title: tr("macMenuManageTunnels"), action: #selector(manageTunnelsClicked), keyEquivalent: "")
|
let manageItem = NSMenuItem(title: tr("macMenuManageTunnels"), action: #selector(manageTunnelsClicked), keyEquivalent: "")
|
||||||
manageItem.target = self
|
manageItem.target = self
|
||||||
|
@ -166,34 +167,121 @@ class StatusMenu: NSMenu {
|
||||||
|
|
||||||
extension StatusMenu {
|
extension StatusMenu {
|
||||||
func insertTunnelMenuItem(for tunnel: TunnelContainer, at tunnelIndex: Int) {
|
func insertTunnelMenuItem(for tunnel: TunnelContainer, at tunnelIndex: Int) {
|
||||||
let menuItem = TunnelMenuItem(tunnel: tunnel, action: #selector(tunnelClicked(sender:)))
|
let nextNumberOfTunnels = numberOfTunnelMenuItems + 1
|
||||||
menuItem.target = self
|
|
||||||
menuItem.isHidden = !tunnel.isTunnelAvailableToUser
|
guard !reparentTunnelMenuItems(nextNumberOfTunnels: nextNumberOfTunnels) else {
|
||||||
insertItem(menuItem, at: firstTunnelMenuItemIndex + tunnelIndex)
|
return
|
||||||
if numberOfTunnelMenuItems == 0 {
|
|
||||||
insertItem(NSMenuItem.separator(), at: firstTunnelMenuItemIndex + tunnelIndex + 1)
|
|
||||||
}
|
}
|
||||||
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) {
|
func removeTunnelMenuItem(at tunnelIndex: Int) {
|
||||||
removeItem(at: firstTunnelMenuItemIndex + tunnelIndex)
|
let nextNumberOfTunnels = numberOfTunnelMenuItems - 1
|
||||||
numberOfTunnelMenuItems -= 1
|
|
||||||
if numberOfTunnelMenuItems == 0 {
|
guard !reparentTunnelMenuItems(nextNumberOfTunnels: nextNumberOfTunnels) else {
|
||||||
if let firstItem = item(at: firstTunnelMenuItemIndex), firstItem.isSeparatorItem {
|
return
|
||||||
removeItem(at: firstTunnelMenuItemIndex)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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) {
|
func moveTunnelMenuItem(from oldTunnelIndex: Int, to newTunnelIndex: Int) {
|
||||||
guard let oldMenuItem = item(at: firstTunnelMenuItemIndex + oldTunnelIndex) as? TunnelMenuItem else { return }
|
let tunnel = tunnelsManager.tunnel(at: newTunnelIndex)
|
||||||
let oldMenuItemTunnel = oldMenuItem.tunnel
|
let menuItem = makeTunnelItem(tunnel: tunnel)
|
||||||
removeItem(at: firstTunnelMenuItemIndex + oldTunnelIndex)
|
|
||||||
let menuItem = TunnelMenuItem(tunnel: oldMenuItemTunnel, action: #selector(tunnelClicked(sender:)))
|
|
||||||
menuItem.target = self
|
|
||||||
insertItem(menuItem, at: firstTunnelMenuItemIndex + newTunnelIndex)
|
|
||||||
|
|
||||||
|
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..<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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue