// // 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 buttonsStack: UIStackView! private var toolbarButtons: [ToolbarItem: 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), ]) buttonsStack = UIStackView() buttonsStack.axis = .horizontal buttonsStack.distribution = .fillEqually buttonsStack.alignment = .fill buttonsStack.translatesAutoresizingMaskIntoConstraints = false addSubview(buttonsStack) let safeAreaConstraint = buttonsStack.bottomAnchor.constraint(equalTo: safeAreaLayoutGuide.bottomAnchor) safeAreaConstraint.priority = .defaultHigh NSLayoutConstraint.activate([ buttonsStack.leadingAnchor.constraint(equalTo: leadingAnchor), buttonsStack.trailingAnchor.constraint(equalTo: trailingAnchor), buttonsStack.topAnchor.constraint(equalTo: topAnchor, constant: 5), safeAreaConstraint, buttonsStack.bottomAnchor.constraint(lessThanOrEqualTo: bottomAnchor, constant: -8) ]) updateNavigationButtons() navigator.navigationOperation .sink { [unowned self] (_) in self.updateNavigationButtons() } .store(in: &cancellables) Preferences.shared.$toolbar .sink { [unowned self] (newValue) in self.createToolbarButtons(newValue) } .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 createToolbarButtons(_ items: [ToolbarItem] = Preferences.shared.toolbar) { toolbarButtons = [:] buttonsStack.arrangedSubviews.forEach { $0.removeFromSuperview() } for item in items { let button = createButton(item) toolbarButtons[item] = button buttonsStack.addArrangedSubview(button) } updateNavigationButtons() } private func createButton(_ item: ToolbarItem) -> UIButton { let button = UIButton() let symbolConfig = UIImage.SymbolConfiguration(pointSize: 24) button.setImage(UIImage(systemName: item.imageName, withConfiguration: symbolConfig)!, for: .normal) button.accessibilityLabel = item.displayName button.isPointerInteractionEnabled = true switch item { case .back: button.addTarget(navigator, action: #selector(NavigationManager.goBack), for: .touchUpInside) // fallback for when UIButton.menu isn't available if #available(iOS 14.0, *) { } else { button.addInteraction(UIContextMenuInteraction(delegate: self)) } case .forward: button.addTarget(navigator, action: #selector(NavigationManager.goForward), for: .touchUpInside) if #available(iOS 14.0, *) { } else { button.addInteraction(UIContextMenuInteraction(delegate: self)) } case .reload: button.addTarget(navigator, action: #selector(NavigationManager.reload), for: .touchUpInside) case .share: button.addTarget(self, action: #selector(sharePressed), for: .touchUpInside) case .home: button.addTarget(self, action: #selector(homePressed), for: .touchUpInside) case .tableOfContents: button.addTarget(self, action: #selector(tableOfContentsPressed), for: .touchUpInside) case .preferences: button.addTarget(self, action: #selector(prefsPressed), for: .touchUpInside) } return button } private func updateNavigationButtons() { if let backButton = toolbarButtons[.back] { backButton.isEnabled = navigator.backStack.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: BrowserHelper.urlForDisplay(entry.url)) { [unowned self] (_) in self.navigator.back(count: backCount) } } else { return UIAction(title: BrowserHelper.urlForDisplay(entry.url)) { [unowned self] (_) in self.navigator.back(count: backCount) } } } backButton.menu = UIMenu(children: back) } } if let forwardsButton = toolbarButtons[.forward] { forwardsButton.isEnabled = navigator.forwardStack.count > 0 if #available(iOS 14.0, *) { 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: BrowserHelper.urlForDisplay(entry.url)) { [unowned self] (_) in self.navigator.forward(count: forwardCount) } } else { return UIAction(title: BrowserHelper.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?(toolbarButtons[.share]!) } @objc private func prefsPressed() { showPreferences?() } @objc private func homePressed() { navigator.changeURL(Preferences.shared.homepage) } } 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 let backButton = toolbarButtons[.back], 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: BrowserHelper.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 let forwardsButton = toolbarButtons[.forward], 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: BrowserHelper.urlForDisplay(entry.url)) { (_) in self.navigator.forward(count: forwardCount) } } return UIMenu(title: "", image: nil, identifier: nil, options: [], children: children) } } else { return nil } } }