// // KeyboardShortcuts.swift // App // // Created by James Magahern on 9/21/20. // import UIKit @objc protocol ShortcutResponder: AnyObject { @objc optional func focusURLBar(_ sender: Any?) @objc optional func focusWebView(_ sender: Any?) @objc optional func goBack(_ sender: Any?) @objc optional func goForward(_ sender: Any?) @objc optional func createTab(_ sender: Any?) @objc optional func previousTab(_ sender: Any?) @objc optional func nextTab(_ sender: Any?) @objc optional func closeTab(_ sender: Any?) @objc optional func findOnPage(_ sender: Any?) @objc optional func refresh(_ sender: Any?) @objc optional func stop(_ sender: Any?) @objc optional func showPreferences(_ sender: Any?) @objc optional func toggleDarkMode(_ sender: Any?) @objc optional func openInReaderMode(_ sender: Any?) @objc optional func showHistory(_ sender: Any?) @objc optional func handleOpenURL(_ sender: Any?, forEvent event: OpenURLEvent?) @objc optional func lowerScriptPolicyRestriction(_ sender: Any?) @objc optional func raiseScriptPolicyRestriction(_ sender: Any?) @objc optional func zoomToActualSize(_ sender: Any?) } public class OpenURLEvent: UIEvent { let url: URL public init(url: URL) { self.url = url } } fileprivate extension Array { func removeNulls() -> Array { self.filter { element in guard let keyCommand = element as? UIKeyCommand else { return true } return !keyCommand.isNull() } } } public class KeyboardShortcuts { public enum Category: CaseIterable { case application case file case go case view } public static func menu(for category: Category) -> [UIMenuElement] { switch category { case .application: return [ // Preferences UIKeyCommand( modifiers: .command, input: ",", title: "Preferences", action: #selector(ShortcutResponder.showPreferences) ) ] case .file: return [ // Open Location... UIKeyCommand( modifiers: .command, input: "L", title: "Open Location", action: #selector(ShortcutResponder.focusURLBar) ), // Open in Reader UIKeyCommand( modifiers: [ .command, .shift ], input: "R", title: "Open in Reader Mode…", action: #selector(ShortcutResponder.openInReaderMode) ), // Tabs UIMenu(options: .displayInline, children: [ // Create Tab UIKeyCommand( modifiers: .command, input: "T", title: "New Tab", action: #selector(ShortcutResponder.createTab) ), // Close tab UIKeyCommand( modifiers: [.command], input: "W", title: "Close Tab", action: #selector(ShortcutResponder.closeTab) ), ]), // Find on page (FindOnPageViewController.isEnabled() ? UIKeyCommand( modifiers: [.command], input: "F", title: "Find on Page", action: #selector(ShortcutResponder.findOnPage) ) : UIKeyCommand.null() ), UIMenu(options: .displayInline, children: [ // Refresh UIKeyCommand( modifiers: [.command], input: "R", title: "Refresh", action: #selector(ShortcutResponder.refresh) ), // Stop UIKeyCommand( modifiers: [.command], input: ".", title: "Stop", action: #selector(ShortcutResponder.stop) ) ]), UIMenu(options: .displayInline, children: [ // Raise Script Policy Restriction UIKeyCommand( modifiers: [.alternate], input: "x", title: "Raise Script Policy Restriction", action: #selector(ShortcutResponder.raiseScriptPolicyRestriction) ), // Lower Script Policy Restriction UIKeyCommand( modifiers: [.alternate], input: "c", title: "Lower Script Policy Restriction", action: #selector(ShortcutResponder.lowerScriptPolicyRestriction) ), ]) ].removeNulls() case .go: return [ // Focus Web View UIKeyCommand( modifiers: [ .command, .shift ], input: "w", title: "Focus Web View", action: #selector(ShortcutResponder.focusWebView) ), // Back/Forward UIMenu(options: .displayInline, children: [ // Go Back UIKeyCommand( modifiers: .command, input: "[", title: "Go Back", action: #selector(ShortcutResponder.goBack) ), // Go Forward UIKeyCommand( modifiers: .command, input: "]", title: "Go Forward", action: #selector(ShortcutResponder.goForward) ), ]), // Tab Navigation UIMenu(options: .displayInline, children: [ // Previous Tab UIKeyCommand( modifiers: [.command, .shift], input: "[", title: "Previous Tab", action: #selector(ShortcutResponder.previousTab) ), // Next Tab UIKeyCommand( modifiers: [.command, .shift], input: "]", title: "Next Tab", action: #selector(ShortcutResponder.nextTab) ), ]) ] case .view: return [ // Zoom UIMenu(options: .displayInline, children: [ // Actual Size UIKeyCommand( modifiers: .command, input: "0", title: "Actual Size", action: #selector(ShortcutResponder.zoomToActualSize) ), // Increase Zoom UIKeyCommand( modifiers: .command, input: "=", title: "Zoom In", action: #selector(UIResponder.increaseSize) ), // Go Forward UIKeyCommand( modifiers: .command, input: "-", title: "Zoom Out", action: #selector(UIResponder.decreaseSize) ), ]), // Toggle Dark Mode UIKeyCommand( modifiers: [.command], input: "M", title: "Toggle Dark Mode", action: #selector(ShortcutResponder.toggleDarkMode) ), ] } } public static func hiddenKeyCommands() -> [UIKeyCommand] { return [ // Go Back UIKeyCommand(input: UIKeyCommand.inputLeftArrow, modifierFlags: [.command, .control], action: #selector(ShortcutResponder.goBack)), // Go Forward UIKeyCommand(input: UIKeyCommand.inputRightArrow, modifierFlags: [.command, .control], action: #selector(ShortcutResponder.goForward)), ] } public static func allKeyCommands() -> [UIKeyCommand] { var commands: [UIKeyCommand] = [] for category in Category.allCases { let menuElements = menu(for: category) for element in menuElements { if let command = element as? UIKeyCommand { commands.append(command) } else if let menu = element as? UIMenu { commands.append(contentsOf: menu.children as! [UIKeyCommand]) } } } return commands + Self.hiddenKeyCommands() } }