Add toolbar customization
This commit is contained in:
parent
bea574d7bb
commit
fe9c42a2f8
|
@ -2,21 +2,14 @@
|
|||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<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>
|
||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>Rocketeer</string>
|
||||
<key>CFBundleDocumentTypes</key>
|
||||
<array>
|
||||
<dict/>
|
||||
</array>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
|
@ -80,6 +73,17 @@
|
|||
</array>
|
||||
</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>
|
||||
<true/>
|
||||
<key>UILaunchStoryboardName</key>
|
||||
|
|
|
@ -52,6 +52,10 @@ class Preferences: Codable, ObservableObject {
|
|||
if let stored = try container.decodeIfPresent(Bool.self, forKey: .pullToRefreshEnabled) {
|
||||
pullToRefreshEnabled = stored
|
||||
}
|
||||
|
||||
if let stored = try container.decodeIfPresent([ToolbarItem].self, forKey: .toolbar) {
|
||||
toolbar = stored
|
||||
}
|
||||
}
|
||||
|
||||
func encode(to encoder: Encoder) throws {
|
||||
|
@ -67,6 +71,8 @@ class Preferences: Codable, ObservableObject {
|
|||
|
||||
try container.encode(hideToolbarsWhenScrolling, forKey: .hideToolbarsWhenScrolling)
|
||||
try container.encode(pullToRefreshEnabled, forKey: .pullToRefreshEnabled)
|
||||
|
||||
try container.encode(toolbar, forKey: .toolbar)
|
||||
}
|
||||
|
||||
@Published var homepage = AppDelegate.defaultHomepage
|
||||
|
@ -80,6 +86,8 @@ class Preferences: Codable, ObservableObject {
|
|||
@Published var hideToolbarsWhenScrolling = true
|
||||
@Published var pullToRefreshEnabled = true
|
||||
|
||||
@Published var toolbar: [ToolbarItem] = [.back, .forward, .reload, .tableOfContents, .share, .preferences]
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case homepage
|
||||
|
||||
|
@ -91,6 +99,8 @@ class Preferences: Codable, ObservableObject {
|
|||
|
||||
case hideToolbarsWhenScrolling
|
||||
case pullToRefreshEnabled
|
||||
|
||||
case toolbar
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -10,7 +10,9 @@ import SwiftUI
|
|||
struct PreferencesView: View {
|
||||
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 {
|
||||
NavigationView {
|
||||
|
@ -76,6 +78,11 @@ struct PreferencesView: View {
|
|||
Section(header: Text("Behavior")) {
|
||||
Toggle("Pull to Refresh", isOn: $preferences.pullToRefreshEnabled)
|
||||
Toggle("Hide Toolbars When Scrolling", isOn: $preferences.hideToolbarsWhenScrolling)
|
||||
NavigationLink {
|
||||
ToolbarPrefView(model: toolbarViewModel)
|
||||
} label: {
|
||||
Text("Customize Toolbar")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,336 @@
|
|||
//
|
||||
// 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)
|
||||
.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))
|
||||
.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 || 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())
|
||||
}
|
||||
}
|
|
@ -18,12 +18,8 @@ class ToolbarView: UIView {
|
|||
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 buttonsStack: UIStackView!
|
||||
private var toolbarButtons: [ToolbarItem: UIButton] = [:]
|
||||
|
||||
private var cancellables = [AnyCancellable]()
|
||||
|
||||
|
@ -45,85 +41,35 @@ class ToolbarView: UIView {
|
|||
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)
|
||||
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([
|
||||
stack.leadingAnchor.constraint(equalTo: leadingAnchor),
|
||||
stack.trailingAnchor.constraint(equalTo: trailingAnchor),
|
||||
stack.topAnchor.constraint(equalTo: topAnchor, constant: 5),
|
||||
buttonsStack.leadingAnchor.constraint(equalTo: leadingAnchor),
|
||||
buttonsStack.trailingAnchor.constraint(equalTo: trailingAnchor),
|
||||
buttonsStack.topAnchor.constraint(equalTo: topAnchor, constant: 5),
|
||||
safeAreaConstraint,
|
||||
stack.bottomAnchor.constraint(lessThanOrEqualTo: bottomAnchor, constant: -8)
|
||||
buttonsStack.bottomAnchor.constraint(lessThanOrEqualTo: bottomAnchor, constant: -8)
|
||||
])
|
||||
|
||||
updateNavigationButtons()
|
||||
|
||||
navigator.navigationOperation
|
||||
.sink { (_) in
|
||||
.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) {
|
||||
|
@ -136,10 +82,64 @@ class ToolbarView: UIView {
|
|||
border.backgroundColor = UIColor(white: traitCollection.userInterfaceStyle == .dark ? 0.25 : 0.75, alpha: 1)
|
||||
}
|
||||
|
||||
private func updateNavigationButtons() {
|
||||
backButton.isEnabled = navigator.backStack.count > 0
|
||||
forwardsButton.isEnabled = navigator.forwardStack.count > 0
|
||||
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
|
||||
|
@ -155,7 +155,12 @@ class ToolbarView: UIView {
|
|||
}
|
||||
}
|
||||
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, *),
|
||||
|
@ -172,25 +177,31 @@ class ToolbarView: UIView {
|
|||
forwardsButton.menu = UIMenu(children: forward)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func tableOfContentsPressed() {
|
||||
showTableOfContents?()
|
||||
}
|
||||
|
||||
@objc private func sharePressed() {
|
||||
showShareSheet?(shareButton)
|
||||
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, 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
|
||||
let children = self.navigator.backStack.suffix(5).enumerated().map { (index, entry) 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)
|
||||
}
|
||||
} else if interaction.view == forwardsButton {
|
||||
} 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
|
||||
|
|
|
@ -37,12 +37,14 @@
|
|||
D62664F024BC0D7700DF9B88 /* GeminiFormat.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D62664A824BBF26A00DF9B88 /* GeminiFormat.framework */; };
|
||||
D62664FA24BC12BC00DF9B88 /* DocumentTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D62664F924BC12BC00DF9B88 /* DocumentTests.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 */; };
|
||||
D653F40D26799F2F004E32B1 /* HomepagePrefView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D653F40C26799F2F004E32B1 /* HomepagePrefView.swift */; };
|
||||
D653F40F2679A0AB004E32B1 /* SetHomepageActivity.swift in Sources */ = {isa = PBXBuildFile; fileRef = D653F40E2679A0AB004E32B1 /* SetHomepageActivity.swift */; };
|
||||
D664673624BD07F700B0B741 /* RenderingBlock.swift in Sources */ = {isa = PBXBuildFile; fileRef = D664673524BD07F700B0B741 /* RenderingBlock.swift */; };
|
||||
D664673824BD086F00B0B741 /* RenderingBlockView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D664673724BD086F00B0B741 /* RenderingBlockView.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 */; };
|
||||
D688F5FF258ACE6B003A0A73 /* browser.css in Resources */ = {isa = PBXBuildFile; fileRef = D688F5FE258ACE6B003A0A73 /* browser.css */; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
|
@ -704,8 +708,10 @@
|
|||
D6BC9ABB258E9862008652BC /* NavigationBarView.swift */,
|
||||
D6BC9AD6258FC8B3008652BC /* TableOfContentsView.swift */,
|
||||
D691A64D25217C6F00348C4B /* Preferences.swift */,
|
||||
D664E4F92713DF72005BAF55 /* ToolbarItem.swift */,
|
||||
D691A66625217FD800348C4B /* PreferencesView.swift */,
|
||||
D653F40C26799F2F004E32B1 /* HomepagePrefView.swift */,
|
||||
D640A2312711DC7700177E85 /* ToolbarPrefView.swift */,
|
||||
D653F40A267996FF004E32B1 /* ActivityItemSource.swift */,
|
||||
D653F40E2679A0AB004E32B1 /* SetHomepageActivity.swift */,
|
||||
D68C1E1A27055EA0002D642B /* Intents */,
|
||||
|
@ -1282,6 +1288,7 @@
|
|||
D688F599258ACAAE003A0A73 /* BrowserWebViewController.swift in Sources */,
|
||||
D688F633258B09BB003A0A73 /* TrackpadScrollGestureRecognizer.swift in Sources */,
|
||||
D6E152A524BFFDF500FDF9D3 /* AppDelegate.swift in Sources */,
|
||||
D640A2322711DC7700177E85 /* ToolbarPrefView.swift in Sources */,
|
||||
D6E152A724BFFDF500FDF9D3 /* SceneDelegate.swift in Sources */,
|
||||
D68C1E35270615D3002D642B /* UserActivities.swift in Sources */,
|
||||
D6376A7026DDAF65005AD89C /* URIFixup.swift in Sources */,
|
||||
|
@ -1291,6 +1298,7 @@
|
|||
D688F64A258C17F3003A0A73 /* SymbolCache.swift in Sources */,
|
||||
D653F40F2679A0AB004E32B1 /* SetHomepageActivity.swift in Sources */,
|
||||
D688F65A258C2256003A0A73 /* BrowserNavigationController.swift in Sources */,
|
||||
D664E4FA2713DF72005BAF55 /* ToolbarItem.swift in Sources */,
|
||||
D6BC9AD7258FC8B3008652BC /* TableOfContentsView.swift in Sources */,
|
||||
D68C1E1927055E09002D642B /* Intents.intentdefinition in Sources */,
|
||||
D688F663258C2479003A0A73 /* UIViewController+Children.swift in Sources */,
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
<key>BrowserCore.xcscheme_^#shared#^_</key>
|
||||
<dict>
|
||||
<key>orderHint</key>
|
||||
<integer>5</integer>
|
||||
<integer>4</integer>
|
||||
</dict>
|
||||
<key>Gemini-iOS.xcscheme_^#shared#^_</key>
|
||||
<dict>
|
||||
|
@ -32,12 +32,12 @@
|
|||
<key>GeminiProtocol.xcscheme_^#shared#^_</key>
|
||||
<dict>
|
||||
<key>orderHint</key>
|
||||
<integer>6</integer>
|
||||
<integer>5</integer>
|
||||
</dict>
|
||||
<key>GeminiRenderer.xcscheme_^#shared#^_</key>
|
||||
<dict>
|
||||
<key>orderHint</key>
|
||||
<integer>4</integer>
|
||||
<integer>6</integer>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>SuppressBuildableAutocreation</key>
|
||||
|
|
Loading…
Reference in New Issue