diff --git a/WireGuard/WireGuard.xcodeproj/project.pbxproj b/WireGuard/WireGuard.xcodeproj/project.pbxproj index f4c2a31..783efae 100644 --- a/WireGuard/WireGuard.xcodeproj/project.pbxproj +++ b/WireGuard/WireGuard.xcodeproj/project.pbxproj @@ -26,6 +26,9 @@ 5FF7B96321CC95DE00A7DD74 /* InterfaceConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5FF7B96121CC95DE00A7DD74 /* InterfaceConfiguration.swift */; }; 5FF7B96521CC95FA00A7DD74 /* PeerConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5FF7B96421CC95FA00A7DD74 /* PeerConfiguration.swift */; }; 5FF7B96621CC95FA00A7DD74 /* PeerConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5FF7B96421CC95FA00A7DD74 /* PeerConfiguration.swift */; }; + 6F4DD16B21DA558800690EAE /* TunnelListCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F4DD16A21DA558800690EAE /* TunnelListCell.swift */; }; + 6F4DD16C21DA558F00690EAE /* NSTableView+Reuse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F4DD16721DA552B00690EAE /* NSTableView+Reuse.swift */; }; + 6F4DD16E21DBEA0700690EAE /* ManageTunnelsRootViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F4DD16D21DBEA0700690EAE /* ManageTunnelsRootViewController.swift */; }; 6F5A2B4621AFDED40081EDD8 /* FileManager+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F5A2B4421AFDE020081EDD8 /* FileManager+Extension.swift */; }; 6F5A2B4821AFF49A0081EDD8 /* FileManager+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F5A2B4421AFDE020081EDD8 /* FileManager+Extension.swift */; }; 6F5D0C1D218352EF000F85AD /* PacketTunnelProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F5D0C1C218352EF000F85AD /* PacketTunnelProvider.swift */; }; @@ -112,6 +115,7 @@ 6FBA103F21D6B6FF0051C35F /* TunnelImporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FBA103D21D6B6D70051C35F /* TunnelImporter.swift */; }; 6FBA104021D6B7040051C35F /* ErrorPresenterProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FBA103A21D6B4280051C35F /* ErrorPresenterProtocol.swift */; }; 6FBA104321D6BC250051C35F /* ErrorPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FBA104121D6BC210051C35F /* ErrorPresenter.swift */; }; + 6FBA104621D7EBFA0051C35F /* TunnelsListTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FBA104521D7EBFA0051C35F /* TunnelsListTableViewController.swift */; }; 6FDEF7E421846C1A00D8FBF6 /* libwg-go.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 6FDEF7E321846C1A00D8FBF6 /* libwg-go.a */; }; 6FDEF7E62185EFB200D8FBF6 /* QRScanViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FDEF7E52185EFAF00D8FBF6 /* QRScanViewController.swift */; }; 6FDEF7FB21863B6100D8FBF6 /* unzip.c in Sources */ = {isa = PBXBuildFile; fileRef = 6FDEF7F621863B6100D8FBF6 /* unzip.c */; }; @@ -212,6 +216,9 @@ 5F9696AF21CD7128008063FE /* TunnelConfiguration+WgQuickConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TunnelConfiguration+WgQuickConfig.swift"; sourceTree = ""; }; 5FF7B96121CC95DE00A7DD74 /* InterfaceConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InterfaceConfiguration.swift; sourceTree = ""; }; 5FF7B96421CC95FA00A7DD74 /* PeerConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PeerConfiguration.swift; sourceTree = ""; }; + 6F4DD16721DA552B00690EAE /* NSTableView+Reuse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSTableView+Reuse.swift"; sourceTree = ""; }; + 6F4DD16A21DA558800690EAE /* TunnelListCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelListCell.swift; sourceTree = ""; }; + 6F4DD16D21DBEA0700690EAE /* ManageTunnelsRootViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ManageTunnelsRootViewController.swift; sourceTree = ""; }; 6F5A2B4421AFDE020081EDD8 /* FileManager+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FileManager+Extension.swift"; sourceTree = ""; }; 6F5D0C1421832391000F85AD /* DNSResolver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DNSResolver.swift; sourceTree = ""; }; 6F5D0C1A218352EF000F85AD /* WireGuardNetworkExtensioniOS.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = WireGuardNetworkExtensioniOS.appex; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -258,6 +265,7 @@ 6FBA103A21D6B4280051C35F /* ErrorPresenterProtocol.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ErrorPresenterProtocol.swift; sourceTree = ""; }; 6FBA103D21D6B6D70051C35F /* TunnelImporter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TunnelImporter.swift; sourceTree = ""; }; 6FBA104121D6BC210051C35F /* ErrorPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorPresenter.swift; sourceTree = ""; }; + 6FBA104521D7EBFA0051C35F /* TunnelsListTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelsListTableViewController.swift; sourceTree = ""; }; 6FDEF7E321846C1A00D8FBF6 /* libwg-go.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; path = "libwg-go.a"; sourceTree = BUILT_PRODUCTS_DIR; }; 6FDEF7E52185EFAF00D8FBF6 /* QRScanViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = QRScanViewController.swift; sourceTree = ""; }; 6FDEF7F621863B6100D8FBF6 /* unzip.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = unzip.c; sourceTree = ""; }; @@ -350,6 +358,14 @@ path = ViewController; sourceTree = ""; }; + 6F4DD16921DA556600690EAE /* View */ = { + isa = PBXGroup; + children = ( + 6F4DD16A21DA558800690EAE /* TunnelListCell.swift */, + ); + path = View; + sourceTree = ""; + }; 6F5D0C1B218352EF000F85AD /* WireGuardNetworkExtension */ = { isa = PBXGroup; children = ( @@ -464,6 +480,8 @@ 6FB1BD5E21D2607A00A991BF /* macOS */ = { isa = PBXGroup; children = ( + 6F4DD16921DA556600690EAE /* View */, + 6FBA104421D7EA750051C35F /* ViewController */, 6FBA101321D613F30051C35F /* Application.swift */, 6FB1BD5F21D2607A00A991BF /* AppDelegate.swift */, 6FBA101621D655340051C35F /* StatusMenu.swift */, @@ -471,10 +489,20 @@ 6FB1BD6121D2607E00A991BF /* Assets.xcassets */, 6FB1BD6621D2607E00A991BF /* Info.plist */, 6FB1BD6721D2607E00A991BF /* WireGuard.entitlements */, + 6F4DD16721DA552B00690EAE /* NSTableView+Reuse.swift */, ); path = macOS; sourceTree = ""; }; + 6FBA104421D7EA750051C35F /* ViewController */ = { + isa = PBXGroup; + children = ( + 6FBA104521D7EBFA0051C35F /* TunnelsListTableViewController.swift */, + 6F4DD16D21DBEA0700690EAE /* ManageTunnelsRootViewController.swift */, + ); + path = ViewController; + sourceTree = ""; + }; 6FDEF7E72186320E00D8FBF6 /* ZipArchive */ = { isa = PBXGroup; children = ( @@ -1021,11 +1049,14 @@ 6FB1BDD021D50F5300A991BF /* TunnelErrors.swift in Sources */, 6FB1BDD121D50F5300A991BF /* ZipImporter.swift in Sources */, 6FB1BDD221D50F5300A991BF /* ZipExporter.swift in Sources */, + 6FBA104621D7EBFA0051C35F /* TunnelsListTableViewController.swift in Sources */, 6FB1BDD321D50F5300A991BF /* ZipArchive.swift in Sources */, 6FB1BDD421D50F5300A991BF /* ioapi.c in Sources */, 6FB1BDD521D50F5300A991BF /* unzip.c in Sources */, 6FB1BDD621D50F5300A991BF /* zip.c in Sources */, 6FB1BDD721D50F5300A991BF /* WireGuardAppError.swift in Sources */, + 6F4DD16E21DBEA0700690EAE /* ManageTunnelsRootViewController.swift in Sources */, + 6F4DD16C21DA558F00690EAE /* NSTableView+Reuse.swift in Sources */, 6FB1BDD821D50F5300A991BF /* WireGuardResult.swift in Sources */, 6FB1BDD921D50F5300A991BF /* LocalizationHelper.swift in Sources */, 6FB1BDCA21D50F1700A991BF /* x25519.c in Sources */, @@ -1034,6 +1065,7 @@ 6FB1BDBC21D50F0200A991BF /* ringlogger.c in Sources */, 6FB1BDBD21D50F0200A991BF /* ringlogger.h in Sources */, 6FBA103F21D6B6FF0051C35F /* TunnelImporter.swift in Sources */, + 6F4DD16B21DA558800690EAE /* TunnelListCell.swift in Sources */, 6FB1BDBE21D50F0200A991BF /* Logger.swift in Sources */, 6FB1BDBF21D50F0200A991BF /* TunnelConfiguration+WgQuickConfig.swift in Sources */, 6FB1BDC021D50F0200A991BF /* NETunnelProviderProtocol+Extension.swift in Sources */, diff --git a/WireGuard/WireGuard/UI/macOS/NSTableView+Reuse.swift b/WireGuard/WireGuard/UI/macOS/NSTableView+Reuse.swift new file mode 100644 index 0000000..3a36b6a --- /dev/null +++ b/WireGuard/WireGuard/UI/macOS/NSTableView+Reuse.swift @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: MIT +// Copyright © 2018 WireGuard LLC. All Rights Reserved. + +import Cocoa + +extension NSTableView { + func dequeueReusableCell() -> T { + let identifier = NSUserInterfaceItemIdentifier(NSStringFromClass(T.self)) + if let cellView = makeView(withIdentifier: identifier, owner: self) { + //swiftlint:disable:next force_cast + return cellView as! T + } + let cellView = T() + cellView.identifier = identifier + return cellView + } +} diff --git a/WireGuard/WireGuard/UI/macOS/StatusMenu.swift b/WireGuard/WireGuard/UI/macOS/StatusMenu.swift index a41cea4..a539073 100644 --- a/WireGuard/WireGuard/UI/macOS/StatusMenu.swift +++ b/WireGuard/WireGuard/UI/macOS/StatusMenu.swift @@ -116,7 +116,11 @@ class StatusMenu: NSMenu { } @objc func manageTunnelsClicked() { - print("Unimplemented") + let manageTunnelsRootVC = ManageTunnelsRootViewController(tunnelsManager: tunnelsManager) + let window = NSWindow(contentViewController: manageTunnelsRootVC) + window.setFrameAutosaveName(NSWindow.FrameAutosaveName("ManageTunnelsWindow")) // Auto-save window position and size + NSApp.activate(ignoringOtherApps: true) + window.makeKeyAndOrderFront(self) } @objc func importTunnelsClicked() { diff --git a/WireGuard/WireGuard/UI/macOS/View/TunnelListCell.swift b/WireGuard/WireGuard/UI/macOS/View/TunnelListCell.swift new file mode 100644 index 0000000..11bf73c --- /dev/null +++ b/WireGuard/WireGuard/UI/macOS/View/TunnelListCell.swift @@ -0,0 +1,75 @@ +// SPDX-License-Identifier: MIT +// Copyright © 2018 WireGuard LLC. All Rights Reserved. + +import Cocoa + +class TunnelListCell: NSView { + var tunnel: TunnelContainer? { + didSet(value) { + // Bind to the tunnel's name + nameLabel.stringValue = tunnel?.name ?? "" + nameObservationToken = tunnel?.observe(\TunnelContainer.name) { [weak self] tunnel, _ in + self?.nameLabel.stringValue = tunnel.name + } + // Bind to the tunnel's status + statusImageView.image = TunnelListCell.image(for: tunnel?.status) + statusObservationToken = tunnel?.observe(\TunnelContainer.status) { [weak self] tunnel, _ in + self?.statusImageView.image = TunnelListCell.image(for: tunnel.status) + } + } + } + + let nameLabel: NSTextField = { + let nameLabel = NSTextField() + nameLabel.isEditable = false + nameLabel.isSelectable = false + nameLabel.isBordered = false + nameLabel.maximumNumberOfLines = 1 + nameLabel.lineBreakMode = .byTruncatingTail + return nameLabel + }() + + let statusImageView = NSImageView() + + private var statusObservationToken: AnyObject? + private var nameObservationToken: AnyObject? + + init() { + super.init(frame: CGRect.zero) + + addSubview(statusImageView) + addSubview(nameLabel) + statusImageView.translatesAutoresizingMaskIntoConstraints = false + nameLabel.translatesAutoresizingMaskIntoConstraints = false + nameLabel.backgroundColor = .clear + NSLayoutConstraint.activate([ + self.leadingAnchor.constraint(equalTo: statusImageView.leadingAnchor), + statusImageView.trailingAnchor.constraint(equalTo: nameLabel.leadingAnchor), + statusImageView.widthAnchor.constraint(equalToConstant: 20), + nameLabel.trailingAnchor.constraint(equalTo: self.trailingAnchor), + statusImageView.centerYAnchor.constraint(equalTo: self.centerYAnchor), + nameLabel.centerYAnchor.constraint(equalTo: self.centerYAnchor) + ]) + } + + required init?(coder decoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + static func image(for status: TunnelStatus?) -> NSImage? { + guard let status = status else { return nil } + switch status { + case .active, .restarting, .reasserting: + return NSImage(named: NSImage.statusAvailableName) + case .activating, .waiting: + return NSImage(named: NSImage.statusPartiallyAvailableName) + case .deactivating, .inactive: + return nil + } + } + + override func prepareForReuse() { + nameLabel.stringValue = "" + statusImageView.image = nil + } +} diff --git a/WireGuard/WireGuard/UI/macOS/ViewController/ManageTunnelsRootViewController.swift b/WireGuard/WireGuard/UI/macOS/ViewController/ManageTunnelsRootViewController.swift new file mode 100644 index 0000000..e63235a --- /dev/null +++ b/WireGuard/WireGuard/UI/macOS/ViewController/ManageTunnelsRootViewController.swift @@ -0,0 +1,56 @@ +// SPDX-License-Identifier: MIT +// Copyright © 2018 WireGuard LLC. All Rights Reserved. + +import Cocoa + +class ManageTunnelsRootViewController: NSViewController { + + let tunnelsManager: TunnelsManager + + init(tunnelsManager: TunnelsManager) { + self.tunnelsManager = tunnelsManager + super.init(nibName: nil, bundle: nil) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func loadView() { + view = NSView() + + let horizontalSpacing: CGFloat = 30 + let verticalSpacing: CGFloat = 20 + + let container = NSLayoutGuide() + view.addLayoutGuide(container) + NSLayoutConstraint.activate([ + container.topAnchor.constraint(equalTo: view.topAnchor, constant: verticalSpacing), + view.bottomAnchor.constraint(equalTo: container.bottomAnchor, constant: verticalSpacing), + container.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: horizontalSpacing), + view.trailingAnchor.constraint(equalTo: container.trailingAnchor, constant: horizontalSpacing) + ]) + + let tunnelsListVC = TunnelsListTableViewController(tunnelsManager: tunnelsManager) + let tunnelsListView = tunnelsListVC.view + let tunnelDetailView = NSView() + tunnelDetailView.wantsLayer = true + tunnelDetailView.layer?.backgroundColor = NSColor.gray.cgColor + + addChild(tunnelsListVC) + view.addSubview(tunnelsListView) + view.addSubview(tunnelDetailView) + + tunnelsListView.translatesAutoresizingMaskIntoConstraints = false + tunnelDetailView.translatesAutoresizingMaskIntoConstraints = false + + NSLayoutConstraint.activate([ + tunnelsListView.topAnchor.constraint(equalTo: container.topAnchor), + tunnelsListView.bottomAnchor.constraint(equalTo: container.bottomAnchor), + tunnelsListView.leadingAnchor.constraint(equalTo: container.leadingAnchor), + tunnelDetailView.leadingAnchor.constraint(equalTo: tunnelsListView.trailingAnchor, constant: horizontalSpacing), + tunnelDetailView.trailingAnchor.constraint(equalTo: container.trailingAnchor), + tunnelsListView.widthAnchor.constraint(equalTo: container.widthAnchor, multiplier: 0.3) + ]) + } +} diff --git a/WireGuard/WireGuard/UI/macOS/ViewController/TunnelsListTableViewController.swift b/WireGuard/WireGuard/UI/macOS/ViewController/TunnelsListTableViewController.swift new file mode 100644 index 0000000..47658d6 --- /dev/null +++ b/WireGuard/WireGuard/UI/macOS/ViewController/TunnelsListTableViewController.swift @@ -0,0 +1,55 @@ +// SPDX-License-Identifier: MIT +// Copyright © 2018 WireGuard LLC. All Rights Reserved. + +import Cocoa + +class TunnelsListTableViewController: NSViewController { + + let tunnelsManager: TunnelsManager + + let tableView: NSTableView = { + let tableView = NSTableView() + tableView.addTableColumn(NSTableColumn(identifier: NSUserInterfaceItemIdentifier("TunnelsList"))) + tableView.headerView = nil + tableView.rowSizeStyle = .medium + return tableView + }() + + init(tunnelsManager: TunnelsManager) { + self.tunnelsManager = tunnelsManager + super.init(nibName: nil, bundle: nil) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func loadView() { + tableView.dataSource = self + tableView.delegate = self + + let scrollView = NSScrollView() + scrollView.hasVerticalScroller = true + scrollView.autohidesScrollers = true + + let clipView = NSClipView() + clipView.documentView = tableView + scrollView.contentView = clipView + + self.view = scrollView + } +} + +extension TunnelsListTableViewController: NSTableViewDataSource { + func numberOfRows(in tableView: NSTableView) -> Int { + return tunnelsManager.numberOfTunnels() + } +} + +extension TunnelsListTableViewController: NSTableViewDelegate { + func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? { + let cell: TunnelListCell = tableView.dequeueReusableCell() + cell.tunnel = tunnelsManager.tunnel(at: row) + return cell + } +}