// // ToolbarView.swift // Gemini-iOS // // Created by Shadowfacts on 12/19/20. // import UIKit import BrowserCore import Combine class ToolbarView: UIView { let navigator: NavigationManager var showTableOfContents: (() -> Void)? var showShareSheet: ((UIView) -> Void)? var showPreferences: (() -> Void)? private var border: UIView! private var backButton: UIButton! private var forwardsButton: UIButton! private var reloadButton: UIButton! private var tableOfContentsButton: UIButton! private var shareButton: UIButton! private var prefsButton: UIButton! private var cancellables = [AnyCancellable]() init(navigator: NavigationManager) { self.navigator = navigator super.init(frame: .zero) backgroundColor = .systemBackground border = UIView() border.translatesAutoresizingMaskIntoConstraints = false border.backgroundColor = UIColor(white: traitCollection.userInterfaceStyle == .dark ? 0.25 : 0.75, alpha: 1) addSubview(border) NSLayoutConstraint.activate([ border.leadingAnchor.constraint(equalTo: leadingAnchor), border.trailingAnchor.constraint(equalTo: trailingAnchor), border.topAnchor.constraint(equalTo: topAnchor), border.heightAnchor.constraint(equalToConstant: 1), ]) let symbolConfig = UIImage.SymbolConfiguration(pointSize: 24) backButton = UIButton() backButton.addTarget(navigator, action: #selector(NavigationManager.goBack), for: .touchUpInside) backButton.isEnabled = navigator.backStack.count > 0 backButton.setImage(UIImage(systemName: "arrow.left", withConfiguration: symbolConfig), for: .normal) backButton.accessibilityLabel = "Back" backButton.isPointerInteractionEnabled = true // fallback for when UIButton.menu isn't available if #available(iOS 14.0, *) { } else { backButton.addInteraction(UIContextMenuInteraction(delegate: self)) } forwardsButton = UIButton() forwardsButton.addTarget(navigator, action: #selector(NavigationManager.goForward), for: .touchUpInside) forwardsButton.isEnabled = navigator.forwardStack.count > 0 forwardsButton.setImage(UIImage(systemName: "arrow.right", withConfiguration: symbolConfig), for: .normal) forwardsButton.accessibilityLabel = "Forward" forwardsButton.isPointerInteractionEnabled = true if #available(iOS 14.0, *) { } else { forwardsButton.addInteraction(UIContextMenuInteraction(delegate: self)) } reloadButton = UIButton() reloadButton.addTarget(navigator, action: #selector(NavigationManager.reload), for: .touchUpInside) reloadButton.setImage(UIImage(systemName: "arrow.clockwise", withConfiguration: symbolConfig), for: .normal) reloadButton.accessibilityLabel = "Reload" reloadButton.isPointerInteractionEnabled = true tableOfContentsButton = UIButton() tableOfContentsButton.addTarget(self, action: #selector(tableOfContentsPressed), for: .touchUpInside) tableOfContentsButton.setImage(UIImage(systemName: "list.bullet.indent", withConfiguration: symbolConfig), for: .normal) tableOfContentsButton.accessibilityLabel = "Table of Contents" tableOfContentsButton.isPointerInteractionEnabled = true shareButton = UIButton() shareButton.addTarget(self, action: #selector(sharePressed), for: .touchUpInside) shareButton.setImage(UIImage(systemName: "square.and.arrow.up", withConfiguration: symbolConfig), for: .normal) shareButton.accessibilityLabel = "Share" shareButton.isPointerInteractionEnabled = true prefsButton = UIButton() prefsButton.addTarget(self, action: #selector(prefsPressed), for: .touchUpInside) prefsButton.setImage(UIImage(systemName: "gear", withConfiguration: symbolConfig), for: .normal) prefsButton.accessibilityLabel = "Preferences" prefsButton.isPointerInteractionEnabled = true let stack = UIStackView(arrangedSubviews: [ backButton, forwardsButton, reloadButton, tableOfContentsButton, shareButton, prefsButton, ]) stack.axis = .horizontal stack.distribution = .fillEqually stack.alignment = .fill stack.translatesAutoresizingMaskIntoConstraints = false addSubview(stack) let safeAreaConstraint = stack.bottomAnchor.constraint(equalTo: safeAreaLayoutGuide.bottomAnchor) safeAreaConstraint.priority = .defaultHigh NSLayoutConstraint.activate([ stack.leadingAnchor.constraint(equalTo: leadingAnchor), stack.trailingAnchor.constraint(equalTo: trailingAnchor), stack.topAnchor.constraint(equalTo: topAnchor, constant: 5), safeAreaConstraint, stack.bottomAnchor.constraint(lessThanOrEqualTo: bottomAnchor, constant: -8) ]) updateNavigationButtons() navigator.navigationOperation .sink { (_) in self.updateNavigationButtons() } .store(in: &cancellables) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { super.traitCollectionDidChange(previousTraitCollection) border.backgroundColor = UIColor(white: traitCollection.userInterfaceStyle == .dark ? 0.25 : 0.75, alpha: 1) } private func urlForDisplay(_ url: URL) -> String { var str = url.host! if let port = url.port, url.scheme != "gemini" || port != 1965 { str += ":\(port)" } str += url.path return str } private func updateNavigationButtons() { backButton.isEnabled = navigator.backStack.count > 0 forwardsButton.isEnabled = navigator.forwardStack.count > 0 if #available(iOS 14.0, *) { let back = navigator.backStack.suffix(5).enumerated().reversed().map { (index, entry) -> UIAction in let backCount = min(5, navigator.backStack.count) - index if #available(iOS 15.0, *), let title = entry.title { return UIAction(title: title, subtitle: urlForDisplay(entry.url)) { [unowned self] (_) in self.navigator.back(count: backCount) } } else { return UIAction(title: urlForDisplay(entry.url)) { [unowned self] (_) in self.navigator.back(count: backCount) } } } backButton.menu = UIMenu(children: back) let forward = navigator.forwardStack.prefix(5).enumerated().map { (index, entry) -> UIAction in let forwardCount = index + 1 if #available(iOS 15.0, *), let title = entry.title { return UIAction(title: title, subtitle: urlForDisplay(entry.url)) { [unowned self] (_) in self.navigator.forward(count: forwardCount) } } else { return UIAction(title: urlForDisplay(entry.url)) { [unowned self] (_) in self.navigator.forward(count: forwardCount) } } } forwardsButton.menu = UIMenu(children: forward) } } @objc private func tableOfContentsPressed() { showTableOfContents?() } @objc private func sharePressed() { showShareSheet?(shareButton) } @objc private func prefsPressed() { showPreferences?() } } extension ToolbarView: UIContextMenuInteractionDelegate { func contextMenuInteraction(_ interaction: UIContextMenuInteraction, configurationForMenuAtLocation location: CGPoint) -> UIContextMenuConfiguration? { // this path is only used on =iOS 14, we don't create a UIContextMenuInteraction if interaction.view == backButton { return UIContextMenuConfiguration(identifier: nil, previewProvider: { nil }) { (_) -> UIMenu? in let children = self.navigator.backStack.suffix(5).enumerated().map { (index, entry) in UIAction(title: self.urlForDisplay(entry.url)) { (_) in self.navigator.back(count: min(5, self.navigator.backStack.count) - index) } } return UIMenu(title: "", image: nil, identifier: nil, options: [], children: children) } } else if interaction.view == forwardsButton { return UIContextMenuConfiguration(identifier: nil, previewProvider: { nil }) { (_) -> UIMenu? in let children = self.navigator.forwardStack.prefix(5).enumerated().map { (index, entry) -> UIAction in let forwardCount = index + 1 return UIAction(title: self.urlForDisplay(entry.url)) { (_) in self.navigator.forward(count: forwardCount) } } return UIMenu(title: "", image: nil, identifier: nil, options: [], children: children) } } else { return nil } } }