Add toolbar customization

This commit is contained in:
Shadowfacts 2021-10-16 00:16:53 -04:00
parent bea574d7bb
commit 0d80b6fdf5
Signed by: shadowfacts
GPG Key ID: 94A5AB95422746E5
8 changed files with 556 additions and 119 deletions

View File

@ -2,21 +2,14 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0"> <plist version="1.0">
<dict> <dict>
<key>UIApplicationShortcutItems</key>
<array>
<dict>
<key>UIApplicationShortcutItemIconType</key>
<string>UIApplicationShortcutIconTypeHome</string>
<key>UIApplicationShortcutItemType</key>
<string>home</string>
<key>UIApplicationShortcutItemTitle</key>
<string>Homepage</string>
</dict>
</array>
<key>CFBundleDevelopmentRegion</key> <key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string> <string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key> <key>CFBundleDisplayName</key>
<string>Rocketeer</string> <string>Rocketeer</string>
<key>CFBundleDocumentTypes</key>
<array>
<dict/>
</array>
<key>CFBundleExecutable</key> <key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string> <string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key> <key>CFBundleIdentifier</key>
@ -80,6 +73,17 @@
</array> </array>
</dict> </dict>
</dict> </dict>
<key>UIApplicationShortcutItems</key>
<array>
<dict>
<key>UIApplicationShortcutItemIconType</key>
<string>UIApplicationShortcutIconTypeHome</string>
<key>UIApplicationShortcutItemTitle</key>
<string>Homepage</string>
<key>UIApplicationShortcutItemType</key>
<string>home</string>
</dict>
</array>
<key>UIApplicationSupportsIndirectInputEvents</key> <key>UIApplicationSupportsIndirectInputEvents</key>
<true/> <true/>
<key>UILaunchStoryboardName</key> <key>UILaunchStoryboardName</key>

View File

