diff --git a/WireGuard/WireGuard.xcodeproj/project.pbxproj b/WireGuard/WireGuard.xcodeproj/project.pbxproj index 0cc3579..a5119b6 100644 --- a/WireGuard/WireGuard.xcodeproj/project.pbxproj +++ b/WireGuard/WireGuard.xcodeproj/project.pbxproj @@ -59,6 +59,7 @@ 6F919EDA218C65C50023B400 /* wireguard_doc_logo_44x58.png in Resources */ = {isa = PBXBuildFile; fileRef = 6F919ED6218C65C50023B400 /* wireguard_doc_logo_44x58.png */; }; 6F919EDB218C65C50023B400 /* wireguard_doc_logo_64x64.png in Resources */ = {isa = PBXBuildFile; fileRef = 6F919ED7218C65C50023B400 /* wireguard_doc_logo_64x64.png */; }; 6F919EDC218C65C50023B400 /* wireguard_doc_logo_320x320.png in Resources */ = {isa = PBXBuildFile; fileRef = 6F919ED8218C65C50023B400 /* wireguard_doc_logo_320x320.png */; }; + 6F9B582921E8D6D100544D02 /* PopupRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F9B582721E8CD4300544D02 /* PopupRow.swift */; }; 6FB1017921C57DE600766195 /* MockTunnels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FB1017821C57DE600766195 /* MockTunnels.swift */; }; 6FB1BD6021D2607A00A991BF /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FB1BD5F21D2607A00A991BF /* AppDelegate.swift */; }; 6FB1BD6221D2607E00A991BF /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 6FB1BD6121D2607E00A991BF /* Assets.xcassets */; }; @@ -267,6 +268,7 @@ 6F919ED6218C65C50023B400 /* wireguard_doc_logo_44x58.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = wireguard_doc_logo_44x58.png; sourceTree = ""; }; 6F919ED7218C65C50023B400 /* wireguard_doc_logo_64x64.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = wireguard_doc_logo_64x64.png; sourceTree = ""; }; 6F919ED8218C65C50023B400 /* wireguard_doc_logo_320x320.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = wireguard_doc_logo_320x320.png; sourceTree = ""; }; + 6F9B582721E8CD4300544D02 /* PopupRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PopupRow.swift; sourceTree = ""; }; 6FB1017821C57DE600766195 /* MockTunnels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockTunnels.swift; sourceTree = ""; }; 6FB1BD5D21D2607A00A991BF /* WireGuard.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = WireGuard.app; sourceTree = BUILT_PRODUCTS_DIR; }; 6FB1BD5F21D2607A00A991BF /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; @@ -387,6 +389,7 @@ 6F613D9A21DE33B8004B217A /* KeyValueRow.swift */, 5F52D0BA21E3781B00283CEA /* ConfTextView.swift */, 5F52D0BC21E3785C00283CEA /* ConfTextStorage.swift */, + 6F9B582721E8CD4300544D02 /* PopupRow.swift */, ); path = View; sourceTree = ""; @@ -1103,6 +1106,7 @@ 6FCD99B121E0EDA900BA4C82 /* TunnelEditViewController.swift in Sources */, 6FB1BDCA21D50F1700A991BF /* x25519.c in Sources */, 6FB1BDCB21D50F1700A991BF /* Curve25519.swift in Sources */, + 6F9B582921E8D6D100544D02 /* PopupRow.swift in Sources */, 6FB1BDBB21D50F0200A991BF /* Localizable.strings in Sources */, 6FB1BDBC21D50F0200A991BF /* ringlogger.c in Sources */, 6FB1BDBD21D50F0200A991BF /* ringlogger.h in Sources */, diff --git a/WireGuard/WireGuard/Base.lproj/Localizable.strings b/WireGuard/WireGuard/Base.lproj/Localizable.strings index 369cfcc..cf73bf3 100644 --- a/WireGuard/WireGuard/Base.lproj/Localizable.strings +++ b/WireGuard/WireGuard/Base.lproj/Localizable.strings @@ -255,6 +255,7 @@ // Mac detail/edit view fields "macFieldKey (%@)" = "%@:"; +"macFieldOnDemand" = "On-Demand:"; // Mac status display diff --git a/WireGuard/WireGuard/UI/macOS/View/PopupRow.swift b/WireGuard/WireGuard/UI/macOS/View/PopupRow.swift new file mode 100644 index 0000000..2ef55f5 --- /dev/null +++ b/WireGuard/WireGuard/UI/macOS/View/PopupRow.swift @@ -0,0 +1,75 @@ +// SPDX-License-Identifier: MIT +// Copyright © 2018 WireGuard LLC. All Rights Reserved. + +import Cocoa + +class PopupRow: NSView { + let keyLabel: NSTextField = { + let keyLabel = NSTextField() + keyLabel.isEditable = false + keyLabel.isSelectable = false + keyLabel.isBordered = false + keyLabel.alignment = .right + keyLabel.maximumNumberOfLines = 1 + keyLabel.lineBreakMode = .byTruncatingTail + keyLabel.backgroundColor = .clear + return keyLabel + }() + + let valuePopup = NSPopUpButton() + + var key: String { + get { return keyLabel.stringValue } + set(value) { keyLabel.stringValue = value } + } + + var valueOptions: [String] { + get { return valuePopup.itemTitles } + set(value) { + valuePopup.removeAllItems() + valuePopup.addItems(withTitles: value) + } + } + + var selectedOptionIndex: Int { + get { return valuePopup.indexOfSelectedItem } + set(value) { valuePopup.selectItem(at: value) } + } + + override var intrinsicContentSize: NSSize { + let height = max(keyLabel.intrinsicContentSize.height, valuePopup.intrinsicContentSize.height) + return NSSize(width: NSView.noIntrinsicMetric, height: height) + } + + init() { + super.init(frame: CGRect.zero) + + addSubview(keyLabel) + addSubview(valuePopup) + keyLabel.translatesAutoresizingMaskIntoConstraints = false + valuePopup.translatesAutoresizingMaskIntoConstraints = false + + NSLayoutConstraint.activate([ + keyLabel.centerYAnchor.constraint(equalTo: self.centerYAnchor), + keyLabel.firstBaselineAnchor.constraint(equalTo: valuePopup.firstBaselineAnchor), + self.leadingAnchor.constraint(equalTo: keyLabel.leadingAnchor), + keyLabel.trailingAnchor.constraint(equalTo: valuePopup.leadingAnchor, constant: -5) + ]) + + keyLabel.setContentCompressionResistancePriority(.defaultHigh + 2, for: .horizontal) + keyLabel.setContentHuggingPriority(.defaultHigh, for: .horizontal) + + let widthConstraint = keyLabel.widthAnchor.constraint(equalToConstant: 150) + widthConstraint.priority = .defaultHigh + 1 + widthConstraint.isActive = true + } + + required init?(coder decoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func prepareForReuse() { + key = "" + valueOptions = [] + } +} diff --git a/WireGuard/WireGuard/UI/macOS/ViewController/TunnelDetailTableViewController.swift b/WireGuard/WireGuard/UI/macOS/ViewController/TunnelDetailTableViewController.swift index 3c07060..5f6c891 100644 --- a/WireGuard/WireGuard/UI/macOS/ViewController/TunnelDetailTableViewController.swift +++ b/WireGuard/WireGuard/UI/macOS/ViewController/TunnelDetailTableViewController.swift @@ -8,12 +8,14 @@ class TunnelDetailTableViewController: NSViewController { private enum TableViewModelRow { case interfaceFieldRow(TunnelViewModel.InterfaceField) case peerFieldRow(peer: TunnelViewModel.PeerData, field: TunnelViewModel.PeerField) + case onDemandRow case spacerRow func localizedSectionKeyString() -> String { switch self { case .interfaceFieldRow: return tr("tunnelSectionTitleInterface") case .peerFieldRow: return tr("tunnelSectionTitlePeer") + case .onDemandRow: return "" case .spacerRow: return "" } } @@ -22,6 +24,7 @@ class TunnelDetailTableViewController: NSViewController { switch self { case .interfaceFieldRow(let field): return field == .name case .peerFieldRow(_, let field): return field == .publicKey + case .onDemandRow: return true case .spacerRow: return false } } @@ -163,6 +166,8 @@ class TunnelDetailTableViewController: NSViewController { tableViewModelRows.append(.peerFieldRow(peer: peerData, field: field)) } } + tableViewModelRows.append(.spacerRow) + tableViewModelRows.append(.onDemandRow) } func updateStatus() { @@ -232,6 +237,12 @@ extension TunnelDetailTableViewController: NSTableViewDelegate { return cell case .spacerRow: return NSView() + case .onDemandRow: + let cell: KeyValueRow = tableView.dequeueReusableCell() + cell.key = tr("macFieldOnDemand") + cell.value = TunnelViewModel.activateOnDemandDetailText(for: tunnel.activateOnDemandSetting) + cell.isKeyInBold = true + return cell } } } diff --git a/WireGuard/WireGuard/UI/macOS/ViewController/TunnelEditViewController.swift b/WireGuard/WireGuard/UI/macOS/ViewController/TunnelEditViewController.swift index ae95fba..aecb3c9 100644 --- a/WireGuard/WireGuard/UI/macOS/ViewController/TunnelEditViewController.swift +++ b/WireGuard/WireGuard/UI/macOS/ViewController/TunnelEditViewController.swift @@ -41,6 +41,12 @@ class TunnelEditViewController: NSViewController { return textView }() + let onDemandRow: PopupRow = { + let popupRow = PopupRow() + popupRow.key = tr("macFieldOnDemand") + return popupRow + }() + let scrollView: NSScrollView = { let scrollView = NSScrollView() scrollView.hasVerticalScroller = true @@ -64,6 +70,13 @@ class TunnelEditViewController: NSViewController { return button }() + let activateOnDemandOptions: [ActivateOnDemandOption] = [ + .none, + .useOnDemandOverWiFiOrEthernet, + .useOnDemandOverWiFiOnly, + .useOnDemandOverEthernetOnly + ] + let tunnelsManager: TunnelsManager let tunnel: TunnelContainer? @@ -82,6 +95,7 @@ class TunnelEditViewController: NSViewController { } func populateTextFields() { + let selectedActivateOnDemandOption: ActivateOnDemandOption if let tunnel = tunnel { // Editing an existing tunnel let tunnelConfiguration = tunnel.tunnelConfiguration! @@ -99,6 +113,11 @@ class TunnelEditViewController: NSViewController { publicKeyRow?.value = "" } } + if tunnel.activateOnDemandSetting.isActivateOnDemandEnabled { + selectedActivateOnDemandOption = tunnel.activateOnDemandSetting.activateOnDemandOption + } else { + selectedActivateOnDemandOption = .none + } } else { // Creating a new tunnel let privateKey = Curve25519.generatePrivateKey() @@ -109,7 +128,13 @@ class TunnelEditViewController: NSViewController { """ publicKeyRow.value = publicKey.base64EncodedString() textView.string = bootstrappingText + selectedActivateOnDemandOption = .none } + + onDemandRow.valueOptions = activateOnDemandOptions.map { + return TunnelViewModel.activateOnDemandOptionText(for: $0) + } + onDemandRow.selectedOptionIndex = activateOnDemandOptions.firstIndex(of: selectedActivateOnDemandOption)! } override func loadView() { @@ -126,7 +151,7 @@ class TunnelEditViewController: NSViewController { let margin: CGFloat = 20 let internalSpacing: CGFloat = 10 - let editorStackView = NSStackView(views: [nameRow, publicKeyRow, scrollView]) + let editorStackView = NSStackView(views: [nameRow, publicKeyRow, onDemandRow, scrollView]) editorStackView.orientation = .vertical editorStackView.setHuggingPriority(.defaultHigh, for: .horizontal) editorStackView.spacing = internalSpacing @@ -157,6 +182,13 @@ class TunnelEditViewController: NSViewController { ErrorPresenter.showErrorAlert(title: tr("macAlertNameIsEmpty"), message: "", from: self) return } + let onDemandSetting: ActivateOnDemandSetting + let onDemandOption = activateOnDemandOptions[onDemandRow.selectedOptionIndex] + if onDemandOption == .none { + onDemandSetting = ActivateOnDemandSetting.defaultSetting + } else { + onDemandSetting = ActivateOnDemandSetting(isActivateOnDemandEnabled: true, activateOnDemandOption: onDemandOption) + } if let tunnel = tunnel { // We're modifying an existing tunnel if name != tunnel.name && tunnelsManager.tunnel(named: name) != nil { @@ -165,7 +197,6 @@ class TunnelEditViewController: NSViewController { } do { let tunnelConfiguration = try TunnelConfiguration(fromWgQuickConfig: textView.string, called: nameRow.value) - let onDemandSetting = ActivateOnDemandSetting.defaultSetting tunnelsManager.modify(tunnel: tunnel, tunnelConfiguration: tunnelConfiguration, activateOnDemandSetting: onDemandSetting) { [weak self] error in if let error = error { ErrorPresenter.showErrorAlert(error: error, from: self) @@ -187,7 +218,6 @@ class TunnelEditViewController: NSViewController { } do { let tunnelConfiguration = try TunnelConfiguration(fromWgQuickConfig: textView.string, called: nameRow.value) - let onDemandSetting = ActivateOnDemandSetting.defaultSetting tunnelsManager.add(tunnelConfiguration: tunnelConfiguration, activateOnDemandSetting: onDemandSetting) { [weak self] result in if let error = result.error { ErrorPresenter.showErrorAlert(error: error, from: self)