| // Copyright 2023 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "ash/quick_insert/quick_insert_controller.h" |
| |
| #include <algorithm> |
| #include <memory> |
| #include <optional> |
| #include <string> |
| #include <string_view> |
| #include <utility> |
| #include <variant> |
| #include <vector> |
| |
| #include "ash/accessibility/accessibility_controller.h" |
| #include "ash/constants/ash_features.h" |
| #include "ash/constants/ash_pref_names.h" |
| #include "ash/constants/ash_switches.h" |
| #include "ash/public/cpp/clipboard_history_controller.h" |
| #include "ash/public/cpp/new_window_delegate.h" |
| #include "ash/public/cpp/shell_window_ids.h" |
| #include "ash/quick_insert/model/quick_insert_action_type.h" |
| #include "ash/quick_insert/model/quick_insert_caps_lock_position.h" |
| #include "ash/quick_insert/model/quick_insert_emoji_history_model.h" |
| #include "ash/quick_insert/model/quick_insert_emoji_suggester.h" |
| #include "ash/quick_insert/model/quick_insert_mode_type.h" |
| #include "ash/quick_insert/model/quick_insert_model.h" |
| #include "ash/quick_insert/model/quick_insert_search_results_section.h" |
| #include "ash/quick_insert/quick_insert_action_on_next_focus_request.h" |
| #include "ash/quick_insert/quick_insert_asset_fetcher.h" |
| #include "ash/quick_insert/quick_insert_asset_fetcher_impl.h" |
| #include "ash/quick_insert/quick_insert_caps_lock_bubble_controller.h" |
| #include "ash/quick_insert/quick_insert_client.h" |
| #include "ash/quick_insert/quick_insert_copy_media.h" |
| #include "ash/quick_insert/quick_insert_insert_media_request.h" |
| #include "ash/quick_insert/quick_insert_paste_request.h" |
| #include "ash/quick_insert/quick_insert_rich_media.h" |
| #include "ash/quick_insert/quick_insert_search_result.h" |
| #include "ash/quick_insert/quick_insert_suggestions_controller.h" |
| #include "ash/quick_insert/quick_insert_transform_case.h" |
| #include "ash/quick_insert/search/quick_insert_search_controller.h" |
| #include "ash/quick_insert/views/quick_insert_caps_lock_state_view.h" |
| #include "ash/quick_insert/views/quick_insert_icons.h" |
| #include "ash/quick_insert/views/quick_insert_positioning.h" |
| #include "ash/quick_insert/views/quick_insert_view.h" |
| #include "ash/quick_insert/views/quick_insert_view_delegate.h" |
| #include "ash/quick_insert/views/quick_insert_widget.h" |
| #include "ash/session/session_controller_impl.h" |
| #include "ash/shell.h" |
| #include "ash/strings/grit/ash_strings.h" |
| #include "ash/wm/window_util.h" |
| #include "base/check.h" |
| #include "base/check_is_test.h" |
| #include "base/command_line.h" |
| #include "base/functional/bind.h" |
| #include "base/hash/sha1.h" |
| #include "base/memory/scoped_refptr.h" |
| #include "base/memory/weak_ptr.h" |
| #include "base/notreached.h" |
| #include "base/strings/utf_string_conversions.h" |
| #include "components/prefs/pref_service.h" |
| #include "components/prefs/scoped_user_pref_update.h" |
| #include "services/network/public/cpp/shared_url_loader_factory.h" |
| #include "third_party/abseil-cpp/absl/functional/overload.h" |
| #include "ui/aura/client/focus_client.h" |
| #include "ui/aura/window.h" |
| #include "ui/base/emoji/emoji_panel_helper.h" |
| #include "ui/base/ime/ash/ime_bridge.h" |
| #include "ui/base/ime/ash/ime_keyboard.h" |
| #include "ui/base/ime/ash/input_method_manager.h" |
| #include "ui/base/ime/ash/text_input_target.h" |
| #include "ui/base/ime/input_method.h" |
| #include "ui/base/ime/text_input_client.h" |
| #include "ui/base/l10n/l10n_util.h" |
| #include "ui/display/screen.h" |
| #include "ui/events/ash/keyboard_capability.h" |
| #include "ui/events/devices/device_data_manager.h" |
| #include "ui/gfx/geometry/point.h" |
| #include "ui/gfx/geometry/rect.h" |
| |
| namespace ash { |
| |
| namespace { |
| |
| bool g_feature_tour_enabled = true; |
| |
| // When spoken feedback is enabled, closing the widget after an insert is |
| // delayed by this amount. |
| constexpr base::TimeDelta kCloseWidgetDelay = base::Milliseconds(200); |
| |
| // Time to wait for a focus event after triggering caps lock. |
| constexpr base::TimeDelta kCapsLockRequestTimeout = base::Seconds(1); |
| |
| constexpr int kCapsLockMinimumTopDisplayCount = 5; |
| constexpr float kCapsLockRatioThresholdForTop = 0.8; |
| constexpr float kCapsLockRatioThresholdForBottom = 0.2; |
| |
| constexpr std::string_view kSupportUrl = |
| "https://support.google.com/chromebook?p=dugong"; |
| |
| ui::TextInputClient* GetFocusedTextInputClient() { |
| const ui::InputMethod* input_method = |
| IMEBridge::Get()->GetInputContextHandler()->GetInputMethod(); |
| if (!input_method || !input_method->GetTextInputClient()) { |
| return nullptr; |
| } |
| return input_method->GetTextInputClient(); |
| } |
| |
| // Gets the current caret bounds in universal screen coordinates in DIP. Returns |
| // an empty rect if there is no active caret or the caret bounds can't be |
| // determined (e.g. no focused input field). |
| gfx::Rect GetCaretBounds() { |
| if (ui::TextInputClient* client = GetFocusedTextInputClient()) { |
| return client->GetCaretBounds(); |
| } |
| return gfx::Rect(); |
| } |
| |
| // Gets the current cursor point in universal screen coordinates in DIP. |
| gfx::Point GetCursorPoint() { |
| return display::Screen::Get()->GetCursorScreenPoint(); |
| } |
| |
| // Gets the bounds of the current focused window in universal screen coordinates |
| // in DIP. Returns an empty rect if there is no currently focused window. |
| gfx::Rect GetFocusedWindowBounds() { |
| return window_util::GetFocusedWindow() |
| ? window_util::GetFocusedWindow()->GetBoundsInScreen() |
| : gfx::Rect(); |
| } |
| |
| input_method::ImeKeyboard& GetImeKeyboard() { |
| auto* input_method_manager = input_method::InputMethodManager::Get(); |
| CHECK(input_method_manager); |
| input_method::ImeKeyboard* ime_keyboard = |
| input_method_manager->GetImeKeyboard(); |
| CHECK(ime_keyboard); |
| return *ime_keyboard; |
| } |
| |
| // The user can ask to insert rich media, a clipboard item, or insert nothing. |
| using InsertionContent = std:: |
| variant<QuickInsertRichMedia, QuickInsertClipboardResult, std::monostate>; |
| |
| InsertionContent GetInsertionContentForResult( |
| const QuickInsertSearchResult& result) { |
| using ReturnType = InsertionContent; |
| return std::visit( |
| absl::Overload{ |
| [](const QuickInsertTextResult& data) -> ReturnType { |
| return QuickInsertTextMedia(data.primary_text); |
| }, |
| [](const QuickInsertEmojiResult& data) -> ReturnType { |
| return QuickInsertTextMedia(data.text); |
| }, |
| [](const QuickInsertClipboardResult& data) -> ReturnType { |
| return data; |
| }, |
| [](const QuickInsertBrowsingHistoryResult& data) -> ReturnType { |
| return QuickInsertLinkMedia(data.url, |
| base::UTF16ToUTF8(data.title)); |
| }, |
| [](const QuickInsertGifResult& data) -> ReturnType { |
| return QuickInsertImageMedia(data.full_url, data.full_dimensions, |
| data.content_description); |
| }, |
| [](const QuickInsertLocalFileResult& data) -> ReturnType { |
| return QuickInsertLocalFileMedia(data.file_path); |
| }, |
| [](const QuickInsertDriveFileResult& data) -> ReturnType { |
| return QuickInsertLinkMedia(data.url, |
| base::UTF16ToUTF8(data.title)); |
| }, |
| [](const QuickInsertCategoryResult& data) -> ReturnType { |
| return std::monostate(); |
| }, |
| [](const QuickInsertSearchRequestResult& data) -> ReturnType { |
| return std::monostate(); |
| }, |
| [](const QuickInsertEditorResult& data) -> ReturnType { |
| return std::monostate(); |
| }, |
| [](const QuickInsertLobsterResult& data) -> ReturnType { |
| return std::monostate(); |
| }, |
| [](const QuickInsertNewWindowResult& data) -> ReturnType { |
| return std::monostate(); |
| }, |
| [](const QuickInsertCapsLockResult& data) -> ReturnType { |
| return std::monostate(); |
| }, |
| [](const QuickInsertCaseTransformResult& data) -> ReturnType { |
| return std::monostate(); |
| }, |
| }, |
| result); |
| } |
| |
| std::vector<QuickInsertSearchResultsSection> |
| CreateSingleSectionForCategoryResults( |
| QuickInsertSectionType section_type, |
| std::vector<QuickInsertSearchResult> results) { |
| if (results.empty()) { |
| return {}; |
| } |
| return {QuickInsertSearchResultsSection(section_type, std::move(results), |
| /*has_more_results=*/false)}; |
| } |
| |
| std::u16string TransformText(std::u16string_view text, |
| QuickInsertCaseTransformResult::Type type) { |
| switch (type) { |
| case QuickInsertCaseTransformResult::Type::kUpperCase: |
| return QuickInsertTransformToUpperCase(text); |
| case QuickInsertCaseTransformResult::Type::kLowerCase: |
| return QuickInsertTransformToLowerCase(text); |
| case QuickInsertCaseTransformResult::Type::kTitleCase: |
| return QuickInsertTransformToTitleCase(text); |
| } |
| NOTREACHED(); |
| } |
| |
| void OpenLink(const GURL& url) { |
| NewWindowDelegate::GetInstance()->OpenUrl( |
| url, NewWindowDelegate::OpenUrlFrom::kUserInteraction, |
| NewWindowDelegate::Disposition::kNewForegroundTab); |
| } |
| |
| void OpenFile(const base::FilePath& path) { |
| NewWindowDelegate::GetInstance()->OpenFile(path); |
| } |
| |
| GURL GetUrlForNewWindow(QuickInsertNewWindowResult::Type type) { |
| switch (type) { |
| case QuickInsertNewWindowResult::Type::kDoc: |
| return GURL("https://docs.new"); |
| case QuickInsertNewWindowResult::Type::kSheet: |
| return GURL("https://sheets.new"); |
| case QuickInsertNewWindowResult::Type::kSlide: |
| return GURL("https://slides.new"); |
| case QuickInsertNewWindowResult::Type::kChrome: |
| return GURL("chrome://newtab"); |
| } |
| } |
| |
| ui::EmojiPickerCategory EmojiResultTypeToCategory( |
| QuickInsertEmojiResult::Type type) { |
| switch (type) { |
| case QuickInsertEmojiResult::Type::kEmoji: |
| return ui::EmojiPickerCategory::kEmojis; |
| case QuickInsertEmojiResult::Type::kSymbol: |
| return ui::EmojiPickerCategory::kSymbols; |
| case QuickInsertEmojiResult::Type::kEmoticon: |
| return ui::EmojiPickerCategory::kEmoticons; |
| } |
| } |
| |
| QuickInsertSectionType GetSectionTypeForCategorySuggestion( |
| QuickInsertCategory category) { |
| switch (category) { |
| case QuickInsertCategory::kUnitsMaths: |
| case QuickInsertCategory::kDatesTimes: |
| return QuickInsertSectionType::kExamples; |
| case QuickInsertCategory::kGifs: |
| return QuickInsertSectionType::kFeaturedGifs; |
| default: |
| return QuickInsertSectionType::kNone; |
| } |
| } |
| |
| } // namespace |
| |
| QuickInsertController::QuickInsertController() |
| : caps_lock_bubble_controller_(&GetImeKeyboard()), |
| asset_fetcher_(std::make_unique<QuickInsertAssetFetcherImpl>(this)), |
| search_controller_(kBurnInPeriod) {} |
| |
| QuickInsertController::~QuickInsertController() { |
| // `widget_` depends on `this`. Destroy the widget synchronously to avoid a |
| // dangling pointer. |
| if (widget_) { |
| widget_->CloseNow(); |
| } |
| } |
| |
| void QuickInsertController::RegisterProfilePrefs(PrefRegistrySimple* registry) { |
| QuickInsertFeatureTour::RegisterProfilePrefs(registry); |
| QuickInsertSessionMetrics::RegisterProfilePrefs(registry); |
| } |
| |
| void QuickInsertController::DisableFeatureTourForTesting() { |
| CHECK_IS_TEST(); |
| g_feature_tour_enabled = false; |
| } |
| |
| void QuickInsertController::SetClient(QuickInsertClient* client) { |
| // `QuickInsertSearchController` may depend on the current client via |
| // `StartSearch`. Stop the search before changing the `client`. This may send |
| // a `StopSearch` call to the current `client_`. |
| search_controller_.StopSearch(); |
| client_ = client; |
| } |
| |
| void QuickInsertController::OnClientPrefsSet(PrefService* prefs) { |
| if (client_ == nullptr) { |
| return; |
| } |
| |
| search_controller_.LoadEmojiLanguagesFromPrefs(prefs); |
| } |
| |
| void QuickInsertController::ToggleWidget( |
| const base::TimeTicks trigger_event_timestamp) { |
| // Show the feature tour if it's the first time this feature is used. |
| if (PrefService* prefs = GetPrefs(); |
| g_feature_tour_enabled && prefs && |
| feature_tour_.MaybeShowForFirstUse( |
| prefs, |
| client_->IsEligibleForEditor() |
| ? QuickInsertFeatureTour::EditorStatus::kEligible |
| : QuickInsertFeatureTour::EditorStatus::kNotEligible, |
| base::BindRepeating(OpenLink, GURL(kSupportUrl)), |
| base::BindRepeating(&QuickInsertController::ShowWidgetPostFeatureTour, |
| weak_ptr_factory_.GetWeakPtr()))) { |
| return; |
| } |
| |
| if (widget_) { |
| CloseWidget(); |
| } else { |
| ShowWidget(trigger_event_timestamp, WidgetTriggerSource::kDefault); |
| } |
| } |
| |
| std::vector<QuickInsertCategory> |
| QuickInsertController::GetAvailableCategories() { |
| return session_ == nullptr ? std::vector<QuickInsertCategory>{} |
| : session_->model.GetAvailableCategories(); |
| } |
| |
| void QuickInsertController::GetZeroStateSuggestedResults( |
| SuggestedResultsCallback callback) { |
| CHECK(client_); |
| suggestions_controller_.GetSuggestions(*client_, session_->model, |
| std::move(callback)); |
| } |
| |
| void QuickInsertController::GetResultsForCategory( |
| QuickInsertCategory category, |
| SearchResultsCallback callback) { |
| CHECK(client_); |
| suggestions_controller_.GetSuggestionsForCategory( |
| *client_, category, |
| base::BindRepeating(CreateSingleSectionForCategoryResults, |
| GetSectionTypeForCategorySuggestion(category)) |
| .Then(std::move(callback))); |
| } |
| |
| void QuickInsertController::StartSearch( |
| std::u16string_view query, |
| std::optional<QuickInsertCategory> category, |
| SearchResultsCallback callback) { |
| CHECK(session_); |
| CHECK(client_); |
| search_controller_.StartSearch( |
| client_, query, std::move(category), GetAvailableCategories(), |
| !session_->model.is_caps_lock_enabled(), |
| session_->model.GetMode() == QuickInsertModeType::kHasSelection, |
| std::move(callback)); |
| } |
| |
| void QuickInsertController::StopSearch() { |
| search_controller_.StopSearch(); |
| } |
| |
| void QuickInsertController::StartEmojiSearch( |
| std::u16string_view query, |
| EmojiSearchResultsCallback callback) { |
| search_controller_.StartEmojiSearch(GetPrefs(), query, std::move(callback)); |
| } |
| |
| void QuickInsertController::CloseWidgetThenInsertResultOnNextFocus( |
| const QuickInsertSearchResult& result) { |
| InsertResultOnNextFocus(result); |
| |
| client_->Announce( |
| l10n_util::GetStringUTF16(IDS_PICKER_INSERTION_ANNOUNCEMENT_TEXT)); |
| |
| if (Shell::Get()->accessibility_controller()->spoken_feedback().enabled()) { |
| close_widget_delay_timer_.Start( |
| FROM_HERE, kCloseWidgetDelay, |
| base::BindOnce(&QuickInsertController::CloseWidget, |
| weak_ptr_factory_.GetWeakPtr())); |
| } else { |
| CloseWidget(); |
| } |
| } |
| |
| void QuickInsertController::OpenResult(const QuickInsertSearchResult& result) { |
| return std::visit( |
| absl::Overload{ |
| [](const QuickInsertTextResult& data) { NOTREACHED(); }, |
| [](const QuickInsertEmojiResult& data) { NOTREACHED(); }, |
| [](const QuickInsertGifResult& data) { NOTREACHED(); }, |
| [](const QuickInsertClipboardResult& data) { NOTREACHED(); }, |
| [&](const QuickInsertBrowsingHistoryResult& data) { |
| session_->session_metrics.SetOutcome( |
| QuickInsertSessionMetrics::SessionOutcome::kOpenLink); |
| OpenLink(data.url); |
| }, |
| [&](const QuickInsertLocalFileResult& data) { |
| session_->session_metrics.SetOutcome( |
| QuickInsertSessionMetrics::SessionOutcome::kOpenFile); |
| OpenFile(data.file_path); |
| }, |
| [&](const QuickInsertDriveFileResult& data) { |
| session_->session_metrics.SetOutcome( |
| QuickInsertSessionMetrics::SessionOutcome::kOpenLink); |
| OpenLink(data.url); |
| }, |
| [](const QuickInsertCategoryResult& data) { NOTREACHED(); }, |
| [](const QuickInsertSearchRequestResult& data) { NOTREACHED(); }, |
| [](const QuickInsertEditorResult& data) { NOTREACHED(); }, |
| [](const QuickInsertLobsterResult& data) { NOTREACHED(); }, |
| [&](const QuickInsertNewWindowResult& data) { |
| session_->session_metrics.SetOutcome( |
| QuickInsertSessionMetrics::SessionOutcome::kCreate); |
| OpenLink(GetUrlForNewWindow(data.type)); |
| }, |
| [&](const QuickInsertCapsLockResult& data) { |
| session_->session_metrics.SetOutcome( |
| QuickInsertSessionMetrics::SessionOutcome::kFormat); |
| caps_lock_request_ = |
| std::make_unique<QuickInsertActionOnNextFocusRequest>( |
| widget_->GetInputMethod(), kCapsLockRequestTimeout, |
| base::BindOnce( |
| [](bool enabled) { |
| GetImeKeyboard().SetCapsLockEnabled(enabled); |
| }, |
| data.enabled), |
| base::BindOnce( |
| [](bool enabled) { |
| GetImeKeyboard().SetCapsLockEnabled(enabled); |
| }, |
| data.enabled)); |
| }, |
| [&](const QuickInsertCaseTransformResult& data) { |
| if (!session_) { |
| return; |
| } |
| session_->session_metrics.SetOutcome( |
| QuickInsertSessionMetrics::SessionOutcome::kFormat); |
| std::u16string_view selected_text = session_->model.selected_text(); |
| InsertResultOnNextFocus(QuickInsertTextResult( |
| TransformText(selected_text, data.type), |
| QuickInsertTextResult::Source::kCaseTransform)); |
| }, |
| }, |
| result); |
| } |
| |
| void QuickInsertController::ShowEmojiPicker(ui::EmojiPickerCategory category, |
| std::u16string_view query) { |
| ui::ShowEmojiPanelInSpecificMode(category, |
| ui::EmojiPickerFocusBehavior::kAlwaysShow, |
| base::UTF16ToUTF8(query)); |
| } |
| |
| void QuickInsertController::ShowEditor( |
| std::optional<std::string> preset_query_id, |
| std::optional<std::string> freeform_text) { |
| if (!show_editor_callback_.is_null()) { |
| std::move(show_editor_callback_) |
| .Run(std::move(preset_query_id), std::move(freeform_text)); |
| } |
| } |
| |
| // TODO: b:370885630 - Considers making selected_text as an argument of this |
| // method. |
| void QuickInsertController::ShowLobster( |
| std::optional<std::string> freeform_text) { |
| if (!show_lobster_callback_.is_null()) { |
| std::move(show_lobster_callback_) |
| .Run(session_ != nullptr && session_->model.selected_text() != u"" |
| ? base::UTF16ToUTF8(session_->model.selected_text()) |
| : std::move(freeform_text)); |
| } |
| } |
| |
| QuickInsertAssetFetcher* QuickInsertController::GetAssetFetcher() { |
| return asset_fetcher_.get(); |
| } |
| |
| QuickInsertSessionMetrics& QuickInsertController::GetSessionMetrics() { |
| return session_->session_metrics; |
| } |
| |
| QuickInsertActionType QuickInsertController::GetActionForResult( |
| const QuickInsertSearchResult& result) { |
| CHECK(session_); |
| const QuickInsertModeType mode = session_->model.GetMode(); |
| return std::visit( |
| absl::Overload{[mode](const QuickInsertTextResult& data) { |
| CHECK(mode == QuickInsertModeType::kNoSelection || |
| mode == QuickInsertModeType::kHasSelection); |
| return QuickInsertActionType::kInsert; |
| }, |
| [mode](const QuickInsertEmojiResult& data) { |
| CHECK(mode == QuickInsertModeType::kNoSelection || |
| mode == QuickInsertModeType::kHasSelection); |
| return QuickInsertActionType::kInsert; |
| }, |
| [mode](const QuickInsertGifResult& data) { |
| CHECK(mode == QuickInsertModeType::kNoSelection || |
| mode == QuickInsertModeType::kHasSelection); |
| return QuickInsertActionType::kInsert; |
| }, |
| [mode](const QuickInsertClipboardResult& data) { |
| CHECK(mode == QuickInsertModeType::kNoSelection || |
| mode == QuickInsertModeType::kHasSelection); |
| return QuickInsertActionType::kInsert; |
| }, |
| [mode](const QuickInsertBrowsingHistoryResult& data) { |
| return mode == QuickInsertModeType::kUnfocused |
| ? QuickInsertActionType::kOpen |
| : QuickInsertActionType::kInsert; |
| }, |
| [mode](const QuickInsertLocalFileResult& data) { |
| return mode == QuickInsertModeType::kUnfocused |
| ? QuickInsertActionType::kOpen |
| : QuickInsertActionType::kInsert; |
| }, |
| [mode](const QuickInsertDriveFileResult& data) { |
| return mode == QuickInsertModeType::kUnfocused |
| ? QuickInsertActionType::kOpen |
| : QuickInsertActionType::kInsert; |
| }, |
| [](const QuickInsertCategoryResult& data) { |
| return QuickInsertActionType::kDo; |
| }, |
| [](const QuickInsertSearchRequestResult& data) { |
| return QuickInsertActionType::kDo; |
| }, |
| [](const QuickInsertEditorResult& data) { |
| return QuickInsertActionType::kCreate; |
| }, |
| [](const QuickInsertLobsterResult& data) { |
| return QuickInsertActionType::kCreate; |
| }, |
| [](const QuickInsertNewWindowResult& data) { |
| return QuickInsertActionType::kDo; |
| }, |
| [](const QuickInsertCapsLockResult& data) { |
| return QuickInsertActionType::kDo; |
| }, |
| [&](const QuickInsertCaseTransformResult& data) { |
| return QuickInsertActionType::kDo; |
| }}, |
| result); |
| } |
| |
| std::vector<QuickInsertEmojiResult> QuickInsertController::GetSuggestedEmoji() { |
| CHECK(session_); |
| return session_->emoji_suggester.GetSuggestedEmoji(); |
| } |
| |
| bool QuickInsertController::IsGifsEnabled() { |
| CHECK(session_); |
| return session_->model.IsGifsEnabled(); |
| } |
| |
| PrefService* QuickInsertController::GetPrefs() { |
| return Shell::Get()->session_controller()->GetLastActiveUserPrefService(); |
| } |
| |
| QuickInsertModeType QuickInsertController::GetMode() { |
| CHECK(session_); |
| return session_->model.GetMode(); |
| } |
| |
| void QuickInsertController::OnViewIsDeleting(views::View* view) { |
| view_observation_.Reset(); |
| |
| session_.reset(); |
| } |
| |
| scoped_refptr<network::SharedURLLoaderFactory> |
| QuickInsertController::GetSharedURLLoaderFactory() { |
| return client_->GetSharedURLLoaderFactory(); |
| } |
| |
| void QuickInsertController::FetchFileThumbnail( |
| const base::FilePath& path, |
| const gfx::Size& size, |
| FetchFileThumbnailCallback callback) { |
| client_->FetchFileThumbnail(path, size, std::move(callback)); |
| } |
| |
| QuickInsertController::Session::Session( |
| PrefService* prefs, |
| ui::TextInputClient* focused_client, |
| input_method::ImeKeyboard* ime_keyboard, |
| QuickInsertModel::EditorStatus editor_status, |
| QuickInsertModel::LobsterStatus lobster_status, |
| QuickInsertEmojiSuggester::GetNameCallback get_name) |
| : model(prefs, focused_client, ime_keyboard, editor_status, lobster_status), |
| emoji_history_model(prefs), |
| emoji_suggester(&emoji_history_model, std::move(get_name)), |
| session_metrics(prefs) { |
| session_metrics.OnStartSession(focused_client); |
| feature_usage_metrics.StartUsage(); |
| } |
| |
| QuickInsertController::Session::~Session() { |
| feature_usage_metrics.StopUsage(); |
| } |
| |
| void QuickInsertController::ShowWidget(base::TimeTicks trigger_event_timestamp, |
| WidgetTriggerSource trigger_source) { |
| ui::TextInputClient* focused_text_input_client = GetFocusedTextInputClient(); |
| show_editor_callback_ = client_->CacheEditorContext(); |
| show_lobster_callback_ = |
| client_->CacheLobsterContext(focused_text_input_client); |
| input_method::ImeKeyboard& keyboard = GetImeKeyboard(); |
| |
| if (focused_text_input_client && |
| focused_text_input_client->GetTextInputType() == |
| ui::TEXT_INPUT_TYPE_PASSWORD) { |
| bool should_enable = !keyboard.IsCapsLockEnabled(); |
| keyboard.SetCapsLockEnabled(should_enable); |
| return; |
| } |
| |
| session_ = std::make_unique<Session>( |
| GetPrefs(), focused_text_input_client, &keyboard, |
| show_editor_callback_.is_null() |
| ? QuickInsertModel::EditorStatus::kDisabled |
| : QuickInsertModel::EditorStatus::kEnabled, |
| show_lobster_callback_.is_null() |
| ? QuickInsertModel::LobsterStatus::kDisabled |
| : QuickInsertModel::LobsterStatus::kEnabled, |
| base::BindRepeating( |
| [](base::WeakPtr<QuickInsertController> weak_controller, |
| std::string_view emoji) -> std::string { |
| if (weak_controller == nullptr) { |
| return ""; |
| } |
| return weak_controller->search_controller_.GetEmojiName(emoji); |
| }, |
| weak_ptr_factory_.GetWeakPtr())); |
| |
| const gfx::Rect anchor_bounds = GetQuickInsertAnchorBounds( |
| GetCaretBounds(), GetCursorPoint(), GetFocusedWindowBounds()); |
| if (trigger_source == WidgetTriggerSource::kFeatureTour && |
| session_->model.GetMode() == QuickInsertModeType::kUnfocused) { |
| widget_ = QuickInsertWidget::CreateCentered(this, anchor_bounds, |
| trigger_event_timestamp); |
| } else { |
| widget_ = |
| QuickInsertWidget::Create(this, anchor_bounds, trigger_event_timestamp); |
| } |
| widget_->Show(); |
| |
| view_observation_.Observe(widget_->GetContentsView()); |
| } |
| |
| void QuickInsertController::CloseWidget() { |
| if (!widget_) { |
| return; |
| } |
| |
| session_->session_metrics.SetOutcome( |
| QuickInsertSessionMetrics::SessionOutcome::kAbandoned); |
| widget_->Close(); |
| } |
| |
| void QuickInsertController::ShowWidgetPostFeatureTour() { |
| ShowWidget(base::TimeTicks::Now(), WidgetTriggerSource::kFeatureTour); |
| } |
| |
| std::optional<QuickInsertWebPasteTarget> |
| QuickInsertController::GetWebPasteTarget() { |
| return client_ ? client_->GetWebPasteTarget() : std::nullopt; |
| } |
| |
| void QuickInsertController::InsertResultOnNextFocus( |
| const QuickInsertSearchResult& result) { |
| if (!widget_) { |
| return; |
| } |
| |
| // Update emoji history in prefs the result is an emoji/symbol/emoticon. |
| CHECK(session_); |
| if (auto* data = std::get_if<QuickInsertEmojiResult>(&result); |
| data != nullptr && session_->model.should_do_learning()) { |
| session_->emoji_history_model.UpdateRecentEmoji( |
| EmojiResultTypeToCategory(data->type), base::UTF16ToUTF8(data->text)); |
| } |
| |
| std::visit( |
| absl::Overload{ |
| [&](QuickInsertRichMedia media) { |
| ui::InputMethod* input_method = widget_->GetInputMethod(); |
| if (input_method == nullptr) { |
| return; |
| } |
| |
| // This cancels the previous request if there was one. |
| insert_media_request_ = std::make_unique< |
| QuickInsertInsertMediaRequest>( |
| input_method, media, kInsertMediaTimeout, |
| base::BindOnce( |
| [](base::WeakPtr<QuickInsertController> weak_controller) { |
| return weak_controller |
| ? weak_controller->GetWebPasteTarget() |
| : std::nullopt; |
| }, |
| weak_ptr_factory_.GetWeakPtr()), |
| base::BindOnce(&QuickInsertController::OnInsertCompleted, |
| weak_ptr_factory_.GetWeakPtr(), media)); |
| }, |
| [&](QuickInsertClipboardResult data) { |
| // This cancels the previous request if there was one. |
| paste_request_ = std::make_unique<QuickInsertPasteRequest>( |
| ClipboardHistoryController::Get(), |
| aura::client::GetFocusClient(widget_->GetNativeView()), |
| data.item_id); |
| }, |
| [](std::monostate) { NOTREACHED(); }, |
| }, |
| GetInsertionContentForResult(result)); |
| |
| session_->session_metrics.SetOutcome( |
| QuickInsertSessionMetrics::SessionOutcome::kInsertedOrCopied); |
| } |
| |
| void QuickInsertController::OnInsertCompleted( |
| const QuickInsertRichMedia& media, |
| QuickInsertInsertMediaRequest::Result result) { |
| // Fallback to copying to the clipboard on failure. |
| if (result != QuickInsertInsertMediaRequest::Result::kSuccess) { |
| CopyMediaToClipboard(media); |
| } |
| } |
| |
| QuickInsertCapsLockPosition QuickInsertController::GetCapsLockPosition() { |
| // Always put the caps lock entry point at the top if the user has caps lock |
| // enabled, since it is they will likely want to disable it. |
| if (GetImeKeyboard().IsCapsLockEnabled()) { |
| return QuickInsertCapsLockPosition::kTop; |
| } |
| |
| PrefService* prefs = GetPrefs(); |
| if (prefs == nullptr) { |
| return QuickInsertCapsLockPosition::kTop; |
| } |
| |
| int caps_lock_displayed_count = |
| prefs->GetInteger(prefs::kQuickInsertCapsLockDisplayedCountPrefName); |
| int caps_lock_selected_count = |
| prefs->GetInteger(prefs::kQuickInsertLockSelectedCountPrefName); |
| float caps_lock_selected_ratio = |
| static_cast<float>(caps_lock_selected_count) / caps_lock_displayed_count; |
| |
| if (caps_lock_displayed_count < kCapsLockMinimumTopDisplayCount || |
| caps_lock_selected_ratio >= kCapsLockRatioThresholdForTop) { |
| return QuickInsertCapsLockPosition::kTop; |
| } |
| if (caps_lock_selected_ratio >= kCapsLockRatioThresholdForBottom) { |
| return QuickInsertCapsLockPosition::kMiddle; |
| } |
| return QuickInsertCapsLockPosition::kBottom; |
| } |
| |
| } // namespace ash |