blob: ed3946f5b68ff1adb12f1f73ebe5506d8aff00a0 [file] [log] [blame]
// Copyright (c) 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/ash/input_method/grammar_manager.h"
#include "ash/constants/ash_features.h"
#include "base/feature_list.h"
#include "base/metrics/histogram_functions.h"
#include "base/strings/utf_string_conversions.h"
#include "base/timer/timer.h"
#include "chrome/browser/ash/input_method/assistive_window_properties.h"
#include "chrome/browser/ash/input_method/ui/suggestion_details.h"
#include "ui/base/ime/chromeos/ime_bridge.h"
#include "ui/base/ime/chromeos/ime_input_context_handler_interface.h"
#include "ui/base/ime/text_input_flags.h"
#include "ui/events/keycodes/dom/dom_code.h"
namespace chromeos {
namespace {
using text_utils::FindCurrentSentence;
using text_utils::FindLastSentence;
using text_utils::Sentence;
constexpr base::TimeDelta kCheckDelay = base::TimeDelta::FromSeconds(2);
const int HashMultiplier = 1024;
void RecordGrammarAction(GrammarActions action) {
base::UmaHistogramEnumeration("InputMethod.Assistive.Grammar.Actions",
action);
}
bool IsValidSentence(const std::u16string& text, const Sentence& sentence) {
uint32_t start = sentence.original_range.start();
uint32_t end = sentence.original_range.end();
if (start >= end || start >= text.size() || end > text.size())
return false;
return FindCurrentSentence(text, start) == sentence;
}
int RangeHash(const gfx::Range& range) {
return range.start() * HashMultiplier + range.end();
}
} // namespace
GrammarManager::GrammarManager(
Profile* profile,
std::unique_ptr<GrammarServiceClient> grammar_client,
SuggestionHandlerInterface* suggestion_handler)
: profile_(profile),
grammar_client_(std::move(grammar_client)),
suggestion_handler_(suggestion_handler),
current_fragment_(gfx::Range(), std::string()),
suggestion_button_(ui::ime::AssistiveWindowButton{
.id = ui::ime::ButtonId::kSuggestion,
.window_type = ui::ime::AssistiveWindowType::kGrammarSuggestion,
}),
ignore_button_(ui::ime::AssistiveWindowButton{
.id = ui::ime::ButtonId::kIgnoreSuggestion,
.window_type = ui::ime::AssistiveWindowType::kGrammarSuggestion,
}) {}
GrammarManager::~GrammarManager() = default;
bool GrammarManager::IsOnDeviceGrammarEnabled() {
return base::FeatureList::IsEnabled(
chromeos::features::kOnDeviceGrammarCheck);
}
void GrammarManager::OnFocus(int context_id, int text_input_flags) {
if (context_id != context_id_) {
current_text_ = u"";
last_sentence_ = Sentence();
new_to_context_ = true;
delay_timer_.Stop();
}
context_id_ = context_id;
text_input_flags_ = text_input_flags;
}
bool GrammarManager::OnKeyEvent(const ui::KeyEvent& event) {
if (!suggestion_shown_ || event.type() != ui::ET_KEY_PRESSED)
return false;
if (event.code() == ui::DomCode::ESCAPE) {
DismissSuggestion();
return true;
}
switch (highlighted_button_) {
case ui::ime::ButtonId::kNone:
if (event.code() == ui::DomCode::TAB ||
event.code() == ui::DomCode::ARROW_UP) {
highlighted_button_ = ui::ime::ButtonId::kSuggestion;
SetButtonHighlighted(suggestion_button_, true);
return true;
}
break;
case ui::ime::ButtonId::kSuggestion:
switch (event.code()) {
case ui::DomCode::TAB:
highlighted_button_ = ui::ime::ButtonId::kIgnoreSuggestion;
SetButtonHighlighted(ignore_button_, true);
return true;
case ui::DomCode::ARROW_DOWN:
highlighted_button_ = ui::ime::ButtonId::kNone;
SetButtonHighlighted(suggestion_button_, false);
return true;
case ui::DomCode::ENTER:
// SetComposingRange and CommitText in AcceptSuggestion will not be
// executed immediately if we are in middle of handling a key event,
// instead they will be delayed and CommitText will always be executed
// first. So we need to call AcceptSuggestion in a post task.
// TODO(crbug.com/1230961): remove PostTask after we remove the delay
// logics.
base::SequencedTaskRunnerHandle::Get()->PostTask(
FROM_HERE, base::BindOnce(&GrammarManager::AcceptSuggestion,
base::Unretained(this)));
return true;
default:
break;
}
break;
case ui::ime::ButtonId::kIgnoreSuggestion:
if (event.code() == ui::DomCode::ENTER) {
IgnoreSuggestion();
return true;
}
break;
default:
break;
}
return false;
}
void GrammarManager::OnSurroundingTextChanged(const std::u16string& text,
int cursor_pos,
int anchor_pos) {
if (text_input_flags_ & ui::TEXT_INPUT_FLAG_SPELLCHECK_OFF)
return;
if (suggestion_shown_)
DismissSuggestion();
bool text_updated = text != current_text_;
current_text_ = text;
current_sentence_ = FindCurrentSentence(text, cursor_pos);
if (new_to_context_) {
new_to_context_ = false;
} else if (text_updated) {
ui::IMEInputContextHandlerInterface* input_context =
ui::IMEBridge::Get()->GetInputContextHandler();
if (!input_context)
return;
// Grammar check is cpu consuming, so we only send request to ml service
// when the user has finished a sentence or stopped typing for some time.
Sentence last_sentence = FindLastSentence(text, cursor_pos);
if (last_sentence_ != last_sentence) {
last_sentence_ = last_sentence;
input_context->ClearGrammarFragments(last_sentence.original_range);
Check(last_sentence);
}
input_context->ClearGrammarFragments(current_sentence_.original_range);
delay_timer_.Start(
FROM_HERE, kCheckDelay,
base::BindOnce(&GrammarManager::Check, base::Unretained(this),
current_sentence_));
return;
}
// Do not show the suggestion when the user is selecting a range of text, so
// that we will not show conflict with the system copy/paste popup.
if (cursor_pos != anchor_pos)
return;
ui::IMEInputContextHandlerInterface* input_context =
ui::IMEBridge::Get()->GetInputContextHandler();
if (!input_context)
return;
absl::optional<ui::GrammarFragment> grammar_fragment_opt =
input_context->GetGrammarFragment(gfx::Range(cursor_pos));
if (grammar_fragment_opt) {
if (current_fragment_ != grammar_fragment_opt.value()) {
current_fragment_ = grammar_fragment_opt.value();
RecordGrammarAction(GrammarActions::kWindowShown);
}
std::string error;
AssistiveWindowProperties properties;
properties.type = ui::ime::AssistiveWindowType::kGrammarSuggestion;
properties.candidates = {base::UTF8ToUTF16(current_fragment_.suggestion)};
properties.visible = true;
suggestion_handler_->SetAssistiveWindowProperties(context_id_, properties,
&error);
if (!error.empty()) {
LOG(ERROR) << "Fail to show suggestion. " << error;
}
highlighted_button_ = ui::ime::ButtonId::kNone;
suggestion_shown_ = true;
}
}
void GrammarManager::Check(const Sentence& sentence) {
if (!IsValidSentence(current_text_, sentence))
return;
grammar_client_->RequestTextCheck(
profile_, sentence.text,
base::BindOnce(&GrammarManager::OnGrammarCheckDone,
base::Unretained(this), sentence));
}
void GrammarManager::OnGrammarCheckDone(
const Sentence& sentence,
bool success,
const std::vector<ui::GrammarFragment>& results) const {
if (!success || !IsValidSentence(current_text_, sentence) || results.empty())
return;
std::vector<ui::GrammarFragment> corrected_results;
auto it = ignored_markers_.find(sentence.text);
for (const ui::GrammarFragment& fragment : results) {
if (it == ignored_markers_.end() ||
it->second.find(RangeHash(fragment.range)) == it->second.end()) {
corrected_results.emplace_back(
gfx::Range(fragment.range.start() + sentence.original_range.start(),
fragment.range.end() + sentence.original_range.start()),
fragment.suggestion);
}
}
ui::IMEInputContextHandlerInterface* input_context =
ui::IMEBridge::Get()->GetInputContextHandler();
if (!input_context)
return;
input_context->AddGrammarFragments(corrected_results);
RecordGrammarAction(GrammarActions::kUnderlined);
}
void GrammarManager::DismissSuggestion() {
std::string error;
suggestion_handler_->DismissSuggestion(context_id_, &error);
if (!error.empty()) {
LOG(ERROR) << "Failed to dismiss suggestion. " << error;
return;
}
suggestion_shown_ = false;
}
void GrammarManager::AcceptSuggestion() {
if (!suggestion_shown_)
return;
DismissSuggestion();
ui::IMEInputContextHandlerInterface* input_context =
ui::IMEBridge::Get()->GetInputContextHandler();
if (!input_context) {
LOG(ERROR) << "Failed to commit grammar suggestion.";
}
if (input_context->HasCompositionText()) {
input_context->SetComposingRange(current_fragment_.range.start(),
current_fragment_.range.end(), {});
input_context->CommitText(
base::UTF8ToUTF16(current_fragment_.suggestion),
ui::TextInputClient::InsertTextCursorBehavior::kMoveCursorAfterText);
} else {
// NOTE: GetSurroundingTextInfo() could return a stale cache that no
// longer reflects reality, due to async-ness between IMF and
// TextInputClient.
// TODO(crbug/1194424): Work around the issue or fix
// GetSurroundingTextInfo().
const ui::SurroundingTextInfo surrounding_text =
input_context->GetSurroundingTextInfo();
// Delete the incorrect grammar fragment.
input_context->DeleteSurroundingText(
-static_cast<int>(surrounding_text.selection_range.start() -
current_fragment_.range.start()),
current_fragment_.range.length() -
surrounding_text.selection_range.length());
// Insert the suggestion and put cursor after it.
input_context->CommitText(
base::UTF8ToUTF16(current_fragment_.suggestion),
ui::TextInputClient::InsertTextCursorBehavior::kMoveCursorAfterText);
}
RecordGrammarAction(GrammarActions::kAccepted);
}
void GrammarManager::IgnoreSuggestion() {
if (!suggestion_shown_)
return;
DismissSuggestion();
ui::IMEInputContextHandlerInterface* input_context =
ui::IMEBridge::Get()->GetInputContextHandler();
if (!input_context)
return;
input_context->ClearGrammarFragments(current_fragment_.range);
if (ignored_markers_.find(current_sentence_.text) == ignored_markers_.end()) {
ignored_markers_[current_sentence_.text] = std::unordered_set<int>();
}
ignored_markers_[current_sentence_.text].insert(
RangeHash(gfx::Range(current_fragment_.range.start() -
current_sentence_.original_range.start(),
current_fragment_.range.end() -
current_sentence_.original_range.start())));
RecordGrammarAction(GrammarActions::kIgnored);
}
void GrammarManager::SetButtonHighlighted(
const ui::ime::AssistiveWindowButton& button,
bool highlighted) {
std::string error;
suggestion_handler_->SetButtonHighlighted(context_id_, button, highlighted,
&error);
if (!error.empty()) {
LOG(ERROR) << "Failed to set button highlighted. " << error;
}
}
} // namespace chromeos