blob: e3902b0d33ff7d3b76caa16526920bbbb6678edc [file] [log] [blame]
// Copyright 2020 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/autocorrect_manager.h"
#include "base/metrics/histogram_functions.h"
#include "base/strings/strcat.h"
#include "base/strings/utf_string_conversions.h"
#include "base/time/time.h"
#include "chrome/browser/chromeos/input_method/assistive_window_properties.h"
#include "chrome/browser/chromeos/input_method/suggestion_enums.h"
#include "chrome/grit/generated_resources.h"
#include "ui/base/ime/chromeos/extension_ime_util.h"
#include "ui/base/ime/chromeos/ime_bridge.h"
#include "ui/base/ime/chromeos/ime_input_context_handler_interface.h"
#include "ui/base/ime/chromeos/input_method_manager.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/events/keycodes/dom/dom_code.h"
namespace chromeos {
namespace {
// These values are persisted to logs. Entries should not be renumbered and
// numeric values should never be reused. Needs to match ImeAutocorrectActions
// in enums.xml.
enum class AutocorrectActions {
kWindowShown = 0,
kUnderlined = 1,
kReverted = 2,
kMaxValue = kReverted,
};
bool IsCurrentInputMethodExperimentalMultilingual() {
auto* input_method_manager = input_method::InputMethodManager::Get();
if (!input_method_manager) {
return false;
}
return extension_ime_util::IsExperimentalMultilingual(
input_method_manager->GetActiveIMEState()->GetCurrentInputMethod().id());
}
void LogAssistiveAutocorrectAction(AutocorrectActions action) {
base::UmaHistogramEnumeration("InputMethod.Assistive.Autocorrect.Actions",
action);
if (IsCurrentInputMethodExperimentalMultilingual()) {
base::UmaHistogramEnumeration(
"InputMethod.MultilingualExperiment.Autocorrect.Actions", action);
}
}
void LogAssistiveAutocorrectDelay(base::TimeDelta delay) {
base::UmaHistogramMediumTimes("InputMethod.Assistive.Autocorrect.Delay",
delay);
if (IsCurrentInputMethodExperimentalMultilingual()) {
base::UmaHistogramMediumTimes(
"InputMethod.MultilingualExperiment.Autocorrect.Delay", delay);
}
}
void RecordAssistiveCoverage(AssistiveType type) {
base::UmaHistogramEnumeration("InputMethod.Assistive.Coverage", type);
}
void RecordAssistiveSuccess(AssistiveType type) {
base::UmaHistogramEnumeration("InputMethod.Assistive.Success", type);
}
constexpr int kKeysUntilAutocorrectWindowHides = 4;
} // namespace
AutocorrectManager::AutocorrectManager(
SuggestionHandlerInterface* suggestion_handler)
: suggestion_handler_(suggestion_handler) {}
void AutocorrectManager::HandleAutocorrect(gfx::Range autocorrect_range,
const std::string& original_text) {
// TODO(crbug/1111135): call setAutocorrectTime() (for metrics)
// TODO(crbug/1111135): record metric (coverage)
ui::IMEInputContextHandlerInterface* input_context =
ui::IMEBridge::Get()->GetInputContextHandler();
if (!input_context)
return;
original_text_ = original_text;
key_presses_until_underline_hide_ = kKeysUntilAutocorrectWindowHides;
ClearUnderline();
input_context->SetAutocorrectRange(autocorrect_range);
LogAssistiveAutocorrectAction(AutocorrectActions::kUnderlined);
RecordAssistiveCoverage(AssistiveType::kAutocorrectUnderlined);
autocorrect_time_ = base::TimeTicks::Now();
}
bool AutocorrectManager::OnKeyEvent(const ui::KeyEvent& event) {
if (event.type() != ui::ET_KEY_PRESSED) {
return false;
}
if (event.code() == ui::DomCode::ARROW_UP && window_visible) {
std::string error;
auto button = ui::ime::AssistiveWindowButton();
button.id = ui::ime::ButtonId::kUndo;
button.window_type = ui::ime::AssistiveWindowType::kUndoWindow;
button.announce_string =
l10n_util::GetStringFUTF8(IDS_SUGGESTION_AUTOCORRECT_UNDO_BUTTON,
base::UTF8ToUTF16(original_text_));
suggestion_handler_->SetButtonHighlighted(context_id_, button, true,
&error);
button_highlighted = true;
return true;
}
if (event.code() == ui::DomCode::ENTER && window_visible &&
button_highlighted) {
UndoAutocorrect();
return true;
}
if (key_presses_until_underline_hide_ > 0) {
--key_presses_until_underline_hide_;
}
if (key_presses_until_underline_hide_ == 0) {
ClearUnderline();
}
return false;
}
void AutocorrectManager::ClearUnderline() {
ui::IMEInputContextHandlerInterface* input_context =
ui::IMEBridge::Get()->GetInputContextHandler();
if (input_context) {
input_context->SetAutocorrectRange(gfx::Range());
}
}
void AutocorrectManager::OnSurroundingTextChanged(const base::string16& text,
const int cursor_pos,
const int anchpr_pos) {
std::string error;
ui::IMEInputContextHandlerInterface* input_context =
ui::IMEBridge::Get()->GetInputContextHandler();
const gfx::Range range = input_context->GetAutocorrectRange();
if (!range.is_empty() && cursor_pos >= range.start() &&
cursor_pos <= range.end()) {
if (!window_visible) {
const std::string autocorrected_text =
base::UTF16ToUTF8(text.substr(range.start(), range.length()));
chromeos::AssistiveWindowProperties properties;
properties.type = ui::ime::AssistiveWindowType::kUndoWindow;
properties.visible = true;
properties.announce_string = l10n_util::GetStringFUTF8(
IDS_SUGGESTION_AUTOCORRECT_UNDO_WINDOW_SHOWN,
base::UTF8ToUTF16(original_text_),
base::UTF8ToUTF16(autocorrected_text));
window_visible = true;
button_highlighted = false;
suggestion_handler_->SetAssistiveWindowProperties(context_id_, properties,
&error);
LogAssistiveAutocorrectAction(AutocorrectActions::kWindowShown);
RecordAssistiveCoverage(AssistiveType::kAutocorrectWindowShown);
}
key_presses_until_underline_hide_ = kKeysUntilAutocorrectWindowHides;
} else if (window_visible) {
chromeos::AssistiveWindowProperties properties;
properties.type = ui::ime::AssistiveWindowType::kUndoWindow;
properties.visible = false;
window_visible = false;
button_highlighted = false;
suggestion_handler_->SetAssistiveWindowProperties(context_id_, properties,
&error);
}
}
void AutocorrectManager::OnFocus(int context_id) {
context_id_ = context_id;
}
void AutocorrectManager::UndoAutocorrect() {
// TODO(crbug/1111135): error handling and metrics
std::string error;
chromeos::AssistiveWindowProperties properties;
properties.type = ui::ime::AssistiveWindowType::kUndoWindow;
properties.visible = false;
window_visible = false;
button_highlighted = false;
window_visible = false;
suggestion_handler_->SetAssistiveWindowProperties(context_id_, properties,
&error);
ui::IMEInputContextHandlerInterface* input_context =
ui::IMEBridge::Get()->GetInputContextHandler();
const gfx::Range range = input_context->GetAutocorrectRange();
const ui::SurroundingTextInfo surrounding_text =
input_context->GetSurroundingTextInfo();
// TODO(crbug/1111135): Can we get away with deleting less text?
// This will not quite work properly if there is text actually highlighted,
// and cursor is at end of the highlight block, but no easy way around it.
// First delete everything before cursor.
input_context->DeleteSurroundingText(
-static_cast<int>(surrounding_text.selection_range.start()),
surrounding_text.surrounding_text.length());
// Submit the text after the cursor in composition mode to leave the cursor at
// the start
ui::CompositionText composition_text;
composition_text.text = surrounding_text.surrounding_text.substr(range.end());
input_context->UpdateCompositionText(composition_text,
/*cursor_pos=*/0, /*visible=*/true);
input_context->ConfirmCompositionText(/*reset_engine=*/false,
/*keep_selection=*/true);
// Insert the text before the cursor - now there should be the correct text
// and the cursor position will not have changed.
input_context->CommitText(
(base::UTF16ToUTF8(
surrounding_text.surrounding_text.substr(0, range.start())) +
original_text_));
LogAssistiveAutocorrectAction(AutocorrectActions::kReverted);
RecordAssistiveCoverage(AssistiveType::kAutocorrectReverted);
RecordAssistiveSuccess(AssistiveType::kAutocorrectReverted);
LogAssistiveAutocorrectDelay(base::TimeTicks::Now() - autocorrect_time_);
}
} // namespace chromeos