| // Copyright 2021 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "chrome/browser/ash/input_method/multi_word_suggester.h" |
| |
| #include <cmath> |
| #include <optional> |
| #include <string_view> |
| |
| #include "ash/constants/ash_pref_names.h" |
| #include "base/metrics/histogram_functions.h" |
| #include "base/strings/string_util.h" |
| #include "base/strings/stringprintf.h" |
| #include "base/strings/utf_string_conversions.h" |
| #include "base/time/time.h" |
| #include "chrome/browser/ash/input_method/suggestion_enums.h" |
| #include "chrome/browser/ui/ash/input_method/suggestion_details.h" |
| #include "chromeos/ash/services/ime/public/cpp/assistive_suggestions.h" |
| #include "components/prefs/scoped_user_pref_update.h" |
| #include "ui/events/keycodes/dom/dom_code.h" |
| |
| namespace ash { |
| namespace input_method { |
| namespace { |
| |
| using ime::AssistiveSuggestion; |
| using ime::AssistiveSuggestionMode; |
| using ime::AssistiveSuggestionType; |
| using ime::SuggestionsTextContext; |
| |
| // Used for UmaHistogramExactLinear, should remain <= 101. |
| constexpr size_t kMaxSuggestionLength = 101; |
| constexpr size_t kMinimumNumberOfCharsToProduceSuggestion = 3; |
| constexpr char kMultiWordFirstAcceptTimeDays[] = "multi_word_first_accept"; |
| constexpr char16_t kSuggestionShownMessage[] = |
| u"predictive writing candidate shown, press down to select or " |
| u"press tab to accept"; |
| constexpr char kSuggestionSelectedMessage[] = |
| "predictive writing candidate selected, candidate text is %s, " |
| "press tab to accept or press up to deselect"; |
| constexpr char16_t kSuggestionAcceptedMessage[] = |
| u"predictive writing candidate inserted"; |
| constexpr char16_t kSuggestionDismissedMessage[] = |
| u"predictive writing candidate dismissed"; |
| |
| std::optional<AssistiveSuggestion> GetMultiWordSuggestion( |
| const std::vector<AssistiveSuggestion>& suggestions) { |
| if (suggestions.empty()) { |
| return std::nullopt; |
| } |
| if (suggestions[0].type == AssistiveSuggestionType::kMultiWord) { |
| // There should only ever be one multi word suggestion given at a time. |
| DCHECK_EQ(suggestions.size(), 1u); |
| return suggestions[0]; |
| } |
| return std::nullopt; |
| } |
| |
| size_t CalculateConfirmedLength(const std::u16string& surrounding_text, |
| const std::u16string& suggestion_text) { |
| if (surrounding_text.empty() || suggestion_text.empty()) { |
| return 0; |
| } |
| |
| for (size_t i = suggestion_text.length(); i >= 1; i--) { |
| if (base::EndsWith(surrounding_text, suggestion_text.substr(0, i))) { |
| return i; |
| } |
| } |
| |
| return 0; |
| } |
| |
| MultiWordSuggestionType ToSuggestionType( |
| const ime::AssistiveSuggestionMode& suggestion_mode) { |
| switch (suggestion_mode) { |
| case ime::AssistiveSuggestionMode::kCompletion: |
| return MultiWordSuggestionType::kCompletion; |
| case ime::AssistiveSuggestionMode::kPrediction: |
| return MultiWordSuggestionType::kPrediction; |
| default: |
| return MultiWordSuggestionType::kUnknown; |
| } |
| } |
| |
| void RecordTimeToAccept(base::TimeDelta delta) { |
| base::UmaHistogramTimes("InputMethod.Assistive.TimeToAccept.MultiWord", |
| delta); |
| } |
| |
| void RecordTimeToDismiss(base::TimeDelta delta) { |
| base::UmaHistogramTimes("InputMethod.Assistive.TimeToDismiss.MultiWord", |
| delta); |
| } |
| |
| void RecordSuggestionLength(size_t suggestion_length) { |
| base::UmaHistogramExactLinear( |
| "InputMethod.Assistive.MultiWord.SuggestionLength", suggestion_length, |
| kMaxSuggestionLength); |
| } |
| |
| void RecordCouldPossiblyShowSuggestion( |
| const ime::AssistiveSuggestionMode& suggestion_mode) { |
| base::UmaHistogramEnumeration( |
| "InputMethod.Assistive.MultiWord.CouldPossiblyShowSuggestion", |
| ToSuggestionType(suggestion_mode)); |
| } |
| |
| void RecordImplicitAcceptance( |
| const ime::AssistiveSuggestionMode& suggestion_mode) { |
| base::UmaHistogramEnumeration( |
| "InputMethod.Assistive.MultiWord.ImplicitAcceptance", |
| ToSuggestionType(suggestion_mode)); |
| } |
| |
| void RecordImplicitRejection( |
| const ime::AssistiveSuggestionMode& suggestion_mode) { |
| base::UmaHistogramEnumeration( |
| "InputMethod.Assistive.MultiWord.ImplicitRejection", |
| ToSuggestionType(suggestion_mode)); |
| } |
| |
| void RecordMultiWordSuggestionState(const MultiWordSuggestionState& state, |
| const ime::AssistiveSuggestionMode& mode) { |
| const std::string histogram = |
| mode == ime::AssistiveSuggestionMode::kCompletion |
| ? "InputMethod.Assistive.MultiWord.SuggestionState.Completion" |
| : "InputMethod.Assistive.MultiWord.SuggestionState.Prediction"; |
| base::UmaHistogramEnumeration(histogram, state); |
| } |
| |
| std::optional<int> GetTimeFirstAcceptedSuggestion(Profile* profile) { |
| ScopedDictPrefUpdate update(profile->GetPrefs(), |
| prefs::kAssistiveInputFeatureSettings); |
| auto value = update->FindInt(kMultiWordFirstAcceptTimeDays); |
| if (value.has_value()) { |
| return value.value(); |
| } |
| return std::nullopt; |
| } |
| |
| void SetTimeFirstAcceptedSuggestion(Profile* profile) { |
| ScopedDictPrefUpdate update(profile->GetPrefs(), |
| prefs::kAssistiveInputFeatureSettings); |
| auto time_since_epoch = base::Time::Now() - base::Time::UnixEpoch(); |
| update->Set(kMultiWordFirstAcceptTimeDays, time_since_epoch.InDaysFloored()); |
| } |
| |
| bool ShouldShowTabGuide(Profile* profile) { |
| auto time_first_accepted = GetTimeFirstAcceptedSuggestion(profile); |
| if (!time_first_accepted) { |
| return true; |
| } |
| |
| base::TimeDelta first_accepted = base::Days(*time_first_accepted); |
| base::TimeDelta time_since_epoch = |
| base::Time::Now() - base::Time::UnixEpoch(); |
| return (time_since_epoch - first_accepted) <= base::Days(7); |
| } |
| |
| bool CouldSuggestWithSurroundingText(std::u16string_view text, |
| const gfx::Range selection_range) { |
| return selection_range.is_empty() && selection_range.end() == text.size() && |
| text.size() >= kMinimumNumberOfCharsToProduceSuggestion; |
| } |
| |
| bool u16_isalpha(char16_t ch) { |
| return (ch >= u'A' && ch <= u'Z') || (ch >= u'a' && ch <= u'z'); |
| } |
| |
| bool WouldBeInCompletionMode(std::u16string_view text) { |
| return !text.empty() && u16_isalpha(text.back()); |
| } |
| |
| // TODO(crbug/1146266): Add DismissedAccuracy metric back in. |
| |
| } // namespace |
| |
| MultiWordSuggester::MultiWordSuggester( |
| SuggestionHandlerInterface* suggestion_handler, |
| Profile* profile) |
| : suggestion_handler_(suggestion_handler), state_(this), profile_(profile) { |
| suggestion_button_.id = ui::ime::ButtonId::kSuggestion; |
| suggestion_button_.window_type = |
| ash::ime::AssistiveWindowType::kMultiWordSuggestion; |
| suggestion_button_.suggestion_index = 0; |
| } |
| |
| MultiWordSuggester::~MultiWordSuggester() = default; |
| |
| void MultiWordSuggester::OnFocus(int context_id) { |
| // Some parts of the code reserve negative/zero context_id for unfocused |
| // context. As a result we should make sure it is not being erroneously set to |
| // a negative number, and cause unexpected behaviour. |
| DCHECK(context_id > 0); |
| focused_context_id_ = context_id; |
| state_.ResetSuggestion(); |
| } |
| |
| void MultiWordSuggester::OnBlur() { |
| focused_context_id_ = std::nullopt; |
| state_.ResetSuggestion(); |
| } |
| |
| void MultiWordSuggester::OnSurroundingTextChanged( |
| const std::u16string& text, |
| const gfx::Range selection_range) { |
| if (CouldSuggestWithSurroundingText(text, selection_range) && |
| !state_.IsSuggestionShowing()) { |
| RecordCouldPossiblyShowSuggestion( |
| WouldBeInCompletionMode(text) |
| ? ime::AssistiveSuggestionMode::kCompletion |
| : ime::AssistiveSuggestionMode::kPrediction); |
| } |
| |
| const uint32_t cursor_pos = selection_range.start(); |
| auto surrounding_text = SuggestionState::SurroundingText{ |
| .text = text, |
| .cursor_pos = cursor_pos, |
| .cursor_at_end_of_text = |
| (selection_range.is_empty() && cursor_pos == text.length())}; |
| state_.UpdateSurroundingText(surrounding_text); |
| DisplaySuggestionIfAvailable(); |
| } |
| |
| void MultiWordSuggester::OnExternalSuggestionsUpdated( |
| const std::vector<AssistiveSuggestion>& suggestions, |
| const std::optional<SuggestionsTextContext>& context) { |
| if (state_.IsSuggestionShowing() || !state_.IsCursorAtEndOfText()) { |
| return; |
| } |
| |
| std::optional<AssistiveSuggestion> multi_word_suggestion = |
| GetMultiWordSuggestion(suggestions); |
| |
| if (!multi_word_suggestion) { |
| state_.UpdateState(SuggestionState::State::kNoSuggestionShown); |
| return; |
| } |
| |
| if (auto suggestion_length = multi_word_suggestion->text.size(); |
| suggestion_length < kMaxSuggestionLength) { |
| RecordSuggestionLength(suggestion_length); |
| } |
| |
| auto suggestion = SuggestionState::Suggestion{ |
| .mode = multi_word_suggestion->mode, |
| .text = base::UTF8ToUTF16(multi_word_suggestion->text), |
| .time_first_shown = base::TimeTicks::Now()}; |
| |
| if (context) { |
| auto suggestion_state = state_.ValidateSuggestion(suggestion, *context); |
| RecordMultiWordSuggestionState(suggestion_state, suggestion.mode); |
| if (suggestion_state != MultiWordSuggestionState::kValid) { |
| return; |
| } |
| } |
| |
| state_.UpdateSuggestion(/*suggestion=*/suggestion, |
| /*new_tracking_behavior=*/context.has_value()); |
| DisplaySuggestionIfAvailable(); |
| } |
| |
| SuggestionStatus MultiWordSuggester::HandleKeyEvent(const ui::KeyEvent& event) { |
| if (!state_.IsSuggestionShowing()) { |
| return SuggestionStatus::kNotHandled; |
| } |
| |
| switch (event.code()) { |
| case ui::DomCode::TAB: |
| AcceptSuggestion(); |
| return SuggestionStatus::kAccept; |
| case ui::DomCode::ARROW_DOWN: |
| if (state_.IsSuggestionHighlighted()) { |
| return SuggestionStatus::kNotHandled; |
| } |
| state_.ToggleSuggestionHighlight(); |
| SetSuggestionHighlight(true); |
| return SuggestionStatus::kBrowsing; |
| case ui::DomCode::ARROW_UP: |
| if (!state_.IsSuggestionHighlighted()) { |
| return SuggestionStatus::kNotHandled; |
| } |
| state_.ToggleSuggestionHighlight(); |
| SetSuggestionHighlight(false); |
| return SuggestionStatus::kBrowsing; |
| case ui::DomCode::ENTER: |
| if (!state_.IsSuggestionHighlighted()) { |
| return SuggestionStatus::kNotHandled; |
| } |
| AcceptSuggestion(); |
| return SuggestionStatus::kAccept; |
| default: |
| return SuggestionStatus::kNotHandled; |
| } |
| } |
| |
| bool MultiWordSuggester::TrySuggestWithSurroundingText( |
| const std::u16string& text, |
| const gfx::Range selection_range) { |
| // MultiWordSuggester does not trigger a suggest based on surrounding text |
| // events. It only triggers suggestions OnExternalSuggestionsUpdated. |
| // |
| // Hence we should return whether the current suggestion is showing from |
| // internal state. |
| return state_.IsSuggestionShowing(); |
| } |
| |
| bool MultiWordSuggester::AcceptSuggestion(size_t index) { |
| if (!focused_context_id_.has_value()) { |
| LOG(ERROR) << "suggest: Failed to accept suggestion. No context id."; |
| return false; |
| } |
| |
| std::string error; |
| suggestion_handler_->AcceptSuggestion(*focused_context_id_, &error); |
| if (!error.empty()) { |
| LOG(ERROR) << "suggest: Failed to accept suggestion - " << error; |
| return false; |
| } |
| |
| auto suggestion = state_.GetSuggestion(); |
| if (suggestion) { |
| RecordTimeToAccept(base::TimeTicks::Now() - suggestion->time_first_shown); |
| } |
| |
| if (!GetTimeFirstAcceptedSuggestion(profile_)) { |
| SetTimeFirstAcceptedSuggestion(profile_); |
| } |
| |
| state_.UpdateState(SuggestionState::State::kSuggestionAccepted); |
| state_.ResetSuggestion(); |
| return true; |
| } |
| |
| void MultiWordSuggester::DismissSuggestion() { |
| if (!focused_context_id_.has_value()) { |
| LOG(ERROR) << "suggest: Failed to dismiss suggestion. No context id."; |
| return; |
| } |
| std::string error; |
| suggestion_handler_->DismissSuggestion(*focused_context_id_, &error); |
| if (!error.empty()) { |
| LOG(ERROR) << "suggest: Failed to dismiss suggestion - " << error; |
| return; |
| } |
| |
| auto suggestion = state_.GetSuggestion(); |
| if (suggestion) { |
| RecordTimeToDismiss(base::TimeTicks::Now() - suggestion->time_first_shown); |
| } |
| |
| state_.UpdateState(SuggestionState::State::kSuggestionDismissed); |
| state_.ResetSuggestion(); |
| } |
| |
| AssistiveType MultiWordSuggester::GetProposeActionType() { |
| return state_.GetLastSuggestionType(); |
| } |
| |
| bool MultiWordSuggester::HasSuggestions() { |
| return state_.GetSuggestion().has_value(); |
| } |
| |
| std::vector<AssistiveSuggestion> MultiWordSuggester::GetSuggestions() { |
| auto suggestion = state_.GetSuggestion(); |
| if (!suggestion) { |
| return {}; |
| } |
| return { |
| AssistiveSuggestion{.mode = suggestion->mode, |
| .type = AssistiveSuggestionType::kMultiWord, |
| .text = base::UTF16ToUTF8(suggestion->text), |
| .confirmed_length = suggestion->confirmed_length}}; |
| } |
| |
| void MultiWordSuggester::DisplaySuggestionIfAvailable() { |
| auto suggestion_to_display = state_.GetSuggestion(); |
| if (suggestion_to_display.has_value()) { |
| DisplaySuggestion(*suggestion_to_display); |
| } |
| } |
| |
| void MultiWordSuggester::DisplaySuggestion( |
| const SuggestionState::Suggestion& suggestion) { |
| if (!focused_context_id_.has_value()) { |
| LOG(ERROR) << "suggest: Failed to show suggestion. No context id."; |
| return; |
| } |
| ui::ime::SuggestionDetails details; |
| details.text = suggestion.text; |
| details.show_accept_annotation = false; |
| details.show_quick_accept_annotation = ShouldShowTabGuide(profile_); |
| details.confirmed_length = suggestion.confirmed_length; |
| details.show_setting_link = false; |
| |
| suggestion_button_.announce_string = base::UTF8ToUTF16(base::StringPrintf( |
| kSuggestionSelectedMessage, base::UTF16ToUTF8(details.text).c_str())); |
| |
| std::string error; |
| suggestion_handler_->SetSuggestion(*focused_context_id_, details, &error); |
| if (!error.empty()) { |
| LOG(ERROR) << "suggest: Failed to show suggestion in assistive framework" |
| << " - " << error; |
| } |
| } |
| |
| void MultiWordSuggester::SetSuggestionHighlight(bool highlighted) { |
| if (!focused_context_id_.has_value()) { |
| LOG(ERROR) << "suggest: Failed to set button highlighted. No context id."; |
| return; |
| } |
| std::string error; |
| suggestion_handler_->SetButtonHighlighted( |
| *focused_context_id_, suggestion_button_, highlighted, &error); |
| if (!error.empty()) { |
| LOG(ERROR) << "Failed to set button highlighted. " << error; |
| } |
| } |
| |
| void MultiWordSuggester::Announce(const std::u16string& message) { |
| if (suggestion_handler_) { |
| suggestion_handler_->Announce(message); |
| } |
| } |
| |
| MultiWordSuggester::SuggestionState::SuggestionState( |
| MultiWordSuggester* suggester) |
| : suggester_(suggester) {} |
| |
| MultiWordSuggester::SuggestionState::~SuggestionState() = default; |
| |
| void MultiWordSuggester::SuggestionState::UpdateState(const State& state) { |
| if (state == State::kPredictionSuggestionShown) { |
| last_suggestion_type_ = AssistiveType::kMultiWordPrediction; |
| } |
| |
| if (state == State::kCompletionSuggestionShown) { |
| last_suggestion_type_ = AssistiveType::kMultiWordCompletion; |
| } |
| |
| if (state_ == State::kNoSuggestionShown && |
| (state == State::kPredictionSuggestionShown || |
| state == State::kCompletionSuggestionShown)) { |
| suggester_->Announce(kSuggestionShownMessage); |
| } |
| |
| if ((state_ == State::kPredictionSuggestionShown || |
| state_ == State::kCompletionSuggestionShown || |
| state_ == State::kTrackingLastSuggestionShown) && |
| state == State::kSuggestionAccepted) { |
| suggester_->Announce(kSuggestionAcceptedMessage); |
| } |
| |
| if ((state_ == State::kPredictionSuggestionShown || |
| state_ == State::kCompletionSuggestionShown || |
| state_ == State::kTrackingLastSuggestionShown) && |
| state == State::kSuggestionDismissed) { |
| suggester_->Announce(kSuggestionDismissedMessage); |
| } |
| |
| state_ = state; |
| } |
| |
| void MultiWordSuggester::SuggestionState::UpdateSurroundingText( |
| const MultiWordSuggester::SuggestionState::SurroundingText& |
| surrounding_text) { |
| size_t prev_cursor_pos = |
| surrounding_text_.has_value() ? surrounding_text_->cursor_pos : 0; |
| |
| surrounding_text_ = SurroundingText{ |
| .text = surrounding_text.text, |
| .cursor_pos = surrounding_text.cursor_pos, |
| .prev_cursor_pos = prev_cursor_pos, |
| .cursor_at_end_of_text = surrounding_text.cursor_at_end_of_text}; |
| |
| ReconcileSuggestionWithText(); |
| } |
| |
| void MultiWordSuggester::SuggestionState::UpdateSuggestion( |
| const MultiWordSuggester::SuggestionState::Suggestion& suggestion, |
| bool new_tracking_behavior) { |
| suggestion_ = suggestion; |
| suggestion_->original_surrounding_text_length = |
| surrounding_text_ ? surrounding_text_->text.length() : 0; |
| UpdateState(suggestion.mode == AssistiveSuggestionMode::kCompletion |
| ? State::kCompletionSuggestionShown |
| : State::kPredictionSuggestionShown); |
| if (suggestion.mode == AssistiveSuggestionMode::kCompletion) { |
| ReconcileSuggestionWithText(); |
| } |
| if (new_tracking_behavior && |
| suggestion.mode == AssistiveSuggestionMode::kPrediction) { |
| // With the new tracking behavior we are guaranteed that any new suggestion |
| // is not stale, and thus can be simply appended to the current surrrounding |
| // text. Therefore there is no need to reconcile with the current text and |
| // we can transition straight to tracking mode. |
| UpdateState(State::kTrackingLastSuggestionShown); |
| } |
| } |
| |
| MultiWordSuggestionState |
| MultiWordSuggester::SuggestionState::ValidateSuggestion( |
| const MultiWordSuggester::SuggestionState::Suggestion& suggestion, |
| const ime::SuggestionsTextContext& context) { |
| if (!surrounding_text_) { |
| return MultiWordSuggestionState::kOther; |
| } |
| |
| // IME service works with UTF8 whereas here in Chromium surrounding text is |
| // UTF16. The length of the surrounding text from the IME service was |
| // calculated on a UTF8 string, so transforming context.last_n_chars to |
| // UTF16 would invalidate the length sent from IME service. |
| const std::string current_text = base::UTF16ToUTF8(surrounding_text_->text); |
| size_t current_text_length = current_text.length(); |
| size_t text_length_when_suggested = context.surrounding_text_length; |
| bool text_matches = base::EndsWith(current_text, context.last_n_chars); |
| |
| if (current_text_length == text_length_when_suggested && text_matches) { |
| return MultiWordSuggestionState::kValid; |
| } |
| |
| if (current_text_length == text_length_when_suggested && !text_matches) { |
| return MultiWordSuggestionState::kStaleAndUserEditedText; |
| } |
| |
| if (current_text_length < text_length_when_suggested) { |
| return MultiWordSuggestionState::kStaleAndUserDeletedText; |
| } |
| |
| if (current_text_length > text_length_when_suggested) { |
| return CalculateConfirmedLength(surrounding_text_->text, suggestion.text) > |
| 0 |
| ? MultiWordSuggestionState::kStaleAndUserAddedMatchingText |
| : MultiWordSuggestionState::kStaleAndUserAddedDifferentText; |
| } |
| |
| return MultiWordSuggestionState::kOther; |
| } |
| |
| void MultiWordSuggester::SuggestionState::ReconcileSuggestionWithText() { |
| if (!(suggestion_ && surrounding_text_)) { |
| return; |
| } |
| |
| size_t new_confirmed_length = |
| CalculateConfirmedLength(surrounding_text_->text, suggestion_->text); |
| |
| // Save the calculated confirmed length on first showing of a completion |
| // suggestion. This will be used later when determining if a suggestion |
| // should be dismissed or not. |
| auto initial_confirmed_length = state_ == State::kCompletionSuggestionShown |
| ? new_confirmed_length |
| : suggestion_->initial_confirmed_length; |
| |
| bool user_typed_suggestion = |
| new_confirmed_length == suggestion_->text.length(); |
| |
| // Are we still tracking the last suggestion shown to the user? |
| // |
| // TODO(b/279114189): Prediction suggestions are not dismissed correctly on |
| // first mismatched character typed, need to investigate. |
| bool no_longer_tracking = |
| state_ == State::kTrackingLastSuggestionShown && |
| ((new_confirmed_length == 0 || |
| new_confirmed_length < suggestion_->initial_confirmed_length) || |
| (new_confirmed_length == suggestion_->confirmed_length && |
| surrounding_text_->cursor_pos != surrounding_text_->prev_cursor_pos) || |
| user_typed_suggestion); |
| |
| bool user_has_typed_more = surrounding_text_->text.length() > |
| suggestion_->original_surrounding_text_length; |
| if ((state_ == State::kPredictionSuggestionShown || |
| state_ == State::kTrackingLastSuggestionShown) && |
| new_confirmed_length == 0 && user_has_typed_more) { |
| RecordImplicitRejection(suggestion_->mode); |
| } |
| |
| if (no_longer_tracking && user_typed_suggestion) { |
| RecordImplicitAcceptance(suggestion_->mode); |
| } |
| |
| if (no_longer_tracking || !surrounding_text_->cursor_at_end_of_text) { |
| UpdateState(State::kSuggestionDismissed); |
| ResetSuggestion(); |
| return; |
| } |
| |
| if (state_ == State::kPredictionSuggestionShown || |
| state_ == State::kCompletionSuggestionShown) { |
| UpdateState(State::kTrackingLastSuggestionShown); |
| } |
| |
| suggestion_ = Suggestion{.mode = suggestion_->mode, |
| .text = suggestion_->text, |
| .confirmed_length = new_confirmed_length, |
| .initial_confirmed_length = initial_confirmed_length, |
| .time_first_shown = suggestion_->time_first_shown, |
| .highlighted = suggestion_->highlighted, |
| .original_surrounding_text_length = |
| suggestion_->original_surrounding_text_length}; |
| } |
| |
| void MultiWordSuggester::SuggestionState::ToggleSuggestionHighlight() { |
| if (!suggestion_) { |
| return; |
| } |
| suggestion_->highlighted = !suggestion_->highlighted; |
| } |
| |
| bool MultiWordSuggester::SuggestionState::IsSuggestionHighlighted() { |
| if (!suggestion_) { |
| return false; |
| } |
| return suggestion_->highlighted; |
| } |
| |
| bool MultiWordSuggester::SuggestionState::IsSuggestionShowing() { |
| return (state_ == State::kPredictionSuggestionShown || |
| state_ == State::kCompletionSuggestionShown || |
| state_ == State::kTrackingLastSuggestionShown); |
| } |
| |
| bool MultiWordSuggester::SuggestionState::IsCursorAtEndOfText() { |
| if (!surrounding_text_) { |
| return false; |
| } |
| return surrounding_text_->cursor_at_end_of_text; |
| } |
| |
| std::optional<MultiWordSuggester::SuggestionState::Suggestion> |
| MultiWordSuggester::SuggestionState::GetSuggestion() { |
| return suggestion_; |
| } |
| |
| void MultiWordSuggester::SuggestionState::ResetSuggestion() { |
| suggestion_ = std::nullopt; |
| UpdateState(State::kNoSuggestionShown); |
| } |
| |
| AssistiveType MultiWordSuggester::SuggestionState::GetLastSuggestionType() { |
| return last_suggestion_type_; |
| } |
| |
| } // namespace input_method |
| } // namespace ash |