| // Copyright 2018 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "ui/base/cocoa/text_services_context_menu.h" |
| |
| #import <AppKit/AppKit.h> |
| |
| #include <utility> |
| |
| #include "base/mac/mac_util.h" |
| #include "base/strings/sys_string_conversions.h" |
| #include "ui/base/l10n/l10n_util.h" |
| #include "ui/strings/grit/ui_strings.h" |
| |
| namespace { |
| |
| // Returns the TextDirection associated associated with the given BiDi |
| // |command_id|. |
| base::i18n::TextDirection GetTextDirectionFromCommandId(int command_id) { |
| switch (command_id) { |
| case ui::TextServicesContextMenu::kWritingDirectionDefault: |
| return base::i18n::UNKNOWN_DIRECTION; |
| case ui::TextServicesContextMenu::kWritingDirectionLtr: |
| return base::i18n::LEFT_TO_RIGHT; |
| case ui::TextServicesContextMenu::kWritingDirectionRtl: |
| return base::i18n::RIGHT_TO_LEFT; |
| default: |
| NOTREACHED(); |
| } |
| } |
| |
| } // namespace |
| |
| namespace ui { |
| |
| TextServicesContextMenu::TextServicesContextMenu(Delegate* delegate) |
| : speech_submenu_model_(this), |
| bidi_submenu_model_(this), |
| delegate_(delegate) { |
| DCHECK(delegate); |
| |
| speech_submenu_model_.AddItemWithStringId(kSpeechStartSpeaking, |
| IDS_SPEECH_START_SPEAKING_MAC); |
| speech_submenu_model_.AddItemWithStringId(kSpeechStopSpeaking, |
| IDS_SPEECH_STOP_SPEAKING_MAC); |
| |
| bidi_submenu_model_.AddCheckItemWithStringId( |
| kWritingDirectionDefault, IDS_CONTENT_CONTEXT_WRITING_DIRECTION_DEFAULT); |
| bidi_submenu_model_.AddCheckItemWithStringId( |
| kWritingDirectionLtr, IDS_CONTENT_CONTEXT_WRITING_DIRECTION_LTR); |
| bidi_submenu_model_.AddCheckItemWithStringId( |
| kWritingDirectionRtl, IDS_CONTENT_CONTEXT_WRITING_DIRECTION_RTL); |
| } |
| |
| // A note about the Speech submenu. |
| // |
| // All standard AppKit implementations of `-(IBAction)startSpeaking:(id)sender` |
| // and `-(IBAction)stopSpeaking:(id)sender` funnel into messages to |
| // `NSApplication`: |
| |
| } // namespace ui |
| |
| @interface NSApplication (Speech) |
| - (void)speakString:(NSString*)string; |
| - (IBAction)stopSpeaking:(id)sender; |
| - (BOOL)isSpeaking; |
| @end |
| |
| namespace ui { |
| |
| // When running on an OS release earlier than macOS 14, or running on macOS 14.4 |
| // and later, do this as well, for two reasons: |
| // |
| // 1. Interoperability with the other parts of the system that use this same |
| // speech synthesizer. |
| // |
| // 2. Working around a bug in `AVSpeechSynthesizer` which does not provide the |
| // correct voice when a specific voice is chosen in the system accessibility |
| // settings (see https://crbug.com/40072850#comment10, FB13197951). |
| // |
| // However, for macOS 14.0 through 14.3, directly use the deprecated |
| // NSSpeechSynthesizer class, as there is a bug with the NSApplication provided |
| // methods that causes occasional hiccups in the audio (see |
| // https://crbug.com/40074199, FB13261400). |
| |
| #pragma clang diagnostic push |
| #pragma clang diagnostic ignored "-Wdeprecated-declarations" |
| |
| namespace FB13261400Workaround { |
| |
| NSSpeechSynthesizer* SharedNSSpeechSynthesizer() { |
| static NSSpeechSynthesizer* speech_synthesizer = |
| [[NSSpeechSynthesizer alloc] initWithVoice:nil]; |
| return speech_synthesizer; |
| } |
| |
| bool IsSpeaking() { |
| return SharedNSSpeechSynthesizer().speaking; |
| } |
| |
| void StopSpeaking() { |
| [SharedNSSpeechSynthesizer() stopSpeaking]; |
| } |
| |
| void SpeakText(const std::u16string& text) { |
| if (IsSpeaking()) { |
| StopSpeaking(); |
| } |
| |
| [SharedNSSpeechSynthesizer() |
| startSpeakingString:base::SysUTF16ToNSString(text)]; |
| } |
| |
| } // namespace FB13261400Workaround |
| |
| #pragma clang diagnostic pop |
| |
| void TextServicesContextMenu::SpeakText(const std::u16string& text) { |
| int version = base::mac::MacOSVersion(); |
| if (version >= 14'00'00 && version < 14'04'00) { |
| FB13261400Workaround::SpeakText(text); |
| } else { |
| [NSApp speakString:base::SysUTF16ToNSString(text)]; |
| } |
| } |
| |
| void TextServicesContextMenu::StopSpeaking() { |
| int version = base::mac::MacOSVersion(); |
| if (version >= 14'00'00 && version < 14'04'00) { |
| FB13261400Workaround::StopSpeaking(); |
| } else { |
| [NSApp stopSpeaking:nil]; |
| } |
| } |
| |
| bool TextServicesContextMenu::IsSpeaking() { |
| int version = base::mac::MacOSVersion(); |
| if (version >= 14'00'00 && version < 14'04'00) { |
| return FB13261400Workaround::IsSpeaking(); |
| } else { |
| return [NSApp isSpeaking]; |
| } |
| } |
| |
| void TextServicesContextMenu::AppendToContextMenu(SimpleMenuModel* model) { |
| model->AddSeparator(NORMAL_SEPARATOR); |
| model->AddSubMenuWithStringId(kSpeechMenu, IDS_SPEECH_MAC, |
| &speech_submenu_model_); |
| } |
| |
| void TextServicesContextMenu::AppendEditableItems(SimpleMenuModel* model) { |
| // MacOS provides a contextual menu to set writing direction for BiDi |
| // languages. This functionality is exposed as a keyboard shortcut on |
| // Windows and Linux. |
| model->AddSubMenuWithStringId(kWritingDirectionMenu, |
| IDS_CONTENT_CONTEXT_WRITING_DIRECTION_MENU, |
| &bidi_submenu_model_); |
| } |
| |
| bool TextServicesContextMenu::SupportsCommand(int command_id) const { |
| switch (command_id) { |
| case kWritingDirectionMenu: |
| case kWritingDirectionDefault: |
| case kWritingDirectionLtr: |
| case kWritingDirectionRtl: |
| case kSpeechMenu: |
| case kSpeechStartSpeaking: |
| case kSpeechStopSpeaking: |
| return true; |
| } |
| |
| return false; |
| } |
| |
| bool TextServicesContextMenu::IsCommandIdChecked(int command_id) const { |
| switch (command_id) { |
| case kWritingDirectionDefault: |
| case kWritingDirectionLtr: |
| case kWritingDirectionRtl: |
| return delegate_->IsTextDirectionChecked( |
| GetTextDirectionFromCommandId(command_id)); |
| case kSpeechStartSpeaking: |
| case kSpeechStopSpeaking: |
| return false; |
| } |
| |
| NOTREACHED(); |
| } |
| |
| bool TextServicesContextMenu::IsCommandIdEnabled(int command_id) const { |
| switch (command_id) { |
| case kSpeechMenu: |
| case kWritingDirectionMenu: |
| return true; |
| case kWritingDirectionDefault: |
| case kWritingDirectionLtr: |
| case kWritingDirectionRtl: |
| return delegate_->IsTextDirectionEnabled( |
| GetTextDirectionFromCommandId(command_id)); |
| case kSpeechStartSpeaking: |
| return true; |
| case kSpeechStopSpeaking: |
| return IsSpeaking(); |
| } |
| |
| NOTREACHED(); |
| } |
| |
| void TextServicesContextMenu::ExecuteCommand(int command_id, int event_flags) { |
| switch (command_id) { |
| case kWritingDirectionDefault: |
| case kWritingDirectionLtr: |
| case kWritingDirectionRtl: |
| delegate_->UpdateTextDirection(GetTextDirectionFromCommandId(command_id)); |
| break; |
| case kSpeechStartSpeaking: |
| SpeakText(delegate_->GetSelectedText()); |
| break; |
| case kSpeechStopSpeaking: |
| StopSpeaking(); |
| break; |
| default: |
| NOTREACHED(); |
| } |
| } |
| |
| } // namespace ui |