aboutsummaryrefslogtreecommitdiffstats
path: root/Sources/WireGuardApp/UI/macOS/ViewController/TunnelDetailTableViewController.swift
diff options
context:
space:
mode:
Diffstat (limited to 'Sources/WireGuardApp/UI/macOS/ViewController/TunnelDetailTableViewController.swift')
-rw-r--r--Sources/WireGuardApp/UI/macOS/ViewController/TunnelDetailTableViewController.swift558
1 files changed, 558 insertions, 0 deletions
diff --git a/Sources/WireGuardApp/UI/macOS/ViewController/TunnelDetailTableViewController.swift b/Sources/WireGuardApp/UI/macOS/ViewController/TunnelDetailTableViewController.swift
new file mode 100644
index 0000000..41d2b15
--- /dev/null
+++ b/Sources/WireGuardApp/UI/macOS/ViewController/TunnelDetailTableViewController.swift
@@ -0,0 +1,558 @@
+// SPDX-License-Identifier: MIT
+// Copyright © 2018-2023 WireGuard LLC. All Rights Reserved.
+
+import Cocoa
+
+class TunnelDetailTableViewController: NSViewController {
+
+ private enum TableViewModelRow {
+ case interfaceFieldRow(TunnelViewModel.InterfaceField)
+ case peerFieldRow(peer: TunnelViewModel.PeerData, field: TunnelViewModel.PeerField)
+ case onDemandRow
+ case onDemandSSIDRow
+ case spacerRow
+
+ func localizedSectionKeyString() -> String {
+ switch self {
+ case .interfaceFieldRow: return tr("tunnelSectionTitleInterface")
+ case .peerFieldRow: return tr("tunnelSectionTitlePeer")
+ case .onDemandRow: return tr("macFieldOnDemand")
+ case .onDemandSSIDRow: return ""
+ case .spacerRow: return ""
+ }
+ }
+
+ func isTitleRow() -> Bool {
+ switch self {
+ case .interfaceFieldRow(let field): return field == .name
+ case .peerFieldRow(_, let field): return field == .publicKey
+ case .onDemandRow: return true
+ case .onDemandSSIDRow: return false
+ case .spacerRow: return false
+ }
+ }
+ }
+
+ static let interfaceFields: [TunnelViewModel.InterfaceField] = [
+ .name, .status, .publicKey, .addresses,
+ .listenPort, .mtu, .dns, .toggleStatus
+ ]
+
+ static let peerFields: [TunnelViewModel.PeerField] = [
+ .publicKey, .preSharedKey, .endpoint,
+ .allowedIPs, .persistentKeepAlive,
+ .rxBytes, .txBytes, .lastHandshakeTime
+ ]
+
+ static let onDemandFields: [ActivateOnDemandViewModel.OnDemandField] = [
+ .onDemand, .ssid
+ ]
+
+ let tableView: NSTableView = {
+ let tableView = NSTableView()
+ tableView.addTableColumn(NSTableColumn(identifier: NSUserInterfaceItemIdentifier("TunnelDetail")))
+ tableView.headerView = nil
+ tableView.rowSizeStyle = .medium
+ tableView.backgroundColor = .clear
+ tableView.selectionHighlightStyle = .none
+ return tableView
+ }()
+
+ let editButton: NSButton = {
+ let button = NSButton()
+ button.title = tr("macButtonEdit")
+ button.setButtonType(.momentaryPushIn)
+ button.bezelStyle = .rounded
+ button.toolTip = tr("macToolTipEditTunnel")
+ return button
+ }()
+
+ let box: NSBox = {
+ let box = NSBox()
+ box.titlePosition = .noTitle
+ box.fillColor = .unemphasizedSelectedContentBackgroundColor
+ return box
+ }()
+
+ let tunnelsManager: TunnelsManager
+ let tunnel: TunnelContainer
+
+ var tunnelViewModel: TunnelViewModel {
+ didSet {
+ updateTableViewModelRowsBySection()
+ updateTableViewModelRows()
+ }
+ }
+
+ var onDemandViewModel: ActivateOnDemandViewModel
+
+ private var tableViewModelRowsBySection = [[(isVisible: Bool, modelRow: TableViewModelRow)]]()
+ private var tableViewModelRows = [TableViewModelRow]()
+
+ private var statusObservationToken: AnyObject?
+ private var tunnelEditVC: TunnelEditViewController?
+ private var reloadRuntimeConfigurationTimer: Timer?
+
+ init(tunnelsManager: TunnelsManager, tunnel: TunnelContainer) {
+ self.tunnelsManager = tunnelsManager
+ self.tunnel = tunnel
+ tunnelViewModel = TunnelViewModel(tunnelConfiguration: tunnel.tunnelConfiguration)
+ onDemandViewModel = ActivateOnDemandViewModel(tunnel: tunnel)
+ super.init(nibName: nil, bundle: nil)
+ updateTableViewModelRowsBySection()
+ updateTableViewModelRows()
+ statusObservationToken = tunnel.observe(\TunnelContainer.status) { [weak self] _, _ in
+ guard let self = self else { return }
+ if tunnel.status == .active {
+ self.startUpdatingRuntimeConfiguration()
+ } else if tunnel.status == .inactive {
+ self.reloadRuntimeConfiguration()
+ self.stopUpdatingRuntimeConfiguration()
+ }
+ }
+ }
+
+ required init?(coder: NSCoder) {
+ fatalError("init(coder:) has not been implemented")
+ }
+
+ override func loadView() {
+ tableView.dataSource = self
+ tableView.delegate = self
+
+ editButton.target = self
+ editButton.action = #selector(handleEditTunnelAction)
+
+ let clipView = NSClipView()
+ clipView.documentView = tableView
+
+ let scrollView = NSScrollView()
+ scrollView.contentView = clipView // Set contentView before setting drawsBackground
+ scrollView.drawsBackground = false
+ scrollView.hasVerticalScroller = true
+ scrollView.autohidesScrollers = true
+
+ let containerView = NSView()
+ let bottomControlsContainer = NSLayoutGuide()
+ containerView.addLayoutGuide(bottomControlsContainer)
+ containerView.addSubview(box)
+ containerView.addSubview(scrollView)
+ containerView.addSubview(editButton)
+ box.translatesAutoresizingMaskIntoConstraints = false
+ scrollView.translatesAutoresizingMaskIntoConstraints = false
+ editButton.translatesAutoresizingMaskIntoConstraints = false
+
+ NSLayoutConstraint.activate([
+ containerView.topAnchor.constraint(equalTo: scrollView.topAnchor),
+ containerView.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor),
+ containerView.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor),
+ containerView.leadingAnchor.constraint(equalTo: bottomControlsContainer.leadingAnchor),
+ containerView.trailingAnchor.constraint(equalTo: bottomControlsContainer.trailingAnchor),
+ bottomControlsContainer.heightAnchor.constraint(equalToConstant: 32),
+ scrollView.bottomAnchor.constraint(equalTo: bottomControlsContainer.topAnchor),
+ bottomControlsContainer.bottomAnchor.constraint(equalTo: containerView.bottomAnchor),
+ editButton.trailingAnchor.constraint(equalTo: bottomControlsContainer.trailingAnchor),
+ bottomControlsContainer.bottomAnchor.constraint(equalTo: editButton.bottomAnchor, constant: 0)
+ ])
+
+ NSLayoutConstraint.activate([
+ scrollView.topAnchor.constraint(equalTo: box.topAnchor),
+ scrollView.bottomAnchor.constraint(equalTo: box.bottomAnchor),
+ scrollView.leadingAnchor.constraint(equalTo: box.leadingAnchor),
+ scrollView.trailingAnchor.constraint(equalTo: box.trailingAnchor)
+ ])
+
+ NSLayoutConstraint.activate([
+ containerView.widthAnchor.constraint(greaterThanOrEqualToConstant: 320),
+ containerView.heightAnchor.constraint(greaterThanOrEqualToConstant: 120)
+ ])
+
+ view = containerView
+ }
+
+ func updateTableViewModelRowsBySection() {
+ var modelRowsBySection = [[(isVisible: Bool, modelRow: TableViewModelRow)]]()
+
+ var interfaceSection = [(isVisible: Bool, modelRow: TableViewModelRow)]()
+ for field in TunnelDetailTableViewController.interfaceFields {
+ let isStatus = field == .status || field == .toggleStatus
+ let isEmpty = tunnelViewModel.interfaceData[field].isEmpty
+ interfaceSection.append((isVisible: isStatus || !isEmpty, modelRow: .interfaceFieldRow(field)))
+ }
+ interfaceSection.append((isVisible: true, modelRow: .spacerRow))
+ modelRowsBySection.append(interfaceSection)
+
+ for peerData in tunnelViewModel.peersData {
+ var peerSection = [(isVisible: Bool, modelRow: TableViewModelRow)]()
+ for field in TunnelDetailTableViewController.peerFields {
+ peerSection.append((isVisible: !peerData[field].isEmpty, modelRow: .peerFieldRow(peer: peerData, field: field)))
+ }
+ peerSection.append((isVisible: true, modelRow: .spacerRow))
+ modelRowsBySection.append(peerSection)
+ }
+
+ var onDemandSection = [(isVisible: Bool, modelRow: TableViewModelRow)]()
+ onDemandSection.append((isVisible: true, modelRow: .onDemandRow))
+ if onDemandViewModel.isWiFiInterfaceEnabled {
+ onDemandSection.append((isVisible: true, modelRow: .onDemandSSIDRow))
+ }
+ modelRowsBySection.append(onDemandSection)
+
+ tableViewModelRowsBySection = modelRowsBySection
+ }
+
+ func updateTableViewModelRows() {
+ tableViewModelRows = tableViewModelRowsBySection.flatMap { $0.filter { $0.isVisible }.map { $0.modelRow } }
+ }
+
+ @objc func handleEditTunnelAction() {
+ PrivateDataConfirmation.confirmAccess(to: tr("macViewPrivateData")) { [weak self] in
+ guard let self = self else { return }
+ let tunnelEditVC = TunnelEditViewController(tunnelsManager: self.tunnelsManager, tunnel: self.tunnel)
+ tunnelEditVC.delegate = self
+ self.presentAsSheet(tunnelEditVC)
+ self.tunnelEditVC = tunnelEditVC
+ }
+ }
+
+ @objc func handleToggleActiveStatusAction() {
+ if tunnel.hasOnDemandRules {
+ let turnOn = !tunnel.isActivateOnDemandEnabled
+ tunnelsManager.setOnDemandEnabled(turnOn, on: tunnel) { error in
+ if error == nil && !turnOn {
+ self.tunnelsManager.startDeactivation(of: self.tunnel)
+ }
+ }
+ } else {
+ if tunnel.status == .inactive {
+ tunnelsManager.startActivation(of: tunnel)
+ } else if tunnel.status == .active {
+ tunnelsManager.startDeactivation(of: tunnel)
+ }
+ }
+ }
+
+ override func viewWillAppear() {
+ if tunnel.status == .active {
+ startUpdatingRuntimeConfiguration()
+ }
+ }
+
+ override func viewWillDisappear() {
+ super.viewWillDisappear()
+ if let tunnelEditVC = tunnelEditVC {
+ dismiss(tunnelEditVC)
+ }
+ stopUpdatingRuntimeConfiguration()
+ }
+
+ func applyTunnelConfiguration(tunnelConfiguration: TunnelConfiguration) {
+ // Incorporates changes from tunnelConfiguation. Ignores any changes in peer ordering.
+
+ let tableView = self.tableView
+
+ func handleSectionFieldsModified<T>(fields: [T], modelRowsInSection: [(isVisible: Bool, modelRow: TableViewModelRow)], rowOffset: Int, changes: [T: TunnelViewModel.Changes.FieldChange]) {
+ var modifiedRowIndices = IndexSet()
+ for (index, field) in fields.enumerated() {
+ guard let change = changes[field] else { continue }
+ if case .modified = change {
+ let row = modelRowsInSection[0 ..< index].filter { $0.isVisible }.count
+ modifiedRowIndices.insert(rowOffset + row)
+ }
+ }
+ if !modifiedRowIndices.isEmpty {
+ tableView.reloadData(forRowIndexes: modifiedRowIndices, columnIndexes: IndexSet(integer: 0))
+ }
+ }
+
+ func handleSectionFieldsAddedOrRemoved<T>(fields: [T], modelRowsInSection: inout [(isVisible: Bool, modelRow: TableViewModelRow)], rowOffset: Int, changes: [T: TunnelViewModel.Changes.FieldChange]) {
+ for (index, field) in fields.enumerated() {
+ guard let change = changes[field] else { continue }
+ let row = modelRowsInSection[0 ..< index].filter { $0.isVisible }.count
+ switch change {
+ case .added:
+ tableView.insertRows(at: IndexSet(integer: rowOffset + row), withAnimation: .effectFade)
+ modelRowsInSection[index].isVisible = true
+ case .removed:
+ tableView.removeRows(at: IndexSet(integer: rowOffset + row), withAnimation: .effectFade)
+ modelRowsInSection[index].isVisible = false
+ case .modified:
+ break
+ }
+ }
+ }
+
+ let changes = self.tunnelViewModel.applyConfiguration(other: tunnelConfiguration)
+
+ if !changes.interfaceChanges.isEmpty {
+ handleSectionFieldsModified(fields: TunnelDetailTableViewController.interfaceFields,
+ modelRowsInSection: self.tableViewModelRowsBySection[0],
+ rowOffset: 0, changes: changes.interfaceChanges)
+ }
+ for (peerIndex, peerChanges) in changes.peerChanges {
+ let sectionIndex = 1 + peerIndex
+ let rowOffset = self.tableViewModelRowsBySection[0 ..< sectionIndex].flatMap { $0.filter { $0.isVisible } }.count
+ handleSectionFieldsModified(fields: TunnelDetailTableViewController.peerFields,
+ modelRowsInSection: self.tableViewModelRowsBySection[sectionIndex],
+ rowOffset: rowOffset, changes: peerChanges)
+ }
+
+ let isAnyInterfaceFieldAddedOrRemoved = changes.interfaceChanges.contains { $0.value == .added || $0.value == .removed }
+ let isAnyPeerFieldAddedOrRemoved = changes.peerChanges.contains { $0.changes.contains { $0.value == .added || $0.value == .removed } }
+
+ if isAnyInterfaceFieldAddedOrRemoved || isAnyPeerFieldAddedOrRemoved || !changes.peersRemovedIndices.isEmpty || !changes.peersInsertedIndices.isEmpty {
+ tableView.beginUpdates()
+ if isAnyInterfaceFieldAddedOrRemoved {
+ handleSectionFieldsAddedOrRemoved(fields: TunnelDetailTableViewController.interfaceFields,
+ modelRowsInSection: &self.tableViewModelRowsBySection[0],
+ rowOffset: 0, changes: changes.interfaceChanges)
+ }
+ if isAnyPeerFieldAddedOrRemoved {
+ for (peerIndex, peerChanges) in changes.peerChanges {
+ let sectionIndex = 1 + peerIndex
+ let rowOffset = self.tableViewModelRowsBySection[0 ..< sectionIndex].flatMap { $0.filter { $0.isVisible } }.count
+ handleSectionFieldsAddedOrRemoved(fields: TunnelDetailTableViewController.peerFields, modelRowsInSection: &self.tableViewModelRowsBySection[sectionIndex], rowOffset: rowOffset, changes: peerChanges)
+ }
+ }
+ if !changes.peersRemovedIndices.isEmpty {
+ for peerIndex in changes.peersRemovedIndices {
+ let sectionIndex = 1 + peerIndex
+ let rowOffset = self.tableViewModelRowsBySection[0 ..< sectionIndex].flatMap { $0.filter { $0.isVisible } }.count
+ let count = self.tableViewModelRowsBySection[sectionIndex].filter { $0.isVisible }.count
+ self.tableView.removeRows(at: IndexSet(integersIn: rowOffset ..< rowOffset + count), withAnimation: .effectFade)
+ self.tableViewModelRowsBySection.remove(at: sectionIndex)
+ }
+ }
+ if !changes.peersInsertedIndices.isEmpty {
+ for peerIndex in changes.peersInsertedIndices {
+ let peerData = self.tunnelViewModel.peersData[peerIndex]
+ let sectionIndex = 1 + peerIndex
+ let rowOffset = self.tableViewModelRowsBySection[0 ..< sectionIndex].flatMap { $0.filter { $0.isVisible } }.count
+ var modelRowsInSection: [(isVisible: Bool, modelRow: TableViewModelRow)] = TunnelDetailTableViewController.peerFields.map {
+ (isVisible: !peerData[$0].isEmpty, modelRow: .peerFieldRow(peer: peerData, field: $0))
+ }
+ modelRowsInSection.append((isVisible: true, modelRow: .spacerRow))
+ let count = modelRowsInSection.filter { $0.isVisible }.count
+ self.tableView.insertRows(at: IndexSet(integersIn: rowOffset ..< rowOffset + count), withAnimation: .effectFade)
+ self.tableViewModelRowsBySection.insert(modelRowsInSection, at: sectionIndex)
+ }
+ }
+ updateTableViewModelRows()
+ tableView.endUpdates()
+ }
+ }
+
+ private func reloadRuntimeConfiguration() {
+ tunnel.getRuntimeTunnelConfiguration { [weak self] tunnelConfiguration in
+ guard let tunnelConfiguration = tunnelConfiguration else { return }
+ self?.applyTunnelConfiguration(tunnelConfiguration: tunnelConfiguration)
+ }
+ }
+
+ func startUpdatingRuntimeConfiguration() {
+ reloadRuntimeConfiguration()
+ reloadRuntimeConfigurationTimer?.invalidate()
+ let reloadTimer = Timer(timeInterval: 1 /* second */, repeats: true) { [weak self] _ in
+ self?.reloadRuntimeConfiguration()
+ }
+ reloadRuntimeConfigurationTimer = reloadTimer
+ RunLoop.main.add(reloadTimer, forMode: .common)
+ }
+
+ func stopUpdatingRuntimeConfiguration() {
+ reloadRuntimeConfiguration()
+ reloadRuntimeConfigurationTimer?.invalidate()
+ reloadRuntimeConfigurationTimer = nil
+ }
+
+}
+
+extension TunnelDetailTableViewController: NSTableViewDataSource {
+ func numberOfRows(in tableView: NSTableView) -> Int {
+ return tableViewModelRows.count
+ }
+}
+
+extension TunnelDetailTableViewController: NSTableViewDelegate {
+ func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? {
+ let modelRow = tableViewModelRows[row]
+ switch modelRow {
+ case .interfaceFieldRow(let field):
+ if field == .status {
+ return statusCell()
+ } else if field == .toggleStatus {
+ return toggleStatusCell()
+ } else {
+ let cell: KeyValueRow = tableView.dequeueReusableCell()
+ let localizedKeyString = modelRow.isTitleRow() ? modelRow.localizedSectionKeyString() : field.localizedUIString
+ cell.key = tr(format: "macFieldKey (%@)", localizedKeyString)
+ cell.value = tunnelViewModel.interfaceData[field]
+ cell.isKeyInBold = modelRow.isTitleRow()
+ return cell
+ }
+ case .peerFieldRow(let peerData, let field):
+ let cell: KeyValueRow = tableView.dequeueReusableCell()
+ let localizedKeyString = modelRow.isTitleRow() ? modelRow.localizedSectionKeyString() : field.localizedUIString
+ cell.key = tr(format: "macFieldKey (%@)", localizedKeyString)
+ if field == .persistentKeepAlive {
+ cell.value = tr(format: "tunnelPeerPersistentKeepaliveValue (%@)", peerData[field])
+ } else if field == .preSharedKey {
+ cell.value = tr("tunnelPeerPresharedKeyEnabled")
+ } else {
+ cell.value = peerData[field]
+ }
+ cell.isKeyInBold = modelRow.isTitleRow()
+ return cell
+ case .spacerRow:
+ return NSView()
+ case .onDemandRow:
+ let cell: KeyValueRow = tableView.dequeueReusableCell()
+ cell.key = modelRow.localizedSectionKeyString()
+ cell.value = onDemandViewModel.localizedInterfaceDescription
+ cell.isKeyInBold = true
+ return cell
+ case .onDemandSSIDRow:
+ let cell: KeyValueRow = tableView.dequeueReusableCell()
+ cell.key = tr("macFieldOnDemandSSIDs")
+ let value: String
+ if onDemandViewModel.ssidOption == .anySSID {
+ value = onDemandViewModel.ssidOption.localizedUIString
+ } else {
+ value = tr(format: "tunnelOnDemandSSIDOptionDescriptionMac (%1$@: %2$@)",
+ onDemandViewModel.ssidOption.localizedUIString,
+ onDemandViewModel.selectedSSIDs.joined(separator: ", "))
+ }
+ cell.value = value
+ cell.isKeyInBold = false
+ return cell
+ }
+ }
+
+ func statusCell() -> NSView {
+ let cell: KeyValueImageRow = tableView.dequeueReusableCell()
+ cell.key = tr(format: "macFieldKey (%@)", tr("tunnelInterfaceStatus"))
+ cell.value = TunnelDetailTableViewController.localizedStatusDescription(for: tunnel)
+ cell.valueImage = TunnelDetailTableViewController.image(for: tunnel)
+ let changeHandler: (TunnelContainer, Any) -> Void = { [weak cell] tunnel, _ in
+ guard let cell = cell else { return }
+ cell.value = TunnelDetailTableViewController.localizedStatusDescription(for: tunnel)
+ cell.valueImage = TunnelDetailTableViewController.image(for: tunnel)
+ }
+ cell.statusObservationToken = tunnel.observe(\.status, changeHandler: changeHandler)
+ cell.isOnDemandEnabledObservationToken = tunnel.observe(\.isActivateOnDemandEnabled, changeHandler: changeHandler)
+ cell.hasOnDemandRulesObservationToken = tunnel.observe(\.hasOnDemandRules, changeHandler: changeHandler)
+ return cell
+ }
+
+ func toggleStatusCell() -> NSView {
+ let cell: ButtonRow = tableView.dequeueReusableCell()
+ cell.buttonTitle = TunnelDetailTableViewController.localizedToggleStatusActionText(for: tunnel)
+ cell.isButtonEnabled = (tunnel.hasOnDemandRules || tunnel.status == .active || tunnel.status == .inactive)
+ cell.buttonToolTip = tr("macToolTipToggleStatus")
+ cell.onButtonClicked = { [weak self] in
+ self?.handleToggleActiveStatusAction()
+ }
+ let changeHandler: (TunnelContainer, Any) -> Void = { [weak cell] tunnel, _ in
+ guard let cell = cell else { return }
+ cell.buttonTitle = TunnelDetailTableViewController.localizedToggleStatusActionText(for: tunnel)
+ cell.isButtonEnabled = (tunnel.hasOnDemandRules || tunnel.status == .active || tunnel.status == .inactive)
+ }
+ cell.statusObservationToken = tunnel.observe(\.status, changeHandler: changeHandler)
+ cell.isOnDemandEnabledObservationToken = tunnel.observe(\.isActivateOnDemandEnabled, changeHandler: changeHandler)
+ cell.hasOnDemandRulesObservationToken = tunnel.observe(\.hasOnDemandRules, changeHandler: changeHandler)
+ return cell
+ }
+
+ private static func localizedStatusDescription(for tunnel: TunnelContainer) -> String {
+ let status = tunnel.status
+ let isOnDemandEngaged = tunnel.isActivateOnDemandEnabled
+
+ var text: String
+ switch status {
+ case .inactive:
+ text = tr("tunnelStatusInactive")
+ case .activating:
+ text = tr("tunnelStatusActivating")
+ case .active:
+ text = tr("tunnelStatusActive")
+ case .deactivating:
+ text = tr("tunnelStatusDeactivating")
+ case .reasserting:
+ text = tr("tunnelStatusReasserting")
+ case .restarting:
+ text = tr("tunnelStatusRestarting")
+ case .waiting:
+ text = tr("tunnelStatusWaiting")
+ }
+
+ if tunnel.hasOnDemandRules {
+ text += isOnDemandEngaged ?
+ tr("tunnelStatusAddendumOnDemandEnabled") : tr("tunnelStatusAddendumOnDemandDisabled")
+ }
+
+ return text
+ }
+
+ private static func image(for tunnel: TunnelContainer?) -> NSImage? {
+ guard let tunnel = tunnel else { return nil }
+ switch tunnel.status {
+ case .active, .restarting, .reasserting:
+ return NSImage(named: NSImage.statusAvailableName)
+ case .activating, .waiting, .deactivating:
+ return NSImage(named: NSImage.statusPartiallyAvailableName)
+ case .inactive:
+ if tunnel.isActivateOnDemandEnabled {
+ return NSImage(named: NSImage.Name.statusOnDemandEnabled)
+ } else {
+ return NSImage(named: NSImage.statusNoneName)
+ }
+ }
+ }
+
+ private static func localizedToggleStatusActionText(for tunnel: TunnelContainer) -> String {
+ if tunnel.hasOnDemandRules {
+ let turnOn = !tunnel.isActivateOnDemandEnabled
+ if turnOn {
+ return tr("macToggleStatusButtonEnableOnDemand")
+ } else {
+ if tunnel.status == .active {
+ return tr("macToggleStatusButtonDisableOnDemandDeactivate")
+ } else {
+ return tr("macToggleStatusButtonDisableOnDemand")
+ }
+ }
+ } else {
+ switch tunnel.status {
+ case .waiting:
+ return tr("macToggleStatusButtonWaiting")
+ case .inactive:
+ return tr("macToggleStatusButtonActivate")
+ case .activating:
+ return tr("macToggleStatusButtonActivating")
+ case .active:
+ return tr("macToggleStatusButtonDeactivate")
+ case .deactivating:
+ return tr("macToggleStatusButtonDeactivating")
+ case .reasserting:
+ return tr("macToggleStatusButtonReasserting")
+ case .restarting:
+ return tr("macToggleStatusButtonRestarting")
+ }
+ }
+ }
+}
+
+extension TunnelDetailTableViewController: TunnelEditViewControllerDelegate {
+ func tunnelSaved(tunnel: TunnelContainer) {
+ tunnelViewModel = TunnelViewModel(tunnelConfiguration: tunnel.tunnelConfiguration)
+ onDemandViewModel = ActivateOnDemandViewModel(tunnel: tunnel)
+ updateTableViewModelRowsBySection()
+ updateTableViewModelRows()
+ tableView.reloadData()
+ self.tunnelEditVC = nil
+ }
+
+ func tunnelEditingCancelled() {
+ self.tunnelEditVC = nil
+ }
+}