From 5c6618f13e1ea43c0f5e386405c8f2d5ad66e616 Mon Sep 17 00:00:00 2001 From: CMK Date: Wed, 19 May 2021 16:45:01 +0800 Subject: [PATCH] feat: add keyboard shortcuts for settings and favorites --- Localization/app.json | 13 ++- Mastodon/Extension/UIViewController.swift | 13 +++ Mastodon/Generated/Strings.swift | 22 +++- .../Resources/ar.lproj/Localizable.strings | 5 +- .../Resources/en.lproj/Localizable.strings | 5 +- .../Scene/MainTab/MainTabBarController.swift | 110 +++++++++++++++++- .../Settings/SettingsViewController.swift | 35 ++++++ 7 files changed, 186 insertions(+), 17 deletions(-) diff --git a/Localization/app.json b/Localization/app.json index c55b28f21..9514fbd39 100644 --- a/Localization/app.json +++ b/Localization/app.json @@ -78,9 +78,13 @@ "home": "Home", "search": "Search", "notification": "Notification", - "profile": "Profile", - "keyboard": { - "switch_to_tab": "Switch to %s" + "profile": "Profile" + }, + "keyboard": { + "common": { + "switch_to_tab": "Switch to %s", + "show_favorites": "Show Favorites", + "open_settings": "Open Settings" } }, "status": { @@ -480,6 +484,9 @@ "clear": "Clear Media Cache", "signout": "Sign Out" } + }, + "keyboard": { + "close_settings_window": "Close Settings Window" } }, "report": { diff --git a/Mastodon/Extension/UIViewController.swift b/Mastodon/Extension/UIViewController.swift index 9ebb3a0a8..1fcfb41e4 100644 --- a/Mastodon/Extension/UIViewController.swift +++ b/Mastodon/Extension/UIViewController.swift @@ -112,3 +112,16 @@ extension UIViewController { } } + +extension UIViewController { + + /// https://stackoverflow.com/a/27301207/3797903 + var isModal: Bool { + let presentingIsModal = presentingViewController != nil + let presentingIsNavigation = navigationController != nil && navigationController?.presentingViewController?.presentedViewController == navigationController + let presentingIsTabBar = tabBarController?.presentingViewController is UITabBarController + + return presentingIsModal || presentingIsNavigation || presentingIsTabBar + } + +} diff --git a/Mastodon/Generated/Strings.swift b/Mastodon/Generated/Strings.swift index 5895b4780..5b7580b92 100644 --- a/Mastodon/Generated/Strings.swift +++ b/Mastodon/Generated/Strings.swift @@ -189,6 +189,18 @@ internal enum L10n { return L10n.tr("Localizable", "Common.Controls.Firendship.UnmuteUser", String(describing: p1)) } } + internal enum Keyboard { + internal enum Common { + /// Open Settings + internal static let openSettings = L10n.tr("Localizable", "Common.Controls.Keyboard.Common.OpenSettings") + /// Show Favorites + internal static let showFavorites = L10n.tr("Localizable", "Common.Controls.Keyboard.Common.ShowFavorites") + /// Switch to %@ + internal static func switchToTab(_ p1: Any) -> String { + return L10n.tr("Localizable", "Common.Controls.Keyboard.Common.SwitchToTab", String(describing: p1)) + } + } + } internal enum Status { /// content warning internal static let contentWarning = L10n.tr("Localizable", "Common.Controls.Status.ContentWarning") @@ -278,12 +290,6 @@ internal enum L10n { internal static let profile = L10n.tr("Localizable", "Common.Controls.Tabs.Profile") /// Search internal static let search = L10n.tr("Localizable", "Common.Controls.Tabs.Search") - internal enum Keyboard { - /// Switch to %@ - internal static func switchToTab(_ p1: Any) -> String { - return L10n.tr("Localizable", "Common.Controls.Tabs.Keyboard.SwitchToTab", String(describing: p1)) - } - } } internal enum Timeline { internal enum Accessibility { @@ -828,6 +834,10 @@ internal enum L10n { internal enum Settings { /// Settings internal static let title = L10n.tr("Localizable", "Scene.Settings.Title") + internal enum Keyboard { + /// Close Settings Window + internal static let closeSettingsWindow = L10n.tr("Localizable", "Scene.Settings.Keyboard.CloseSettingsWindow") + } internal enum Section { internal enum Appearance { /// Automatic diff --git a/Mastodon/Resources/ar.lproj/Localizable.strings b/Mastodon/Resources/ar.lproj/Localizable.strings index e56fffcd3..5b3a62ff8 100644 --- a/Mastodon/Resources/ar.lproj/Localizable.strings +++ b/Mastodon/Resources/ar.lproj/Localizable.strings @@ -64,6 +64,9 @@ Please check your internet connection."; "Common.Controls.Firendship.UnblockUser" = "Unblock %@"; "Common.Controls.Firendship.Unmute" = "Unmute"; "Common.Controls.Firendship.UnmuteUser" = "Unmute %@"; +"Common.Controls.Keyboard.Common.OpenSettings" = "Open Settings"; +"Common.Controls.Keyboard.Common.ShowFavorites" = "Show Favorites"; +"Common.Controls.Keyboard.Common.SwitchToTab" = "Switch to %@"; "Common.Controls.Status.Actions.Favorite" = "Favorite"; "Common.Controls.Status.Actions.Menu" = "Menu"; "Common.Controls.Status.Actions.Reblog" = "Reblog"; @@ -91,7 +94,6 @@ Please check your internet connection."; "Common.Controls.Status.UserReblogged" = "%@ reblogged"; "Common.Controls.Status.UserRepliedTo" = "Replied to %@"; "Common.Controls.Tabs.Home" = "Home"; -"Common.Controls.Tabs.Keyboard.SwitchToTab" = "Switch to %@"; "Common.Controls.Tabs.Notification" = "Notification"; "Common.Controls.Tabs.Profile" = "Profile"; "Common.Controls.Tabs.Search" = "Search"; @@ -273,6 +275,7 @@ any server."; "Scene.ServerRules.Subtitle" = "These rules are set by the admins of %@."; "Scene.ServerRules.TermsOfService" = "terms of service"; "Scene.ServerRules.Title" = "Some ground rules."; +"Scene.Settings.Keyboard.CloseSettingsWindow" = "Close Settings Window"; "Scene.Settings.Section.Appearance.Automatic" = "Automatic"; "Scene.Settings.Section.Appearance.Dark" = "Always Dark"; "Scene.Settings.Section.Appearance.Light" = "Always Light"; diff --git a/Mastodon/Resources/en.lproj/Localizable.strings b/Mastodon/Resources/en.lproj/Localizable.strings index e56fffcd3..5b3a62ff8 100644 --- a/Mastodon/Resources/en.lproj/Localizable.strings +++ b/Mastodon/Resources/en.lproj/Localizable.strings @@ -64,6 +64,9 @@ Please check your internet connection."; "Common.Controls.Firendship.UnblockUser" = "Unblock %@"; "Common.Controls.Firendship.Unmute" = "Unmute"; "Common.Controls.Firendship.UnmuteUser" = "Unmute %@"; +"Common.Controls.Keyboard.Common.OpenSettings" = "Open Settings"; +"Common.Controls.Keyboard.Common.ShowFavorites" = "Show Favorites"; +"Common.Controls.Keyboard.Common.SwitchToTab" = "Switch to %@"; "Common.Controls.Status.Actions.Favorite" = "Favorite"; "Common.Controls.Status.Actions.Menu" = "Menu"; "Common.Controls.Status.Actions.Reblog" = "Reblog"; @@ -91,7 +94,6 @@ Please check your internet connection."; "Common.Controls.Status.UserReblogged" = "%@ reblogged"; "Common.Controls.Status.UserRepliedTo" = "Replied to %@"; "Common.Controls.Tabs.Home" = "Home"; -"Common.Controls.Tabs.Keyboard.SwitchToTab" = "Switch to %@"; "Common.Controls.Tabs.Notification" = "Notification"; "Common.Controls.Tabs.Profile" = "Profile"; "Common.Controls.Tabs.Search" = "Search"; @@ -273,6 +275,7 @@ any server."; "Scene.ServerRules.Subtitle" = "These rules are set by the admins of %@."; "Scene.ServerRules.TermsOfService" = "terms of service"; "Scene.ServerRules.Title" = "Some ground rules."; +"Scene.Settings.Keyboard.CloseSettingsWindow" = "Close Settings Window"; "Scene.Settings.Section.Appearance.Automatic" = "Automatic"; "Scene.Settings.Section.Appearance.Dark" = "Always Dark"; "Scene.Settings.Section.Appearance.Light" = "Always Light"; diff --git a/Mastodon/Scene/MainTab/MainTabBarController.swift b/Mastodon/Scene/MainTab/MainTabBarController.swift index fe19640ed..b102d309e 100644 --- a/Mastodon/Scene/MainTab/MainTabBarController.swift +++ b/Mastodon/Scene/MainTab/MainTabBarController.swift @@ -190,28 +190,126 @@ extension MainTabBarController { } - +// HIG: keyboard UX +// https://developer.apple.com/design/human-interface-guidelines/macos/user-interaction/keyboard/ extension MainTabBarController { - override var keyCommands: [UIKeyCommand]? { + var switchToTabKeyCommands: [UIKeyCommand] { var commands: [UIKeyCommand] = [] for (i, tab) in Tab.allCases.enumerated() { - let title = L10n.Common.Controls.Tabs.Keyboard.switchToTab(tab.title) + let title = L10n.Common.Controls.Keyboard.Common.switchToTab(tab.title) let input = String(i + 1) - let command = UIKeyCommand(title: title, image: nil, action: #selector(MainTabBarController.switchToTab(_:)), input: input, modifierFlags: .control, propertyList: tab.rawValue, alternates: [], discoverabilityTitle: nil, attributes: [], state: .off) + let command = UIKeyCommand( + title: title, + image: nil, + action: #selector(MainTabBarController.switchToTabKeyCommandHandler(_:)), + input: input, + modifierFlags: .command, + propertyList: tab.rawValue, + alternates: [], + discoverabilityTitle: nil, + attributes: [], + state: .off + ) commands.append(command) - } return commands } - @objc private func switchToTab(_ sender: UIKeyCommand) { + var showFavoritesKeyCommand: UIKeyCommand { + UIKeyCommand( + title: L10n.Common.Controls.Keyboard.Common.showFavorites, + image: nil, + action: #selector(MainTabBarController.showFavoritesKeyCommandHandler(_:)), + input: "f", + modifierFlags: .command, + propertyList: nil, + alternates: [], + discoverabilityTitle: nil, + attributes: [], + state: .off + ) + } + + var openSettingsKeyCommand: UIKeyCommand { + UIKeyCommand( + title: L10n.Common.Controls.Keyboard.Common.openSettings, + image: nil, + action: #selector(MainTabBarController.openSettingsKeyCommandHandler(_:)), + input: ",", + modifierFlags: .command, + propertyList: nil, + alternates: [], + discoverabilityTitle: nil, + attributes: [], + state: .off + ) + } + + override var keyCommands: [UIKeyCommand]? { + guard let topMost = self.topMost else { + return [] + } + + var commands: [UIKeyCommand] = [] + + if topMost.isModal { + + } else { + // switch tabs + commands.append(contentsOf: switchToTabKeyCommands) + + // show favorites + if !(self.topMost is FavoriteViewController) { + commands.append(showFavoritesKeyCommand) + } + + // open settings + if context.settingService.currentSetting.value != nil { + commands.append(openSettingsKeyCommand) + } + } + + return commands + } + + @objc private func switchToTabKeyCommandHandler(_ sender: UIKeyCommand) { guard let rawValue = sender.propertyList as? Int, let tab = Tab(rawValue: rawValue) else { return } os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: %s", ((#file as NSString).lastPathComponent), #line, #function, tab.title) guard let index = Tab.allCases.firstIndex(of: tab) else { return } + let previousTab = Tab(rawValue: selectedIndex) selectedIndex = index + + if let previousTab = previousTab { + switch (tab, previousTab) { + case (.home, .home): + guard let navigationController = topMost?.navigationController else { return } + if navigationController.viewControllers.count > 1 { + // pop to top when previous tab position already is home + navigationController.popToRootViewController(animated: true) + } else if let homeTimelineViewController = topMost as? HomeTimelineViewController { + // trigger scrollToTop if topMost is home timeline + homeTimelineViewController.scrollToTop(animated: true) + } + default: + break + } + } + } + + @objc private func showFavoritesKeyCommandHandler(_ sender: UIKeyCommand) { + os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) + let favoriteViewModel = FavoriteViewModel(context: context) + coordinator.present(scene: .favorite(viewModel: favoriteViewModel), from: nil, transition: .show) + } + + @objc private func openSettingsKeyCommandHandler(_ sender: UIKeyCommand) { + os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) + guard let setting = context.settingService.currentSetting.value else { return } + let settingsViewModel = SettingsViewModel(context: context, setting: setting) + coordinator.present(scene: .settings(viewModel: settingsViewModel), from: nil, transition: .modal(animated: true, completion: nil)) } } diff --git a/Mastodon/Scene/Settings/SettingsViewController.swift b/Mastodon/Scene/Settings/SettingsViewController.swift index 1c69ef6c5..0ffb09cfb 100644 --- a/Mastodon/Scene/Settings/SettingsViewController.swift +++ b/Mastodon/Scene/Settings/SettingsViewController.swift @@ -439,6 +439,41 @@ extension SettingsViewController: ActiveLabelDelegate { } } +// MARK: - UIAdaptivePresentationControllerDelegate +extension SettingsViewController: UIAdaptivePresentationControllerDelegate { + func adaptivePresentationStyle(for controller: UIPresentationController, traitCollection: UITraitCollection) -> UIModalPresentationStyle { + return .formSheet + } +} + +extension SettingsViewController { + + var closeKeyCommand: UIKeyCommand { + UIKeyCommand( + title: L10n.Scene.Settings.Keyboard.closeSettingsWindow, + image: nil, + action: #selector(SettingsViewController.closeSettingsWindowKeyCommandHandler(_:)), + input: "w", + modifierFlags: .command, + propertyList: nil, + alternates: [], + discoverabilityTitle: nil, + attributes: [], + state: .off + ) + } + + override var keyCommands: [UIKeyCommand]? { + return [closeKeyCommand] + } + + @objc private func closeSettingsWindowKeyCommandHandler(_ sender: UIKeyCommand) { + os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) + dismiss(animated: true, completion: nil) + } + +} + #if canImport(SwiftUI) && DEBUG import SwiftUI