blob: af3a142f646ae3ba5d4aaf63b0f0dad51e0cb623 [file] [log] [blame]
// Copyright 2021 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chrome/browser/chromeos/input_method/multi_word_suggester.h"
#include <cmath>
#include "base/metrics/histogram_functions.h"
#include "base/strings/utf_string_conversions.h"
#include "chrome/browser/chromeos/input_method/ui/suggestion_details.h"
#include "chromeos/services/ime/public/cpp/suggestions.h"
#include "ui/events/keycodes/dom/dom_code.h"
namespace chromeos {
namespace {
using ::chromeos::ime::TextSuggestion;
using ::chromeos::ime::TextSuggestionMode;
using ::chromeos::ime::TextSuggestionType;
absl::optional<TextSuggestion> GetMultiWordSuggestion(
const std::vector<TextSuggestion>& suggestions) {
if (suggestions.empty())
return absl::nullopt;
if (suggestions[0].type == TextSuggestionType::kMultiWord) {
// There should only ever be one multi word suggestion given at a time.
DCHECK_EQ(suggestions.size(), 1);
return suggestions[0];
}
return absl::nullopt;
}
int CalculateNumberMatchingChars(const std::u16string& first,
const std::u16string& second) {
int matching_character_count = 0;
for (int i = 0; i < first.size() && i < second.size(); i++) {
if (first[i] != second[i])
break;
matching_character_count++;
}
return matching_character_count;
}
std::u16string ExtractFinalWord(const std::u16string& text) {
size_t last_space_index = text.rfind(u' ');
size_t offset =
last_space_index == std::u16string::npos ? 0 : last_space_index + 1;
return text.substr(offset);
}
int CalculateDismissedAccuracy(const LastKnownSuggestionState& state) {
size_t confirmed_text_end_pos = state.start_pos + state.confirmed_length;
size_t num_chars_accurately_predicted =
confirmed_text_end_pos >= state.predicted_text_start_pos
? confirmed_text_end_pos - state.predicted_text_start_pos
: 0;
double accuracy = static_cast<double>(num_chars_accurately_predicted) /
state.predicted_text_length;
return std::round(accuracy * 100);
}
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 RecordDismissedAccuracy(int percentage) {
base::UmaHistogramPercentage(
"InputMethod.Assistive.DismissedAccuracy.MultiWord", percentage);
}
} // namespace
MultiWordSuggester::MultiWordSuggester(
SuggestionHandlerInterface* suggestion_handler)
: suggestion_handler_(suggestion_handler) {}
MultiWordSuggester::~MultiWordSuggester() = default;
void MultiWordSuggester::OnFocus(int context_id) {
focused_context_id_ = context_id;
ResetSuggestionState();
ResetTextState();
}
void MultiWordSuggester::OnBlur() {
focused_context_id_ = 0;
ResetSuggestionState();
ResetTextState();
}
void MultiWordSuggester::OnSurroundingTextChanged(const std::u16string& text,
size_t cursor_pos,
size_t anchor_pos) {
bool cursor_at_end_of_text =
cursor_pos == anchor_pos && cursor_pos == text.length();
text_state_ = LastKnownTextState{
.text = text, .cursor_at_end_of_text = cursor_at_end_of_text};
}
void MultiWordSuggester::OnExternalSuggestionsUpdated(
const std::vector<TextSuggestion>& suggestions) {
if (suggestion_state_ || !text_state_.cursor_at_end_of_text)
return;
absl::optional<TextSuggestion> multi_word_suggestion =
GetMultiWordSuggestion(suggestions);
if (multi_word_suggestion) {
auto suggestion = multi_word_suggestion.value();
auto suggestion_text = base::UTF8ToUTF16(suggestion.text);
auto final_word = ExtractFinalWord(text_state_.text);
int confirmed_length =
suggestion.mode == TextSuggestionMode::kCompletion
? CalculateNumberMatchingChars(suggestion_text, final_word)
: 0;
DisplaySuggestion(suggestion_text, confirmed_length);
size_t start_pos = text_state_.text.length() >= confirmed_length
? text_state_.text.length() - confirmed_length
: 0;
size_t predicted_text_start_pos = start_pos + confirmed_length;
size_t predicted_text_length = suggestion_text.length() - confirmed_length;
suggestion_state_ = LastKnownSuggestionState{
.start_pos = start_pos,
.text = suggestion_text,
.confirmed_length = static_cast<size_t>(confirmed_length),
.predicted_text_start_pos = predicted_text_start_pos,
.predicted_text_length = predicted_text_length,
.suggestion_mode = suggestion.mode,
.time_shown_to_user = base::TimeTicks::Now()};
}
}
SuggestionStatus MultiWordSuggester::HandleKeyEvent(const ui::KeyEvent& event) {
if (!suggestion_state_)
return SuggestionStatus::kNotHandled;
switch (event.code()) {
case ui::DomCode::TAB:
AcceptSuggestion();
return SuggestionStatus::kAccept;
default:
return SuggestionStatus::kNotHandled;
}
}
bool MultiWordSuggester::Suggest(const std::u16string& text,
size_t cursor_pos,
size_t anchor_pos) {
if (!suggestion_state_ || cursor_pos != text.length() ||
suggestion_state_->start_pos > text.length())
return false;
auto last_suggestion_shown = suggestion_state_.value();
auto possibly_confirmed_text =
last_suggestion_shown.start_pos < text.length() &&
last_suggestion_shown.start_pos >= 0
? text.substr(last_suggestion_shown.start_pos)
: base::EmptyString16();
bool matches_last_suggestion =
base::StartsWith(last_suggestion_shown.text, possibly_confirmed_text,
base::CompareCase::INSENSITIVE_ASCII);
if (matches_last_suggestion) {
int confirmed_length = possibly_confirmed_text.length();
DisplaySuggestion(last_suggestion_shown.text, confirmed_length);
suggestion_state_->confirmed_length = confirmed_length;
return true;
}
return false;
}
bool MultiWordSuggester::AcceptSuggestion(size_t index) {
std::string error;
suggestion_handler_->AcceptSuggestion(focused_context_id_, &error);
if (!error.empty()) {
LOG(ERROR) << "suggest: failed to accept suggestion - " << error;
return false;
}
if (suggestion_state_) {
RecordTimeToAccept(base::TimeTicks::Now() -
suggestion_state_->time_shown_to_user);
}
ResetSuggestionState();
return true;
}
void MultiWordSuggester::DismissSuggestion() {
std::string error;
suggestion_handler_->DismissSuggestion(focused_context_id_, &error);
if (!error.empty()) {
LOG(ERROR) << "suggest: Failed to dismiss suggestion - " << error;
return;
}
if (suggestion_state_) {
RecordTimeToDismiss(base::TimeTicks::Now() -
suggestion_state_->time_shown_to_user);
RecordDismissedAccuracy(
CalculateDismissedAccuracy(suggestion_state_.value()));
}
ResetSuggestionState();
}
AssistiveType MultiWordSuggester::GetProposeActionType() {
if (!suggestion_state_)
return AssistiveType::kGenericAction;
AssistiveType multi_word_type =
suggestion_state_->suggestion_mode == TextSuggestionMode::kCompletion
? AssistiveType::kMultiWordCompletion
: AssistiveType::kMultiWordPrediction;
return multi_word_type;
}
bool MultiWordSuggester::HasSuggestions() {
return false;
}
std::vector<TextSuggestion> MultiWordSuggester::GetSuggestions() {
return {};
}
void MultiWordSuggester::DisplaySuggestion(const std::u16string& text,
int confirmed_length) {
ui::ime::SuggestionDetails details;
details.text = text;
details.show_accept_annotation = false;
details.show_quick_accept_annotation = true;
details.confirmed_length = confirmed_length;
details.show_setting_link = false;
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::ResetSuggestionState() {
suggestion_state_ = absl::nullopt;
}
void MultiWordSuggester::ResetTextState() {
text_state_ = LastKnownTextState{
.text = u"",
.cursor_at_end_of_text = true,
};
}
} // namespace chromeos