@ -52,6 +52,10 @@ class Preferences: Codable, ObservableObject {
if let stored = try container.decodeIfPresent(Bool.self, forKey: .pullToRefreshEnabled) { if let stored = try container.decodeIfPresent(Bool.self, forKey: .pullToRefreshEnabled) {
pullToRefreshEnabled = stored pullToRefreshEnabled = stored
} }
if let stored = try container.decodeIfPresent([ToolbarItem].self, forKey: .toolbar) {
toolbar = stored
}
} }
func encode(to encoder: Encoder) throws { func encode(to encoder: Encoder) throws {
@ -67,6 +71,8 @@ class Preferences: Codable, ObservableObject {
try container.encode(hideToolbarsWhenScrolling, forKey: .hideToolbarsWhenScrolling) try container.encode(hideToolbarsWhenScrolling, forKey: .hideToolbarsWhenScrolling)
try container.encode(pullToRefreshEnabled, forKey: .pullToRefreshEnabled) try container.encode(pullToRefreshEnabled, forKey: .pullToRefreshEnabled)
try container.encode(toolbar, forKey: .toolbar)
} }
@Published var homepage = AppDelegate.defaultHomepage @Published var homepage = AppDelegate.defaultHomepage
@ -80,6 +86,8 @@ class Preferences: Codable, ObservableObject {
@Published var hideToolbarsWhenScrolling = true @Published var hideToolbarsWhenScrolling = true
@Published var pullToRefreshEnabled = true @Published var pullToRefreshEnabled = true
@Published var toolbar: [ToolbarItem] = [.back, .forward, .reload, .tableOfContents, .share, .preferences]
enum CodingKeys: String, CodingKey { enum CodingKeys: String, CodingKey {
case homepage case homepage
@ -91,6 +99,8 @@ class Preferences: Codable, ObservableObject {
case hideToolbarsWhenScrolling case hideToolbarsWhenScrolling
case pullToRefreshEnabled case pullToRefreshEnabled
case toolbar
} }
} }

View File

@ -10,7 +10,9 @@ import SwiftUI
struct PreferencesView: View { struct PreferencesView: View {
let dismiss: () -> Void let dismiss: () -> Void
@ObservedObject var preferences: Preferences = .shared @ObservedObject private var preferences: Preferences = .shared
// todo: this should really be a @StateObject on ToolbarPrefView
@State private var toolbarViewModel = CustomizeToolbarViewModel()
var body: some View { var body: some View {
NavigationView { NavigationView {
@ -76,6 +78,11 @@ struct PreferencesView: View {
Section(header: Text("Behavior")) { Section(header: Text("Behavior")) {
Toggle("Pull to Refresh", isOn: $preferences.pullToRefreshEnabled) Toggle("Pull to Refresh", isOn: $preferences.pullToRefreshEnabled)
Toggle("Hide Toolbars When Scrolling", isOn: $preferences.hideToolbarsWhenScrolling) Toggle("Hide Toolbars When Scrolling", isOn: $preferences.hideToolbarsWhenScrolling)
NavigationLink {
ToolbarPrefView(model: toolbarViewModel)
} label: {
Text("Customize Toolbar")
}
} }
} }
} }

View File

@ -0,0 +1,58 @@
//
// ToolbarItem.swift
// Gemini-iOS
//
// Created by Shadowfacts on 10/10/21.
//
import Foundation
enum ToolbarItem: String, Codable, CaseIterable {
case back
case forward
case reload
case share
case home
case tableOfContents
case preferences
}
extension ToolbarItem {
var imageName: String {
switch self {
case .back:
return "arrow.left"
case .forward:
return "arrow.right"
case .reload:
return "arrow.clockwise"
case .tableOfContents:
return "list.bullet.indent"
case .share:
return "square.and.arrow.up"
case .preferences:
return "gear"
case .home:
return "house"
}
}
var displayName: String {
switch self {
case .back:
return "Go Back"
case .forward:
return "Go Forward"
case .reload:
return "Reload"
case .tableOfContents:
return "Table of Contents"
case .share:
return "Share"
case .preferences:
return "Preferences"
case .home:
return "Home"
}
}
}

View File

@ -0,0 +1,338 @@
//
// ToolbarPrefView.swift
// Gemini-iOS
//
// Created by Shadowfacts on 10/9/21.
//
import SwiftUI
import Combine
let toolbarItemType = "space.vaccor.Gemini.toolbar-item"
struct ToolbarPrefView: View {
// todo: this should really be a @StateObject and shouldn't be passed in from the outside, but that requires iOS 14
@ObservedObject private var model: CustomizeToolbarViewModel
init(model: CustomizeToolbarViewModel) {
self.model = model
}
var body: some View {
VStack(spacing: 0) {
Spacer()
Text("Drag and drop items to change your toolbar")
Spacer()
separator
HStack(spacing: 8) {
Spacer()
ForEach(model.items) { (toolbarItem) in
Image(systemName: toolbarItem.imageName)
.font(.system(size: 24))
.foregroundColor(toolbarItem.isPlaceholder ? .gray : .blue)
.hoverEffect()
.overlay(GeometryReader { (proxy) in
Color.clear
.preference(key: FloatPrefKey.self, value: proxy.size.width)
.onPreferenceChange(FloatPrefKey.self) { newValue in
model.itemWidths[toolbarItem] = newValue
}
})
.onDrag {
guard !toolbarItem.isPlaceholder else {
return NSItemProvider()
}
model.beginDragging(item: toolbarItem.item)
return NSItemProvider(item: nil, typeIdentifier: toolbarItemType)
}
Spacer()
}
.animation(.default, value: model.items)
}
.padding(.vertical, 8)
.overlay(GeometryReader { proxy in
Color.clear
.preference(key: FloatPrefKey.self, value: proxy.size.width)
.onPreferenceChange(FloatPrefKey.self) { newValue in
model.totalWidth = newValue
}
})
.frame(maxWidth: .infinity)
.padding(.vertical, 4)
// this onDrop is needed because the one on the .background doesn't fire when the drag item is held above one of the items themselves
.onDrop(of: [toolbarItemType], delegate: model)
.background(
Color(UIColor.systemBackground)
// this onDrop is needed because the one on the ForEach doesn't fire when is the drag item held in between two of the ForEach items (i.e., above one of the spacers)
.onDrop(of: [toolbarItemType], delegate: model)
)
separator
Spacer(minLength: 16)
VStack(alignment: .textLeading, spacing: 8) {
ForEach(ToolbarItem.allCases, id: \.rawValue) { (item) in
HStack(spacing: 4) {
Image(systemName: item.imageName)
.font(.system(size: 24))
.foregroundColor(canAdd(item: item) ? .blue : .gray)
.disabled(!canAdd(item: item))
.hoverEffect()
.animation(.default, value: canAdd(item: item))
.onDrag {
model.beginDragging(item: item)
return NSItemProvider(item: nil, typeIdentifier: toolbarItemType)
}
Text(item.displayName)
.alignmentGuide(.textLeading) { d in d[HorizontalAlignment.leading] }
}
}
}
Spacer()
}
.background(
Color(.secondarySystemBackground)
.edgesIgnoringSafeArea(.all)
// this stupid onDrop is necessary because sometimes when quickly dragging an item out of the toolbar, dropExited is never called on the main delegate
// so the BackgroundDropDelegate ensures that any placeholders are removed
.onDrop(of: [toolbarItemType], delegate: BackgroundDropDelegate(toolbarDropDelegate: model))
)
}
private var separator: some View {
Rectangle()
.frame(height: 0.5)
.foregroundColor(.gray)
}
private func canAdd(item: ToolbarItem) -> Bool {
return !model.items.contains(.item(item))
}
}
private extension HorizontalAlignment {
struct TextLeadingAlignment: AlignmentID {
static func defaultValue(in context: ViewDimensions) -> CGFloat {
return context[HorizontalAlignment.center]
}
}
/// The alignment used for aligning the leading edges of all the text in the toolbar item list.
static let textLeading = HorizontalAlignment(TextLeadingAlignment.self)
}
private enum CustomizeToolbarItem: Identifiable, Equatable, Hashable {
case item(ToolbarItem)
case placeholder(ToolbarItem)
var id: String {
switch self {
case let .item(item):
return item.rawValue
case let .placeholder(item):
return "placeholder_\(item.rawValue)"
}
}
var imageName: String {
switch self {
case .item(let item), .placeholder(let item):
return item.imageName
}
}
var isPlaceholder: Bool {
switch self {
case .item(_):
return false
case .placeholder(_):
return true
}
}
var item: ToolbarItem {
switch self {
case .item(let item), .placeholder(let item):
return item
}
}
}
class CustomizeToolbarViewModel: ObservableObject, DropDelegate {
@Published fileprivate var items: [CustomizeToolbarItem]
fileprivate var totalWidth: CGFloat = 0
fileprivate var itemWidths: [CustomizeToolbarItem: CGFloat] = [:]
fileprivate var draggedItem: ToolbarItem?
private var isDraggingExisting = false
private var cancellables = Set<AnyCancellable>()
init() {
items = Preferences.shared.toolbar.map { .item($0) }
Preferences.shared.$toolbar
.sink { [unowned self] (newValue) in
self.items = newValue.map { .item($0) }
}
.store(in: &cancellables)
}
fileprivate func beginDragging(item: ToolbarItem) {
draggedItem = item
if let index = items.firstIndex(where: { $0.item == item }) {
items[index] = .placeholder(item)
isDraggingExisting = true
} else {
isDraggingExisting = false
}
}
fileprivate func cleanupCustomizeItems() {
items = items.compactMap {
switch $0 {
case .item(_):
return $0
case .placeholder(let item):
if canRemove(item: item) {
return nil
} else {
return .item(item)
}
}
}
updatePreferences()
}
private func canRemove(item: ToolbarItem) -> Bool {
// preferences can't be removed because the user would lose access to the toolbar preferences
return item != .preferences
}
private func updatePreferences() {
Preferences.shared.toolbar = self.items.compactMap {
switch $0 {
case let .item(item):
return item
case .placeholder:
return nil
}
}
}
private func proposedDropIndex(info: DropInfo) -> Int {
let totalItemWidth = itemWidths.filter { items.contains($0.key) }.reduce(0, { $0 + $1.value })
let remainingWidth = totalWidth - totalItemWidth
let spacerWidth = remainingWidth / CGFloat(items.count + 1)
var accumulatedWidth = spacerWidth
for (index, item) in items.enumerated() {
let itemWidth = itemWidths[item]!
if info.location.x < accumulatedWidth + itemWidth / 2 {
return index
} else {
accumulatedWidth += itemWidth + spacerWidth
}
}
return items.count
}
// MARK: DropDelegate
func validateDrop(info: DropInfo) -> Bool {
return draggedItem != nil && (isDraggingExisting || UIDevice.current.userInterfaceIdiom == .pad || items.filter { !$0.isPlaceholder }.count < 6)
}
func dropEntered(info: DropInfo) {
items.removeAll(where: \.isPlaceholder)
// if we just exited the other onDrop handler, we need to re-add the placeholder
if !items.contains(where: { $0.item == draggedItem! }) {
items.insert(.placeholder(draggedItem!), at: proposedDropIndex(info: info))
}
}
func dropUpdated(info: DropInfo) -> DropProposal? {
if isDraggingExisting {
let index = items.firstIndex { $0.item == draggedItem! }!
items[index] = .placeholder(draggedItem!)
items.move(fromOffsets: IndexSet(integer: index), toOffset: proposedDropIndex(info: info))
return DropProposal(operation: .move)
} else {
items.removeAll(where: \.isPlaceholder)
let index = proposedDropIndex(info: info)
items.insert(.placeholder(draggedItem!), at: index)
return DropProposal(operation: .copy)
}
}
func dropExited(info: DropInfo) {
cleanupCustomizeItems()
}
func performDrop(info: DropInfo) -> Bool {
let placeholderIndex = items.firstIndex(where: \.isPlaceholder)!
items[placeholderIndex] = .item(draggedItem!)
self.updatePreferences()
return true
}
}
private struct BackgroundDropDelegate: DropDelegate {
let toolbarDropDelegate: CustomizeToolbarViewModel
func dropUpdated(info: DropInfo) -> DropProposal? {
if toolbarDropDelegate.draggedItem != nil {
toolbarDropDelegate.cleanupCustomizeItems()
}
return DropProposal(operation: .move)
}
func performDrop(info: DropInfo) -> Bool {
return false
}
}
private struct FloatPrefKey: PreferenceKey {
static var defaultValue: CGFloat = 0
static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
value = nextValue()
}
}
private extension View {
@ViewBuilder
func onDragWithPreviewIfPossible<V>(_ data: @escaping () -> NSItemProvider, preview: () -> V) -> some View where V : View {
if #available(iOS 15.0, *) {
self.onDrag(data, preview: preview)
} else {
self.onDrag(data)
}
}
@ViewBuilder
func onDragIf(condition: () -> Bool, data: @escaping () -> NSItemProvider) -> some View {
if condition() {
self.onDrag(data)
} else {
self
}
}
}
struct ToolbarPrefView_Previews: PreviewProvider {
static var previews: some View {
ToolbarPrefView(model: CustomizeToolbarViewModel())
}
}

View File

@ -18,12 +18,8 @@ class ToolbarView: UIView {
var showPreferences: (() -> Void)? var showPreferences: (() -> Void)?
private var border: UIView! private var border: UIView!
private var backButton: UIButton! private var buttonsStack: UIStackView!
private var forwardsButton: UIButton! private var toolbarButtons: [ToolbarItem: UIButton] = [:]
private var reloadButton: UIButton!
private var tableOfContentsButton: UIButton!
private var shareButton: UIButton!
private var prefsButton: UIButton!
private var cancellables = [AnyCancellable]() private var cancellables = [AnyCancellable]()
@ -45,85 +41,35 @@ class ToolbarView: UIView {
border.heightAnchor.constraint(equalToConstant: 1), border.heightAnchor.constraint(equalToConstant: 1),
]) ])
let symbolConfig = UIImage.SymbolConfiguration(pointSize: 24) buttonsStack = UIStackView()
buttonsStack.axis = .horizontal
backButton = UIButton() buttonsStack.distribution = .fillEqually
backButton.addTarget(navigator, action: #selector(NavigationManager.goBack), for: .touchUpInside) buttonsStack.alignment = .fill
backButton.isEnabled = navigator.backStack.count > 0 buttonsStack.translatesAutoresizingMaskIntoConstraints = false
backButton.setImage(UIImage(systemName: "arrow.left", withConfiguration: symbolConfig), for: .normal) addSubview(buttonsStack)
backButton.accessibilityLabel = "Back" let safeAreaConstraint = buttonsStack.bottomAnchor.constraint(equalTo: safeAreaLayoutGuide.bottomAnchor)
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 safeAreaConstraint.priority = .defaultHigh
NSLayoutConstraint.activate([ NSLayoutConstraint.activate([
stack.leadingAnchor.constraint(equalTo: leadingAnchor), buttonsStack.leadingAnchor.constraint(equalTo: leadingAnchor),
stack.trailingAnchor.constraint(equalTo: trailingAnchor), buttonsStack.trailingAnchor.constraint(equalTo: trailingAnchor),
stack.topAnchor.constraint(equalTo: topAnchor, constant: 5), buttonsStack.topAnchor.constraint(equalTo: topAnchor, constant: 5),
safeAreaConstraint, safeAreaConstraint,
stack.bottomAnchor.constraint(lessThanOrEqualTo: bottomAnchor, constant: -8) buttonsStack.bottomAnchor.constraint(lessThanOrEqualTo: bottomAnchor, constant: -8)
]) ])
updateNavigationButtons() updateNavigationButtons()
navigator.navigationOperation navigator.navigationOperation
.sink { (_) in .sink { [unowned self] (_) in
self.updateNavigationButtons() self.updateNavigationButtons()
} }
.store(in: &cancellables) .store(in: &cancellables)
Preferences.shared.$toolbar
.sink { [unowned self] (newValue) in
self.createToolbarButtons(newValue)
}
.store(in: &cancellables)
} }
required init?(coder: NSCoder) { required init?(coder: NSCoder) {
@ -136,40 +82,100 @@ class ToolbarView: UIView {
border.backgroundColor = UIColor(white: traitCollection.userInterfaceStyle == .dark ? 0.25 : 0.75, alpha: 1) 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() { private func updateNavigationButtons() {
backButton.isEnabled = navigator.backStack.count > 0 if let backButton = toolbarButtons[.back] {
forwardsButton.isEnabled = navigator.forwardStack.count > 0 backButton.isEnabled = navigator.backStack.count > 0
if #available(iOS 14.0, *) {
if #available(iOS 14.0, *) { let back = navigator.backStack.suffix(5).enumerated().reversed().map { (index, entry) -> UIAction in
let back = navigator.backStack.suffix(5).enumerated().reversed().map { (index, entry) -> UIAction in let backCount = min(5, navigator.backStack.count) - index
let backCount = min(5, navigator.backStack.count) - index if #available(iOS 15.0, *),
if #available(iOS 15.0, *), let title = entry.title {
let title = entry.title { return UIAction(title: title, subtitle: BrowserHelper.urlForDisplay(entry.url)) { [unowned self] (_) in
return UIAction(title: title, subtitle: BrowserHelper.urlForDisplay(entry.url)) { [unowned self] (_) in self.navigator.back(count: backCount)
self.navigator.back(count: backCount) }
} } else {
} else { return UIAction(title: BrowserHelper.urlForDisplay(entry.url)) { [unowned self] (_) in
return UIAction(title: BrowserHelper.urlForDisplay(entry.url)) { [unowned self] (_) in self.navigator.back(count: backCount)
self.navigator.back(count: backCount) }
} }
} }
backButton.menu = UIMenu(children: back)
} }
backButton.menu = UIMenu(children: back) }
let forward = navigator.forwardStack.prefix(5).enumerated().map { (index, entry) -> UIAction in if let forwardsButton = toolbarButtons[.forward] {
let forwardCount = index + 1 forwardsButton.isEnabled = navigator.forwardStack.count > 0
if #available(iOS 15.0, *), if #available(iOS 14.0, *) {
let title = entry.title { let forward = navigator.forwardStack.prefix(5).enumerated().map { (index, entry) -> UIAction in
return UIAction(title: title, subtitle: BrowserHelper.urlForDisplay(entry.url)) { [unowned self] (_) in let forwardCount = index + 1
self.navigator.forward(count: forwardCount) if #available(iOS 15.0, *),
} let title = entry.title {
} else { return UIAction(title: title, subtitle: BrowserHelper.urlForDisplay(entry.url)) { [unowned self] (_) in
return UIAction(title: BrowserHelper.urlForDisplay(entry.url)) { [unowned self] (_) in self.navigator.forward(count: forwardCount)
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)
} }
forwardsButton.menu = UIMenu(children: forward)
} }
} }
@ -178,19 +184,24 @@ class ToolbarView: UIView {
} }
@objc private func sharePressed() { @objc private func sharePressed() {
showShareSheet?(shareButton) showShareSheet?(toolbarButtons[.share]!)
} }
@objc private func prefsPressed() { @objc private func prefsPressed() {
showPreferences?() showPreferences?()
} }
@objc private func homePressed() {
navigator.changeURL(Preferences.shared.homepage)
}
} }
extension ToolbarView: UIContextMenuInteractionDelegate { extension ToolbarView: UIContextMenuInteractionDelegate {
func contextMenuInteraction(_ interaction: UIContextMenuInteraction, configurationForMenuAtLocation location: CGPoint) -> UIContextMenuConfiguration? { func contextMenuInteraction(_ interaction: UIContextMenuInteraction, configurationForMenuAtLocation location: CGPoint) -> UIContextMenuConfiguration? {
// this path is only used on <iOS 14, on >=iOS 14, we don't create a UIContextMenuInteraction // this path is only used on <iOS 14, on >=iOS 14, we don't create a UIContextMenuInteraction
if interaction.view == backButton { if let backButton = toolbarButtons[.back],
interaction.view == backButton {
return UIContextMenuConfiguration(identifier: nil, previewProvider: { nil }) { (_) -> UIMenu? in return UIContextMenuConfiguration(identifier: nil, previewProvider: { nil }) { (_) -> UIMenu? in
let children = self.navigator.backStack.suffix(5).enumerated().map { (index, entry) in let children = self.navigator.backStack.suffix(5).enumerated().map { (index, entry) in
UIAction(title: BrowserHelper.urlForDisplay(entry.url)) { (_) in UIAction(title: BrowserHelper.urlForDisplay(entry.url)) { (_) in
@ -199,7 +210,8 @@ extension ToolbarView: UIContextMenuInteractionDelegate {
} }
return UIMenu(title: "", image: nil, identifier: nil, options: [], children: children) return UIMenu(title: "", image: nil, identifier: nil, options: [], children: children)
} }
} else if interaction.view == forwardsButton { } else if let forwardsButton = toolbarButtons[.forward],
interaction.view == forwardsButton {
return UIContextMenuConfiguration(identifier: nil, previewProvider: { nil }) { (_) -> UIMenu? in return UIContextMenuConfiguration(identifier: nil, previewProvider: { nil }) { (_) -> UIMenu? in
let children = self.navigator.forwardStack.prefix(5).enumerated().map { (index, entry) -> UIAction in let children = self.navigator.forwardStack.prefix(5).enumerated().map { (index, entry) -> UIAction in
let forwardCount = index + 1 let forwardCount = index + 1

View File

@ -37,12 +37,14 @@
D62664F024BC0D7700DF9B88 /* GeminiFormat.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D62664A824BBF26A00DF9B88 /* GeminiFormat.framework */; }; D62664F024BC0D7700DF9B88 /* GeminiFormat.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D62664A824BBF26A00DF9B88 /* GeminiFormat.framework */; };
D62664FA24BC12BC00DF9B88 /* DocumentTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D62664F924BC12BC00DF9B88 /* DocumentTests.swift */; }; D62664FA24BC12BC00DF9B88 /* DocumentTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D62664F924BC12BC00DF9B88 /* DocumentTests.swift */; };
D6376A7026DDAF65005AD89C /* URIFixup.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6376A6F26DDAF65005AD89C /* URIFixup.swift */; }; D6376A7026DDAF65005AD89C /* URIFixup.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6376A6F26DDAF65005AD89C /* URIFixup.swift */; };
D640A2322711DC7700177E85 /* ToolbarPrefView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D640A2312711DC7700177E85 /* ToolbarPrefView.swift */; };
D653F40B267996FF004E32B1 /* ActivityItemSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = D653F40A267996FF004E32B1 /* ActivityItemSource.swift */; }; D653F40B267996FF004E32B1 /* ActivityItemSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = D653F40A267996FF004E32B1 /* ActivityItemSource.swift */; };
D653F40D26799F2F004E32B1 /* HomepagePrefView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D653F40C26799F2F004E32B1 /* HomepagePrefView.swift */; }; D653F40D26799F2F004E32B1 /* HomepagePrefView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D653F40C26799F2F004E32B1 /* HomepagePrefView.swift */; };
D653F40F2679A0AB004E32B1 /* SetHomepageActivity.swift in Sources */ = {isa = PBXBuildFile; fileRef = D653F40E2679A0AB004E32B1 /* SetHomepageActivity.swift */; }; D653F40F2679A0AB004E32B1 /* SetHomepageActivity.swift in Sources */ = {isa = PBXBuildFile; fileRef = D653F40E2679A0AB004E32B1 /* SetHomepageActivity.swift */; };
D664673624BD07F700B0B741 /* RenderingBlock.swift in Sources */ = {isa = PBXBuildFile; fileRef = D664673524BD07F700B0B741 /* RenderingBlock.swift */; }; D664673624BD07F700B0B741 /* RenderingBlock.swift in Sources */ = {isa = PBXBuildFile; fileRef = D664673524BD07F700B0B741 /* RenderingBlock.swift */; };
D664673824BD086F00B0B741 /* RenderingBlockView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D664673724BD086F00B0B741 /* RenderingBlockView.swift */; }; D664673824BD086F00B0B741 /* RenderingBlockView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D664673724BD086F00B0B741 /* RenderingBlockView.swift */; };
D664673A24BD0B8E00B0B741 /* Fonts.swift in Sources */ = {isa = PBXBuildFile; fileRef = D664673924BD0B8E00B0B741 /* Fonts.swift */; }; D664673A24BD0B8E00B0B741 /* Fonts.swift in Sources */ = {isa = PBXBuildFile; fileRef = D664673924BD0B8E00B0B741 /* Fonts.swift */; };
D664E4FA2713DF72005BAF55 /* ToolbarItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D664E4F92713DF72005BAF55 /* ToolbarItem.swift */; };
D688F599258ACAAE003A0A73 /* BrowserWebViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D688F598258ACAAE003A0A73 /* BrowserWebViewController.swift */; }; D688F599258ACAAE003A0A73 /* BrowserWebViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D688F598258ACAAE003A0A73 /* BrowserWebViewController.swift */; };
D688F5FF258ACE6B003A0A73 /* browser.css in Resources */ = {isa = PBXBuildFile; fileRef = D688F5FE258ACE6B003A0A73 /* browser.css */; }; D688F5FF258ACE6B003A0A73 /* browser.css in Resources */ = {isa = PBXBuildFile; fileRef = D688F5FE258ACE6B003A0A73 /* browser.css */; };
D688F633258B09BB003A0A73 /* TrackpadScrollGestureRecognizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = D688F632258B09BB003A0A73 /* TrackpadScrollGestureRecognizer.swift */; }; D688F633258B09BB003A0A73 /* TrackpadScrollGestureRecognizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = D688F632258B09BB003A0A73 /* TrackpadScrollGestureRecognizer.swift */; };
@ -347,12 +349,14 @@
D62664ED24BC0BCE00DF9B88 /* MaybeLazyVStack.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MaybeLazyVStack.swift; sourceTree = "<group>"; }; D62664ED24BC0BCE00DF9B88 /* MaybeLazyVStack.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MaybeLazyVStack.swift; sourceTree = "<group>"; };
D62664F924BC12BC00DF9B88 /* DocumentTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DocumentTests.swift; sourceTree = "<group>"; }; D62664F924BC12BC00DF9B88 /* DocumentTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DocumentTests.swift; sourceTree = "<group>"; };
D6376A6F26DDAF65005AD89C /* URIFixup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URIFixup.swift; sourceTree = "<group>"; }; D6376A6F26DDAF65005AD89C /* URIFixup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URIFixup.swift; sourceTree = "<group>"; };
D640A2312711DC7700177E85 /* ToolbarPrefView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToolbarPrefView.swift; sourceTree = "<group>"; };
D653F40A267996FF004E32B1 /* ActivityItemSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivityItemSource.swift; sourceTree = "<group>"; }; D653F40A267996FF004E32B1 /* ActivityItemSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivityItemSource.swift; sourceTree = "<group>"; };
D653F40C26799F2F004E32B1 /* HomepagePrefView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomepagePrefView.swift; sourceTree = "<group>"; }; D653F40C26799F2F004E32B1 /* HomepagePrefView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomepagePrefView.swift; sourceTree = "<group>"; };
D653F40E2679A0AB004E32B1 /* SetHomepageActivity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SetHomepageActivity.swift; sourceTree = "<group>"; }; D653F40E2679A0AB004E32B1 /* SetHomepageActivity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SetHomepageActivity.swift; sourceTree = "<group>"; };
D664673524BD07F700B0B741 /* RenderingBlock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RenderingBlock.swift; sourceTree = "<group>"; }; D664673524BD07F700B0B741 /* RenderingBlock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RenderingBlock.swift; sourceTree = "<group>"; };
D664673724BD086F00B0B741 /* RenderingBlockView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RenderingBlockView.swift; sourceTree = "<group>"; }; D664673724BD086F00B0B741 /* RenderingBlockView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RenderingBlockView.swift; sourceTree = "<group>"; };
D664673924BD0B8E00B0B741 /* Fonts.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Fonts.swift; sourceTree = "<group>"; }; D664673924BD0B8E00B0B741 /* Fonts.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Fonts.swift; sourceTree = "<group>"; };
D664E4F92713DF72005BAF55 /* ToolbarItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToolbarItem.swift; sourceTree = "<group>"; };
D688F585258AC738003A0A73 /* GeminiHTMLRenderer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GeminiHTMLRenderer.swift; sourceTree = "<group>"; }; D688F585258AC738003A0A73 /* GeminiHTMLRenderer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GeminiHTMLRenderer.swift; sourceTree = "<group>"; };
D688F598258ACAAE003A0A73 /* BrowserWebViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrowserWebViewController.swift; sourceTree = "<group>"; }; D688F598258ACAAE003A0A73 /* BrowserWebViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrowserWebViewController.swift; sourceTree = "<group>"; };
D688F5FE258ACE6B003A0A73 /* browser.css */ = {isa = PBXFileReference; lastKnownFileType = text.css; path = browser.css; sourceTree = "<group>"; }; D688F5FE258ACE6B003A0A73 /* browser.css */ = {isa = PBXFileReference; lastKnownFileType = text.css; path = browser.css; sourceTree = "<group>"; };
@ -704,8 +708,10 @@
D6BC9ABB258E9862008652BC /* NavigationBarView.swift */, D6BC9ABB258E9862008652BC /* NavigationBarView.swift */,
D6BC9AD6258FC8B3008652BC /* TableOfContentsView.swift */, D6BC9AD6258FC8B3008652BC /* TableOfContentsView.swift */,
D691A64D25217C6F00348C4B /* Preferences.swift */, D691A64D25217C6F00348C4B /* Preferences.swift */,
D664E4F92713DF72005BAF55 /* ToolbarItem.swift */,
D691A66625217FD800348C4B /* PreferencesView.swift */, D691A66625217FD800348C4B /* PreferencesView.swift */,
D653F40C26799F2F004E32B1 /* HomepagePrefView.swift */, D653F40C26799F2F004E32B1 /* HomepagePrefView.swift */,
D640A2312711DC7700177E85 /* ToolbarPrefView.swift */,
D653F40A267996FF004E32B1 /* ActivityItemSource.swift */, D653F40A267996FF004E32B1 /* ActivityItemSource.swift */,
D653F40E2679A0AB004E32B1 /* SetHomepageActivity.swift */, D653F40E2679A0AB004E32B1 /* SetHomepageActivity.swift */,
D68C1E1A27055EA0002D642B /* Intents */, D68C1E1A27055EA0002D642B /* Intents */,
@ -1282,6 +1288,7 @@
D688F599258ACAAE003A0A73 /* BrowserWebViewController.swift in Sources */, D688F599258ACAAE003A0A73 /* BrowserWebViewController.swift in Sources */,
D688F633258B09BB003A0A73 /* TrackpadScrollGestureRecognizer.swift in Sources */, D688F633258B09BB003A0A73 /* TrackpadScrollGestureRecognizer.swift in Sources */,
D6E152A524BFFDF500FDF9D3 /* AppDelegate.swift in Sources */, D6E152A524BFFDF500FDF9D3 /* AppDelegate.swift in Sources */,
D640A2322711DC7700177E85 /* ToolbarPrefView.swift in Sources */,
D6E152A724BFFDF500FDF9D3 /* SceneDelegate.swift in Sources */, D6E152A724BFFDF500FDF9D3 /* SceneDelegate.swift in Sources */,
D68C1E35270615D3002D642B /* UserActivities.swift in Sources */, D68C1E35270615D3002D642B /* UserActivities.swift in Sources */,
D6376A7026DDAF65005AD89C /* URIFixup.swift in Sources */, D6376A7026DDAF65005AD89C /* URIFixup.swift in Sources */,
@ -1291,6 +1298,7 @@
D688F64A258C17F3003A0A73 /* SymbolCache.swift in Sources */, D688F64A258C17F3003A0A73 /* SymbolCache.swift in Sources */,
D653F40F2679A0AB004E32B1 /* SetHomepageActivity.swift in Sources */, D653F40F2679A0AB004E32B1 /* SetHomepageActivity.swift in Sources */,
D688F65A258C2256003A0A73 /* BrowserNavigationController.swift in Sources */, D688F65A258C2256003A0A73 /* BrowserNavigationController.swift in Sources */,
D664E4FA2713DF72005BAF55 /* ToolbarItem.swift in Sources */,
D6BC9AD7258FC8B3008652BC /* TableOfContentsView.swift in Sources */, D6BC9AD7258FC8B3008652BC /* TableOfContentsView.swift in Sources */,
D68C1E1927055E09002D642B /* Intents.intentdefinition in Sources */, D68C1E1927055E09002D642B /* Intents.intentdefinition in Sources */,
D688F663258C2479003A0A73 /* UIViewController+Children.swift in Sources */, D688F663258C2479003A0A73 /* UIViewController+Children.swift in Sources */,

View File

@ -7,7 +7,7 @@
<key>BrowserCore.xcscheme_^#shared#^_</key> <key>BrowserCore.xcscheme_^#shared#^_</key>
<dict> <dict>
<key>orderHint</key> <key>orderHint</key>
<integer>5</integer> <integer>4</integer>
</dict> </dict>
<key>Gemini-iOS.xcscheme_^#shared#^_</key> <key>Gemini-iOS.xcscheme_^#shared#^_</key>
<dict> <dict>
@ -32,12 +32,12 @@
<key>GeminiProtocol.xcscheme_^#shared#^_</key> <key>GeminiProtocol.xcscheme_^#shared#^_</key>
<dict> <dict>
<key>orderHint</key> <key>orderHint</key>
<integer>6</integer> <integer>5</integer>
</dict> </dict>
<key>GeminiRenderer.xcscheme_^#shared#^_</key> <key>GeminiRenderer.xcscheme_^#shared#^_</key>
<dict> <dict>
<key>orderHint</key> <key>orderHint</key>
<integer>4</integer> <integer>6</integer>
</dict> </dict>
</dict> </dict>
<key>SuppressBuildableAutocreation</key> <key>SuppressBuildableAutocreation</key>