From 0bcfc14aa6524c3fe52e4e27dfe9e45ce3bdc40a Mon Sep 17 00:00:00 2001 From: CMK Date: Fri, 21 May 2021 19:12:01 +0800 Subject: [PATCH] feat: add keyboard shortcuts for compose scene --- Localization/app.json | 17 ++- Mastodon/Generated/Strings.swift | 30 +++- .../SegmentedControlNavigateable.swift | 4 +- .../Resources/ar.lproj/Localizable.strings | 11 +- .../Resources/en.lproj/Localizable.strings | 11 +- .../Scene/Compose/ComposeViewController.swift | 134 +++++++++++++++++- .../Scene/MainTab/MainTabBarController.swift | 26 ++++ 7 files changed, 218 insertions(+), 15 deletions(-) diff --git a/Localization/app.json b/Localization/app.json index ee2f763e2..8bd1316ab 100644 --- a/Localization/app.json +++ b/Localization/app.json @@ -87,10 +87,9 @@ "keyboard": { "common": { "switch_to_tab": "Switch to %s", + "compose_new_post": "Compose New Post", "show_favorites": "Show Favorites", - "open_settings": "Open Settings", - "previous_section": "Previous Section", - "next_section": "Next Section" + "open_settings": "Open Settings" }, "timeline": { "previous_status": "Previous Status", @@ -103,6 +102,10 @@ "toggle_favorite": "Toggle Status Favorite", "toggle_content_warning": "Toggle Content Warning", "preview_image": "Preview Image" + }, + "segmented_control": { + "previous_section": "Previous Section", + "next_section": "Next Section" } }, "status": { @@ -379,6 +382,14 @@ "post_visibility_menu": "Post visibility menu", "input_limit_remains_count": "Input limit remains %ld", "input_limit_exceeds_count": "Input limit exceeds %ld" + }, + "keyboard": { + "discard_post": "Discard Post", + "publish_post": "Publish Post", + "toggle_poll": "Toggle Poll", + "toggle_content_warning": "Toggle Content Warning", + "append_attachment_entry": "Append Attachment - %s", + "select_visibility_entry": "Select Visibility - %s" } }, "profile": { diff --git a/Mastodon/Generated/Strings.swift b/Mastodon/Generated/Strings.swift index 0b2ecfbfb..7253d6ef0 100644 --- a/Mastodon/Generated/Strings.swift +++ b/Mastodon/Generated/Strings.swift @@ -199,12 +199,10 @@ internal enum L10n { } internal enum Keyboard { internal enum Common { - /// Next Section - internal static let nextSection = L10n.tr("Localizable", "Common.Controls.Keyboard.Common.NextSection") + /// Compose New Post + internal static let composeNewPost = L10n.tr("Localizable", "Common.Controls.Keyboard.Common.ComposeNewPost") /// Open Settings internal static let openSettings = L10n.tr("Localizable", "Common.Controls.Keyboard.Common.OpenSettings") - /// Previous Section - internal static let previousSection = L10n.tr("Localizable", "Common.Controls.Keyboard.Common.PreviousSection") /// Show Favorites internal static let showFavorites = L10n.tr("Localizable", "Common.Controls.Keyboard.Common.ShowFavorites") /// Switch to %@ @@ -212,6 +210,12 @@ internal enum L10n { return L10n.tr("Localizable", "Common.Controls.Keyboard.Common.SwitchToTab", String(describing: p1)) } } + internal enum SegmentedControl { + /// Next Section + internal static let nextSection = L10n.tr("Localizable", "Common.Controls.Keyboard.SegmentedControl.NextSection") + /// Previous Section + internal static let previousSection = L10n.tr("Localizable", "Common.Controls.Keyboard.SegmentedControl.PreviousSection") + } internal enum Timeline { /// Next Status internal static let nextStatus = L10n.tr("Localizable", "Common.Controls.Keyboard.Timeline.NextStatus") @@ -436,6 +440,24 @@ internal enum L10n { /// Write an accurate warning here... internal static let placeholder = L10n.tr("Localizable", "Scene.Compose.ContentWarning.Placeholder") } + internal enum Keyboard { + /// Append Attachment - %@ + internal static func appendAttachmentEntry(_ p1: Any) -> String { + return L10n.tr("Localizable", "Scene.Compose.Keyboard.AppendAttachmentEntry", String(describing: p1)) + } + /// Discard Post + internal static let discardPost = L10n.tr("Localizable", "Scene.Compose.Keyboard.DiscardPost") + /// Publish Post + internal static let publishPost = L10n.tr("Localizable", "Scene.Compose.Keyboard.PublishPost") + /// Select Visibility - %@ + internal static func selectVisibilityEntry(_ p1: Any) -> String { + return L10n.tr("Localizable", "Scene.Compose.Keyboard.SelectVisibilityEntry", String(describing: p1)) + } + /// Toggle Content Warning + internal static let toggleContentWarning = L10n.tr("Localizable", "Scene.Compose.Keyboard.ToggleContentWarning") + /// Toggle Poll + internal static let togglePoll = L10n.tr("Localizable", "Scene.Compose.Keyboard.TogglePoll") + } internal enum MediaSelection { /// Browse internal static let browse = L10n.tr("Localizable", "Scene.Compose.MediaSelection.Browse") diff --git a/Mastodon/Protocol/SegmentedControlNavigateable.swift b/Mastodon/Protocol/SegmentedControlNavigateable.swift index 16ff25460..ed76de21f 100644 --- a/Mastodon/Protocol/SegmentedControlNavigateable.swift +++ b/Mastodon/Protocol/SegmentedControlNavigateable.swift @@ -27,8 +27,8 @@ enum SegmentedControlNavigationDirection: String, CaseIterable { var title: String { switch self { - case .previous: return L10n.Common.Controls.Keyboard.Common.previousSection - case .next: return L10n.Common.Controls.Keyboard.Common.nextSection + case .previous: return L10n.Common.Controls.Keyboard.SegmentedControl.previousSection + case .next: return L10n.Common.Controls.Keyboard.SegmentedControl.nextSection } } diff --git a/Mastodon/Resources/ar.lproj/Localizable.strings b/Mastodon/Resources/ar.lproj/Localizable.strings index 794f75ba0..ceb690459 100644 --- a/Mastodon/Resources/ar.lproj/Localizable.strings +++ b/Mastodon/Resources/ar.lproj/Localizable.strings @@ -68,11 +68,12 @@ Please check your internet connection."; "Common.Controls.Firendship.UnblockUser" = "Unblock %@"; "Common.Controls.Firendship.Unmute" = "Unmute"; "Common.Controls.Firendship.UnmuteUser" = "Unmute %@"; -"Common.Controls.Keyboard.Common.NextSection" = "Next Section"; +"Common.Controls.Keyboard.Common.ComposeNewPost" = "Compose New Post"; "Common.Controls.Keyboard.Common.OpenSettings" = "Open Settings"; -"Common.Controls.Keyboard.Common.PreviousSection" = "Previous Section"; "Common.Controls.Keyboard.Common.ShowFavorites" = "Show Favorites"; "Common.Controls.Keyboard.Common.SwitchToTab" = "Switch to %@"; +"Common.Controls.Keyboard.SegmentedControl.NextSection" = "Next Section"; +"Common.Controls.Keyboard.SegmentedControl.PreviousSection" = "Previous Section"; "Common.Controls.Keyboard.Timeline.NextStatus" = "Next Status"; "Common.Controls.Keyboard.Timeline.OpenAuthorProfile" = "Open Author Profile"; "Common.Controls.Keyboard.Timeline.OpenRebloggerProfile" = "Open Reblogger Profile"; @@ -149,6 +150,12 @@ uploaded to Mastodon."; "Scene.Compose.ComposeAction" = "Publish"; "Scene.Compose.ContentInputPlaceholder" = "Type or paste what's on your mind"; "Scene.Compose.ContentWarning.Placeholder" = "Write an accurate warning here..."; +"Scene.Compose.Keyboard.AppendAttachmentEntry" = "Append Attachment - %@"; +"Scene.Compose.Keyboard.DiscardPost" = "Discard Post"; +"Scene.Compose.Keyboard.PublishPost" = "Publish Post"; +"Scene.Compose.Keyboard.SelectVisibilityEntry" = "Select Visibility - %@"; +"Scene.Compose.Keyboard.ToggleContentWarning" = "Toggle Content Warning"; +"Scene.Compose.Keyboard.TogglePoll" = "Toggle Poll"; "Scene.Compose.MediaSelection.Browse" = "Browse"; "Scene.Compose.MediaSelection.Camera" = "Take Photo"; "Scene.Compose.MediaSelection.PhotoLibrary" = "Photo Library"; diff --git a/Mastodon/Resources/en.lproj/Localizable.strings b/Mastodon/Resources/en.lproj/Localizable.strings index 794f75ba0..ceb690459 100644 --- a/Mastodon/Resources/en.lproj/Localizable.strings +++ b/Mastodon/Resources/en.lproj/Localizable.strings @@ -68,11 +68,12 @@ Please check your internet connection."; "Common.Controls.Firendship.UnblockUser" = "Unblock %@"; "Common.Controls.Firendship.Unmute" = "Unmute"; "Common.Controls.Firendship.UnmuteUser" = "Unmute %@"; -"Common.Controls.Keyboard.Common.NextSection" = "Next Section"; +"Common.Controls.Keyboard.Common.ComposeNewPost" = "Compose New Post"; "Common.Controls.Keyboard.Common.OpenSettings" = "Open Settings"; -"Common.Controls.Keyboard.Common.PreviousSection" = "Previous Section"; "Common.Controls.Keyboard.Common.ShowFavorites" = "Show Favorites"; "Common.Controls.Keyboard.Common.SwitchToTab" = "Switch to %@"; +"Common.Controls.Keyboard.SegmentedControl.NextSection" = "Next Section"; +"Common.Controls.Keyboard.SegmentedControl.PreviousSection" = "Previous Section"; "Common.Controls.Keyboard.Timeline.NextStatus" = "Next Status"; "Common.Controls.Keyboard.Timeline.OpenAuthorProfile" = "Open Author Profile"; "Common.Controls.Keyboard.Timeline.OpenRebloggerProfile" = "Open Reblogger Profile"; @@ -149,6 +150,12 @@ uploaded to Mastodon."; "Scene.Compose.ComposeAction" = "Publish"; "Scene.Compose.ContentInputPlaceholder" = "Type or paste what's on your mind"; "Scene.Compose.ContentWarning.Placeholder" = "Write an accurate warning here..."; +"Scene.Compose.Keyboard.AppendAttachmentEntry" = "Append Attachment - %@"; +"Scene.Compose.Keyboard.DiscardPost" = "Discard Post"; +"Scene.Compose.Keyboard.PublishPost" = "Publish Post"; +"Scene.Compose.Keyboard.SelectVisibilityEntry" = "Select Visibility - %@"; +"Scene.Compose.Keyboard.ToggleContentWarning" = "Toggle Content Warning"; +"Scene.Compose.Keyboard.TogglePoll" = "Toggle Poll"; "Scene.Compose.MediaSelection.Browse" = "Browse"; "Scene.Compose.MediaSelection.Camera" = "Take Photo"; "Scene.Compose.MediaSelection.PhotoLibrary" = "Photo Library"; diff --git a/Mastodon/Scene/Compose/ComposeViewController.swift b/Mastodon/Scene/Compose/ComposeViewController.swift index b0d445d72..80a64064b 100644 --- a/Mastodon/Scene/Compose/ComposeViewController.swift +++ b/Mastodon/Scene/Compose/ComposeViewController.swift @@ -37,6 +37,8 @@ final class ComposeViewController: UIViewController, NeedsDependency { button.adjustsImageWhenHighlighted = false return button }() + + private(set) lazy var cancelBarButtonItem = UIBarButtonItem(title: L10n.Common.Controls.Actions.cancel, style: .plain, target: self, action: #selector(ComposeViewController.cancelBarButtonItemPressed(_:))) private(set) lazy var publishBarButtonItem: UIBarButtonItem = { let barButtonItem = UIBarButtonItem(customView: publishButton) return barButtonItem @@ -138,7 +140,7 @@ extension ComposeViewController { } .store(in: &disposeBag) view.backgroundColor = Asset.Scene.Compose.background.color - navigationItem.leftBarButtonItem = UIBarButtonItem(title: L10n.Common.Controls.Actions.cancel, style: .plain, target: self, action: #selector(ComposeViewController.cancelBarButtonItemPressed(_:))) + navigationItem.leftBarButtonItem = cancelBarButtonItem navigationItem.rightBarButtonItem = publishBarButtonItem publishButton.addTarget(self, action: #selector(ComposeViewController.publishBarButtonItemPressed(_:)), for: .touchUpInside) @@ -247,7 +249,8 @@ extension ComposeViewController { // adjust inset for auto-complete let autoCompleteTableViewBottomInset: CGFloat = { - let tableViewFrameInWindow = self.autoCompleteViewController.tableView.superview!.convert(self.autoCompleteViewController.tableView.frame, to: nil) + guard let superview = self.autoCompleteViewController.tableView.superview else { return .zero } + let tableViewFrameInWindow = superview.convert(self.autoCompleteViewController.tableView.frame, to: nil) let padding = tableViewFrameInWindow.maxY + self.composeToolbarView.frame.height + AutoCompleteViewController.chevronViewHeight - endFrame.minY return max(0, padding) }() @@ -1257,3 +1260,130 @@ extension ComposeViewController: AutoCompleteViewControllerDelegate { } } } + +extension ComposeViewController { + override var keyCommands: [UIKeyCommand]? { + composeKeyCommands + } +} + +extension ComposeViewController { + + enum ComposeKeyCommand: String, CaseIterable { + case discardPost + case publishPost + case mediaBrowse + case mediaPhotoLibrary + case mediaCamera + case togglePoll + case toggleContentWarning + case selectVisibilityPublic + case selectVisibilityUnlisted + case selectVisibilityPrivate + case selectVisibilityDirect + + var title: String { + switch self { + case .discardPost: return L10n.Scene.Compose.Keyboard.discardPost + case .publishPost: return L10n.Scene.Compose.Keyboard.publishPost + case .mediaBrowse: return L10n.Scene.Compose.Keyboard.appendAttachmentEntry(L10n.Scene.Compose.MediaSelection.browse) + case .mediaPhotoLibrary: return L10n.Scene.Compose.Keyboard.appendAttachmentEntry(L10n.Scene.Compose.MediaSelection.photoLibrary) + case .mediaCamera: return L10n.Scene.Compose.Keyboard.appendAttachmentEntry(L10n.Scene.Compose.MediaSelection.camera) + case .togglePoll: return L10n.Scene.Compose.Keyboard.togglePoll + case .toggleContentWarning: return L10n.Scene.Compose.Keyboard.toggleContentWarning + case .selectVisibilityPublic: return L10n.Scene.Compose.Keyboard.selectVisibilityEntry(L10n.Scene.Compose.Visibility.public) + case .selectVisibilityUnlisted: return L10n.Scene.Compose.Keyboard.selectVisibilityEntry(L10n.Scene.Compose.Visibility.unlisted) + case .selectVisibilityPrivate: return L10n.Scene.Compose.Keyboard.selectVisibilityEntry(L10n.Scene.Compose.Visibility.private) + case .selectVisibilityDirect: return L10n.Scene.Compose.Keyboard.selectVisibilityEntry(L10n.Scene.Compose.Visibility.direct) + } + } + + // UIKeyCommand input + var input: String { + switch self { + case .discardPost: return "w" // + command + case .publishPost: return "\r" // (enter) + command + case .mediaBrowse: return "b" // + option + command + case .mediaPhotoLibrary: return "p" // + option + command + case .mediaCamera: return "c" // + option + command + case .togglePoll: return "p" // + shift + command + case .toggleContentWarning: return "c" // + shift + command + case .selectVisibilityPublic: return "1" // + command + case .selectVisibilityUnlisted: return "2" // + command + case .selectVisibilityPrivate: return "3" // + command + case .selectVisibilityDirect: return "4" // + command + } + } + + var modifierFlags: UIKeyModifierFlags { + switch self { + case .discardPost: return [.command] + case .publishPost: return [.command] + case .mediaBrowse: return [.alternate, .command] + case .mediaPhotoLibrary: return [.alternate, .command] + case .mediaCamera: return [.alternate, .command] + case .togglePoll: return [.shift, .command] + case .toggleContentWarning: return [.shift, .command] + case .selectVisibilityPublic: return [.command] + case .selectVisibilityUnlisted: return [.command] + case .selectVisibilityPrivate: return [.command] + case .selectVisibilityDirect: return [.command] + } + } + + var propertyList: Any { + return rawValue + } + } + + var composeKeyCommands: [UIKeyCommand]? { + ComposeKeyCommand.allCases.map { command in + UIKeyCommand( + title: command.title, + image: nil, + action: #selector(Self.composeKeyCommandHandler(_:)), + input: command.input, + modifierFlags: command.modifierFlags, + propertyList: command.propertyList, + alternates: [], + discoverabilityTitle: nil, + attributes: [], + state: .off + ) + } + } + + @objc private func composeKeyCommandHandler(_ sender: UIKeyCommand) { + guard let rawValue = sender.propertyList as? String, + let command = ComposeKeyCommand(rawValue: rawValue) else { return } + + switch command { + case .discardPost: + cancelBarButtonItemPressed(cancelBarButtonItem) + case .publishPost: + publishBarButtonItemPressed(publishBarButtonItem) + case .mediaBrowse: + present(documentPickerController, animated: true, completion: nil) + case .mediaPhotoLibrary: + present(imagePicker, animated: true, completion: nil) + case .mediaCamera: + guard UIImagePickerController.isSourceTypeAvailable(.camera) else { + return + } + present(imagePickerController, animated: true, completion: nil) + case .togglePoll: + composeToolbarView.pollButton.sendActions(for: .touchUpInside) + case .toggleContentWarning: + composeToolbarView.contentWarningButton.sendActions(for: .touchUpInside) + case .selectVisibilityPublic: + viewModel.selectedStatusVisibility.value = .public + case .selectVisibilityUnlisted: + viewModel.selectedStatusVisibility.value = .unlisted + case .selectVisibilityPrivate: + viewModel.selectedStatusVisibility.value = .private + case .selectVisibilityDirect: + viewModel.selectedStatusVisibility.value = .direct + } + } + +} diff --git a/Mastodon/Scene/MainTab/MainTabBarController.swift b/Mastodon/Scene/MainTab/MainTabBarController.swift index b102d309e..2df0cdf31 100644 --- a/Mastodon/Scene/MainTab/MainTabBarController.swift +++ b/Mastodon/Scene/MainTab/MainTabBarController.swift @@ -246,6 +246,21 @@ extension MainTabBarController { ) } + var composeNewPostKeyCommand: UIKeyCommand { + UIKeyCommand( + title: L10n.Common.Controls.Keyboard.Common.composeNewPost, + image: nil, + action: #selector(MainTabBarController.composeNewPostKeyCommandHandler(_:)), + input: "n", + modifierFlags: .command, + propertyList: nil, + alternates: [], + discoverabilityTitle: nil, + attributes: [], + state: .off + ) + } + override var keyCommands: [UIKeyCommand]? { guard let topMost = self.topMost else { return [] @@ -259,6 +274,11 @@ extension MainTabBarController { // switch tabs commands.append(contentsOf: switchToTabKeyCommands) + // show compose + if !(self.topMost is ComposeViewController) { + commands.append(composeNewPostKeyCommand) + } + // show favorites if !(self.topMost is FavoriteViewController) { commands.append(showFavoritesKeyCommand) @@ -312,4 +332,10 @@ extension MainTabBarController { coordinator.present(scene: .settings(viewModel: settingsViewModel), from: nil, transition: .modal(animated: true, completion: nil)) } + @objc private func composeNewPostKeyCommandHandler(_ sender: UIKeyCommand) { + os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) + let composeViewModel = ComposeViewModel(context: context, composeKind: .post) + coordinator.present(scene: .compose(viewModel: composeViewModel), from: nil, transition: .modal(animated: true, completion: nil)) + } + }