UI: More elegant copy-to-clipboard behavior
This commit is contained in:
parent
264adcdc9a
commit
34b9023f67
|
@ -7,6 +7,7 @@
|
|||
objects = {
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
6BB8400421892C920003598F /* CopyableLabelTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6BB8400321892C920003598F /* CopyableLabelTableViewCell.swift */; };
|
||||
6F5D0C1521832391000F85AD /* DNSResolver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F5D0C1421832391000F85AD /* DNSResolver.swift */; };
|
||||
6F5D0C1D218352EF000F85AD /* PacketTunnelProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F5D0C1C218352EF000F85AD /* PacketTunnelProvider.swift */; };
|
||||
6F5D0C22218352EF000F85AD /* WireGuardNetworkExtension.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = 6F5D0C1A218352EF000F85AD /* WireGuardNetworkExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
|
||||
|
@ -72,6 +73,7 @@
|
|||
/* End PBXCopyFilesBuildPhase section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
6BB8400321892C920003598F /* CopyableLabelTableViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CopyableLabelTableViewCell.swift; sourceTree = "<group>"; };
|
||||
6F5D0C1421832391000F85AD /* DNSResolver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DNSResolver.swift; sourceTree = "<group>"; };
|
||||
6F5D0C1A218352EF000F85AD /* WireGuardNetworkExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = WireGuardNetworkExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
6F5D0C1C218352EF000F85AD /* PacketTunnelProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PacketTunnelProvider.swift; sourceTree = "<group>"; };
|
||||
|
@ -186,6 +188,7 @@
|
|||
6F7774DE217181B1006A79B3 /* iOS */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
6BB8400321892C920003598F /* CopyableLabelTableViewCell.swift */,
|
||||
6FDEF7E52185EFAF00D8FBF6 /* QRScanViewController.swift */,
|
||||
6F7774E0217181B1006A79B3 /* AppDelegate.swift */,
|
||||
6F7774DF217181B1006A79B3 /* MainViewController.swift */,
|
||||
|
@ -439,6 +442,7 @@
|
|||
6F7774EF21722D97006A79B3 /* TunnelsManager.swift in Sources */,
|
||||
6F5D0C1521832391000F85AD /* DNSResolver.swift in Sources */,
|
||||
6F5D0C482183C6A3000F85AD /* PacketTunnelOptionsGenerator.swift in Sources */,
|
||||
6BB8400421892C920003598F /* CopyableLabelTableViewCell.swift in Sources */,
|
||||
6F693A562179E556008551C1 /* Endpoint.swift in Sources */,
|
||||
6FDEF7E62185EFB200D8FBF6 /* QRScanViewController.swift in Sources */,
|
||||
6F6899A62180447E0012E523 /* x25519.c in Sources */,
|
||||
|
|
|
@ -10,7 +10,6 @@ class TunnelViewModel {
|
|||
case privateKey = "Private key"
|
||||
case publicKey = "Public key"
|
||||
case generateKeyPair = "Generate keypair"
|
||||
case copyPublicKey = "Copy public key"
|
||||
case addresses = "Addresses"
|
||||
case listenPort = "Listen port"
|
||||
case mtu = "MTU"
|
||||
|
@ -18,7 +17,7 @@ class TunnelViewModel {
|
|||
}
|
||||
|
||||
static let interfaceFieldsWithControl: Set<InterfaceField> = [
|
||||
.generateKeyPair, .copyPublicKey
|
||||
.generateKeyPair
|
||||
]
|
||||
|
||||
enum PeerField: String {
|
||||
|
|
|
@ -0,0 +1,51 @@
|
|||
// SPDX-License-Identifier: MIT
|
||||
// Copyright © 2018 WireGuard LLC. All Rights Reserved.
|
||||
|
||||
import UIKit
|
||||
|
||||
class CopyableLabelTableViewCell: UITableViewCell {
|
||||
var copyableGesture = true
|
||||
|
||||
required init?(coder aDecoder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
|
||||
super.init(style: style, reuseIdentifier: reuseIdentifier)
|
||||
let gestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(handleTapGesture(_:)))
|
||||
self.addGestureRecognizer(gestureRecognizer)
|
||||
self.isUserInteractionEnabled = true
|
||||
}
|
||||
|
||||
// MARK: - UIGestureRecognizer
|
||||
@objc func handleTapGesture(_ recognizer: UIGestureRecognizer) {
|
||||
if !self.copyableGesture {
|
||||
return
|
||||
}
|
||||
guard recognizer.state == .recognized else { return }
|
||||
|
||||
if let recognizerView = recognizer.view,
|
||||
let recognizerSuperView = recognizerView.superview, recognizerView.becomeFirstResponder() {
|
||||
let menuController = UIMenuController.shared
|
||||
menuController.setTargetRect(self.detailTextLabel?.frame ?? recognizerView.frame, in: self.detailTextLabel?.superview ?? recognizerSuperView)
|
||||
menuController.setMenuVisible(true, animated: true)
|
||||
}
|
||||
}
|
||||
|
||||
override var canBecomeFirstResponder: Bool {
|
||||
return true
|
||||
}
|
||||
|
||||
override func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool {
|
||||
return (action == #selector(UIResponderStandardEditActions.copy(_:)))
|
||||
}
|
||||
|
||||
override func copy(_ sender: Any?) {
|
||||
UIPasteboard.general.string = self.detailTextLabel?.text
|
||||
}
|
||||
|
||||
override func prepareForReuse() {
|
||||
super.prepareForReuse()
|
||||
self.copyableGesture = true
|
||||
}
|
||||
}
|
|
@ -7,10 +7,9 @@ import UIKit
|
|||
|
||||
class TunnelDetailTableViewController: UITableViewController {
|
||||
|
||||
let interfaceFieldsBySection: [[TunnelViewModel.InterfaceField]] = [
|
||||
[.name],
|
||||
[.publicKey, .copyPublicKey],
|
||||
[.addresses, .listenPort, .mtu, .dns]
|
||||
let interfaceFields: [TunnelViewModel.InterfaceField] = [
|
||||
.name, .publicKey, .addresses,
|
||||
.listenPort, .mtu, .dns
|
||||
]
|
||||
|
||||
let peerFields: [TunnelViewModel.PeerField] = [
|
||||
|
@ -95,32 +94,22 @@ extension TunnelDetailTableViewController: TunnelEditTableViewControllerDelegate
|
|||
|
||||
extension TunnelDetailTableViewController {
|
||||
override func numberOfSections(in tableView: UITableView) -> Int {
|
||||
let interfaceData = tunnelViewModel.interfaceData
|
||||
let numberOfInterfaceSections = (0 ..< interfaceFieldsBySection.count).filter { section in
|
||||
(!interfaceData.filterFieldsWithValueOrControl(interfaceFields: interfaceFieldsBySection[section]).isEmpty)
|
||||
}.count
|
||||
let numberOfPeerSections = tunnelViewModel.peersData.count
|
||||
|
||||
return 1 + numberOfInterfaceSections + numberOfPeerSections + 1
|
||||
return 3 + tunnelViewModel.peersData.count
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
||||
let interfaceData = tunnelViewModel.interfaceData
|
||||
let numberOfInterfaceSections = (0 ..< interfaceFieldsBySection.count).filter { section in
|
||||
(!interfaceData.filterFieldsWithValueOrControl(interfaceFields: interfaceFieldsBySection[section]).isEmpty)
|
||||
}.count
|
||||
let numberOfPeerSections = tunnelViewModel.peersData.count
|
||||
|
||||
if (section == 0) {
|
||||
// Status
|
||||
return 1
|
||||
} else if (section < (1 + numberOfInterfaceSections)) {
|
||||
} else if (section == 1) {
|
||||
// Interface
|
||||
return interfaceData.filterFieldsWithValueOrControl(interfaceFields: interfaceFieldsBySection[section - 1]).count
|
||||
} else if ((numberOfPeerSections > 0) && (section < (1 + numberOfInterfaceSections + numberOfPeerSections))) {
|
||||
return interfaceData.filterFieldsWithValueOrControl(interfaceFields: interfaceFields).count
|
||||
} else if ((numberOfPeerSections > 0) && (section < (2 + numberOfPeerSections))) {
|
||||
// Peer
|
||||
let peerIndex = (section - numberOfInterfaceSections - 1)
|
||||
let peerData = tunnelViewModel.peersData[peerIndex]
|
||||
let peerData = tunnelViewModel.peersData[section - 2]
|
||||
return peerData.filterFieldsWithValueOrControl(peerFields: peerFields).count
|
||||
} else {
|
||||
// Delete tunnel
|
||||
|
@ -129,32 +118,25 @@ extension TunnelDetailTableViewController {
|
|||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
|
||||
let interfaceData = tunnelViewModel.interfaceData
|
||||
let numberOfInterfaceSections = (0 ..< interfaceFieldsBySection.count).filter { section in
|
||||
(!interfaceData.filterFieldsWithValueOrControl(interfaceFields: interfaceFieldsBySection[section]).isEmpty)
|
||||
}.count
|
||||
let numberOfPeerSections = tunnelViewModel.peersData.count
|
||||
|
||||
if (section == 0) {
|
||||
// Status
|
||||
return "Status"
|
||||
} else if (section < 1 + numberOfInterfaceSections) {
|
||||
} else if (section == 1) {
|
||||
// Interface
|
||||
return (section == 1) ? "Interface" : nil
|
||||
} else if ((numberOfPeerSections > 0) && (section < (1 + numberOfInterfaceSections + numberOfPeerSections))) {
|
||||
return "Interface"
|
||||
} else if ((numberOfPeerSections > 0) && (section < (2 + numberOfPeerSections))) {
|
||||
// Peer
|
||||
return "Peer"
|
||||
} else {
|
||||
// Add peer
|
||||
// Delete tunnel
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
||||
let interfaceData = tunnelViewModel.interfaceData
|
||||
let numberOfInterfaceSections = (0 ..< interfaceFieldsBySection.count).filter { section in
|
||||
(!interfaceData.filterFieldsWithValueOrControl(interfaceFields: interfaceFieldsBySection[section]).isEmpty)
|
||||
}.count
|
||||
let numberOfPeerSections = tunnelViewModel.peersData.count
|
||||
|
||||
let section = indexPath.section
|
||||
|
@ -186,32 +168,22 @@ extension TunnelDetailTableViewController {
|
|||
}
|
||||
}
|
||||
return cell
|
||||
} else if (section < 1 + numberOfInterfaceSections) {
|
||||
} else if (section == 1) {
|
||||
// Interface
|
||||
let field = interfaceData.filterFieldsWithValueOrControl(interfaceFields: interfaceFieldsBySection[section - 1])[row]
|
||||
if (field == .copyPublicKey) {
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: TunnelDetailTableViewButtonCell.id, for: indexPath) as! TunnelDetailTableViewButtonCell
|
||||
cell.buttonText = field.rawValue
|
||||
cell.onTapped = {
|
||||
UIPasteboard.general.string = interfaceData[.publicKey]
|
||||
}
|
||||
return cell
|
||||
} else {
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: TunnelDetailTableViewKeyValueCell.id, for: indexPath) as! TunnelDetailTableViewKeyValueCell
|
||||
// Set key and value
|
||||
cell.key = field.rawValue
|
||||
cell.value = interfaceData[field]
|
||||
if (field != .publicKey) {
|
||||
cell.detailTextLabel?.allowsDefaultTighteningForTruncation = true
|
||||
cell.detailTextLabel?.adjustsFontSizeToFitWidth = true
|
||||
cell.detailTextLabel?.minimumScaleFactor = 0.85
|
||||
}
|
||||
return cell
|
||||
let field = interfaceData.filterFieldsWithValueOrControl(interfaceFields: interfaceFields)[row]
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: TunnelDetailTableViewKeyValueCell.id, for: indexPath) as! TunnelDetailTableViewKeyValueCell
|
||||
// Set key and value
|
||||
cell.key = field.rawValue
|
||||
cell.value = interfaceData[field]
|
||||
if (field != .publicKey) {
|
||||
cell.detailTextLabel?.allowsDefaultTighteningForTruncation = true
|
||||
cell.detailTextLabel?.adjustsFontSizeToFitWidth = true
|
||||
cell.detailTextLabel?.minimumScaleFactor = 0.85
|
||||
}
|
||||
} else if ((numberOfPeerSections > 0) && (section < (1 + numberOfInterfaceSections + numberOfPeerSections))) {
|
||||
return cell
|
||||
} else if ((numberOfPeerSections > 0) && (section < (2 + numberOfPeerSections))) {
|
||||
// Peer
|
||||
let peerIndex = (section - numberOfInterfaceSections - 1)
|
||||
let peerData = tunnelViewModel.peersData[peerIndex]
|
||||
let peerData = tunnelViewModel.peersData[section - 2]
|
||||
let field = peerData.filterFieldsWithValueOrControl(peerFields: peerFields)[row]
|
||||
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: TunnelDetailTableViewKeyValueCell.id, for: indexPath) as! TunnelDetailTableViewKeyValueCell
|
||||
|
@ -226,7 +198,7 @@ extension TunnelDetailTableViewController {
|
|||
|
||||
return cell
|
||||
} else {
|
||||
assert(section == (1 + numberOfInterfaceSections + numberOfPeerSections))
|
||||
assert(section == (2 + numberOfPeerSections))
|
||||
// Delete configuration
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: TunnelDetailTableViewButtonCell.id, for: indexPath) as! TunnelDetailTableViewButtonCell
|
||||
cell.buttonText = "Delete tunnel"
|
||||
|
@ -328,7 +300,7 @@ class TunnelDetailTableViewStatusCell: UITableViewCell {
|
|||
}
|
||||
}
|
||||
|
||||
class TunnelDetailTableViewKeyValueCell: UITableViewCell {
|
||||
class TunnelDetailTableViewKeyValueCell: CopyableLabelTableViewCell {
|
||||
static let id: String = "TunnelDetailTableViewKeyValueCell"
|
||||
var key: String {
|
||||
get { return textLabel?.text ?? "" }
|
||||
|
@ -355,7 +327,7 @@ class TunnelDetailTableViewKeyValueCell: UITableViewCell {
|
|||
}
|
||||
|
||||
class TunnelDetailTableViewButtonCell: UITableViewCell {
|
||||
static let id: String = "TunnelsEditTableViewButtonCell"
|
||||
static let id: String = "TunnelDetailTableViewButtonCell"
|
||||
var buttonText: String {
|
||||
get { return button.title(for: .normal) ?? "" }
|
||||
set(value) { button.setTitle(value, for: .normal) }
|
||||
|
|
|
@ -361,7 +361,7 @@ extension TunnelEditTableViewController {
|
|||
}
|
||||
}
|
||||
|
||||
class TunnelEditTableViewKeyValueCell: UITableViewCell {
|
||||
class TunnelEditTableViewKeyValueCell: CopyableLabelTableViewCell {
|
||||
static let id: String = "TunnelEditTableViewKeyValueCell"
|
||||
var key: String {
|
||||
get { return keyLabel.text ?? "" }
|
||||
|
@ -378,6 +378,7 @@ class TunnelEditTableViewKeyValueCell: UITableViewCell {
|
|||
var isValueEditable: Bool {
|
||||
get { return valueTextField.isEnabled }
|
||||
set(value) {
|
||||
super.copyableGesture = !value
|
||||
valueTextField.isEnabled = value
|
||||
keyLabel.textColor = value ? UIColor.black : UIColor.gray
|
||||
valueTextField.textColor = value ? UIColor.black : UIColor.gray
|
||||
|
@ -409,6 +410,7 @@ class TunnelEditTableViewKeyValueCell: UITableViewCell {
|
|||
keyLabel = UILabel()
|
||||
valueTextField = UITextField()
|
||||
super.init(style: style, reuseIdentifier: reuseIdentifier)
|
||||
isValueEditable = true
|
||||
contentView.addSubview(keyLabel)
|
||||
keyLabel.translatesAutoresizingMaskIntoConstraints = false
|
||||
keyLabel.textAlignment = .right
|
||||
|
|
Loading…
Reference in New Issue