Commit 96e66c5c authored by My Nguyen's avatar My Nguyen Committed by Commit Bot

Add emoji suggester

MVP.
Use the old CandidateWindow for UI.
Screenshot view here http://screen/RMd0QQFyoNo
Emoji-map lives in Google3, view here
http://google3/i18n/input/javascript/chos/ui/resource/emoji/emoji-map.csv
Interactions:
-Tab,Right,Enter commit the chosen emoji
-Up,Down moves to choose candidates

Bug: 1065308
Change-Id: If1b3a662a3a6e7e410099c1abca8cfce3d2a1a32
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2123494
Commit-Queue: My Nguyen <myy@chromium.org>
Reviewed-by: default avatarDarren Shen <shend@chromium.org>
Reviewed-by: default avatarKeith Lee <keithlee@chromium.org>
Cr-Commit-Position: refs/heads/master@{#755984}
parent 2aa99b91
......@@ -1261,6 +1261,8 @@ source_set("chromeos") {
"input_method/candidate_window_controller_impl.h",
"input_method/component_extension_ime_manager_impl.cc",
"input_method/component_extension_ime_manager_impl.h",
"input_method/emoji_suggester.cc",
"input_method/emoji_suggester.h",
"input_method/ime_service_connector.cc",
"input_method/ime_service_connector.h",
"input_method/input_method_configuration.cc",
......
......@@ -29,24 +29,31 @@ bool IsAssistPersonalInfoEnabled() {
return base::FeatureList::IsEnabled(chromeos::features::kAssistPersonalInfo);
}
bool IsEmojiSuggestAdditionEnabled() {
return base::FeatureList::IsEnabled(
chromeos::features::kEmojiSuggestAddition);
}
} // namespace
bool IsAssistiveFeatureEnabled() {
return IsAssistPersonalInfoEnabled();
return IsAssistPersonalInfoEnabled() || IsEmojiSuggestAdditionEnabled();
}
AssistiveSuggester::AssistiveSuggester(InputMethodEngine* engine,
Profile* profile)
: personal_info_suggester_(engine, profile) {}
: personal_info_suggester_(engine, profile), emoji_suggester_(engine) {}
void AssistiveSuggester::OnFocus(int context_id) {
context_id_ = context_id;
personal_info_suggester_.OnFocus(context_id_);
emoji_suggester_.OnFocus(context_id_);
}
void AssistiveSuggester::OnBlur() {
context_id_ = -1;
personal_info_suggester_.OnBlur();
emoji_suggester_.OnBlur();
}
bool AssistiveSuggester::OnKeyEvent(
......@@ -122,6 +129,10 @@ void AssistiveSuggester::Suggest(const base::string16& text,
if (IsAssistPersonalInfoEnabled() &&
personal_info_suggester_.Suggest(text_before_cursor)) {
current_suggester_ = &personal_info_suggester_;
} else if (IsEmojiSuggestAdditionEnabled() &&
emoji_suggester_.Suggest(text_before_cursor)) {
current_suggester_ = &emoji_suggester_;
RecordAssistiveCoverage(current_suggester_->GetProposeActionType());
} else {
current_suggester_ = nullptr;
}
......
......@@ -7,6 +7,7 @@
#include <string>
#include "chrome/browser/chromeos/input_method/emoji_suggester.h"
#include "chrome/browser/chromeos/input_method/input_method_engine.h"
#include "chrome/browser/chromeos/input_method/personal_info_suggester.h"
#include "chrome/browser/chromeos/input_method/suggester.h"
......@@ -59,6 +60,7 @@ class AssistiveSuggester {
bool IsSuggestionShown();
PersonalInfoSuggester personal_info_suggester_;
EmojiSuggester emoji_suggester_;
// ID of the focused text field, 0 if none is focused.
int context_id_ = -1;
......
// 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/emoji_suggester.h"
#include "base/files/file_util.h"
#include "base/i18n/number_formatting.h"
#include "base/strings/string_split.h"
#include "base/strings/utf_string_conversions.h"
#include "base/task/task_traits.h"
#include "base/task/thread_pool.h"
#include "chromeos/services/ime/constants.h"
using input_method::InputMethodEngineBase;
namespace chromeos {
namespace {
const int MAX_CANDIDATE_SIZE = 5;
const base::FilePath::CharType kEmojiMapFilePath[] =
FILE_PATH_LITERAL("/emoji/emoji-map.csv");
std::string ReadEmojiDataFromFile() {
if (!base::DirectoryExists(base::FilePath(ime::kBundledInputMethodsDirPath)))
return base::EmptyString();
std::string emoji_data;
base::FilePath::StringType path(ime::kBundledInputMethodsDirPath);
path.append(kEmojiMapFilePath);
if (!base::ReadFileToString(base::FilePath(path), &emoji_data))
LOG(WARNING) << "Emoji map file missing.";
return emoji_data;
}
std::vector<std::string> SplitString(const std::string& str,
const std::string& delimiter) {
return base::SplitString(str, delimiter, base::TRIM_WHITESPACE,
base::SPLIT_WANT_NONEMPTY);
}
std::string GetLastWord(const std::string& str) {
size_t last_pos_to_search = str.length() - 1;
// Search from second last char if last char is a white space.
if (base::IsAsciiWhitespace(str.back()))
last_pos_to_search = str.length() - 2;
const auto space_before_last_word = str.find_last_of(" ", last_pos_to_search);
// If not found, return the entire string up to the last position to search
// else return the last word.
return space_before_last_word == std::string::npos
? str.substr(0, last_pos_to_search + 1)
: str.substr(space_before_last_word + 1,
last_pos_to_search - space_before_last_word);
}
// Create emoji suggestion's candidate window property.
InputMethodEngine::CandidateWindowProperty CreateProperty(int candidates_size) {
InputMethodEngine::CandidateWindowProperty properties_out;
properties_out.is_cursor_visible = true;
properties_out.page_size = std::min(candidates_size, MAX_CANDIDATE_SIZE);
properties_out.show_window_at_composition = false;
properties_out.is_vertical = true;
properties_out.is_auxiliary_text_visible = false;
return properties_out;
}
} // namespace
EmojiSuggester::EmojiSuggester(InputMethodEngine* engine) : engine_(engine) {
LoadEmojiMap();
}
EmojiSuggester::~EmojiSuggester() = default;
void EmojiSuggester::LoadEmojiMap() {
base::ThreadPool::PostTaskAndReplyWithResult(
FROM_HERE, {base::MayBlock()}, base::BindOnce(&ReadEmojiDataFromFile),
base::BindOnce(&EmojiSuggester::OnEmojiDataLoaded,
weak_factory_.GetWeakPtr()));
}
void EmojiSuggester::OnEmojiDataLoaded(const std::string& emoji_data) {
// Split data into lines.
for (const auto& line : SplitString(emoji_data, "\n")) {
// Get a word and a string of emojis from the line.
const auto comma_pos = line.find_first_of(",");
DCHECK(comma_pos != std::string::npos);
std::string word = line.substr(0, comma_pos);
std::string emojis = line.substr(comma_pos + 1);
// Build emoji_map_ from splitting the string of emojis.
emoji_map_[word] = SplitString(emojis, ";");
}
}
void EmojiSuggester::OnFocus(int context_id) {
context_id_ = context_id;
}
void EmojiSuggester::OnBlur() {
context_id_ = -1;
}
SuggestionStatus EmojiSuggester::HandleKeyEvent(
const InputMethodEngineBase::KeyboardEvent& event) {
if (!suggestion_shown_)
return SuggestionStatus::kNotHandled;
SuggestionStatus status = SuggestionStatus::kNotHandled;
std::string error;
if (event.key == "Tab" || event.key == "Right" || event.key == "Enter") {
suggestion_shown_ = false;
engine_->CommitText(context_id_, candidates_[candidate_id_].value.c_str(),
&error);
engine_->SetCandidateWindowVisible(false, &error);
status = SuggestionStatus::kAccept;
} else if (event.key == "Down") {
candidate_id_ < static_cast<int>(candidates_.size()) - 1
? candidate_id_++
: candidate_id_ = 0;
engine_->SetCursorPosition(context_id_, candidate_id_, &error);
status = SuggestionStatus::kBrowsing;
} else if (event.key == "Up") {
candidate_id_ > 0
? candidate_id_--
: candidate_id_ = static_cast<int>(candidates_.size()) - 1;
engine_->SetCursorPosition(context_id_, candidate_id_, &error);
status = SuggestionStatus::kBrowsing;
} else {
DismissSuggestion();
suggestion_shown_ = false;
status = SuggestionStatus::kDismiss;
}
if (!error.empty()) {
LOG(ERROR) << "Fail to handle event. " << error;
}
return status;
}
bool EmojiSuggester::Suggest(const base::string16& text) {
if (emoji_map_.empty())
return false;
std::string last_word = GetLastWord(base::UTF16ToUTF8(text));
if (!last_word.empty() && emoji_map_.count(last_word)) {
ShowSuggestion(last_word);
suggestion_shown_ = true;
}
return suggestion_shown_;
}
void EmojiSuggester::ShowSuggestion(const std::string& text) {
std::string error;
candidates_.clear();
const std::vector<std::string>& candidates = emoji_map_.at(text);
for (size_t i = 0; i < candidates.size(); i++) {
candidates_.emplace_back();
candidates_.back().value = candidates[i];
candidates_.back().id = i;
candidates_.back().label = base::UTF16ToUTF8(base::FormatNumber(i + 1));
}
engine_->SetCandidates(context_id_, candidates_, &error);
candidate_id_ = 0;
engine_->SetCandidateWindowProperty(
CreateProperty(static_cast<int>(candidates_.size())));
engine_->SetCandidateWindowVisible(true, &error);
engine_->SetCursorPosition(context_id_, candidate_id_, &error);
if (!error.empty()) {
LOG(ERROR) << "Fail to show suggestion. " << error;
}
}
void EmojiSuggester::DismissSuggestion() {
std::string error;
engine_->SetCandidateWindowVisible(false, &error);
if (!error.empty()) {
LOG(ERROR) << "Failed to dismiss suggestion. " << error;
}
}
AssistiveType EmojiSuggester::GetProposeActionType() {
return AssistiveType::kEmoji;
}
} // namespace chromeos
// 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.
#ifndef CHROME_BROWSER_CHROMEOS_INPUT_METHOD_EMOJI_SUGGESTER_H_
#define CHROME_BROWSER_CHROMEOS_INPUT_METHOD_EMOJI_SUGGESTER_H_
#include <string>
#include "chrome/browser/chromeos/input_method/input_method_engine.h"
#include "chrome/browser/chromeos/input_method/suggester.h"
#include "chrome/browser/chromeos/input_method/suggestion_enums.h"
#include "chrome/browser/ui/input_method/input_method_engine_base.h"
namespace chromeos {
// An agent to suggest emoji when the user types, and adopt or
// dismiss the suggestion according to the user action.
class EmojiSuggester : public Suggester {
public:
explicit EmojiSuggester(InputMethodEngine* engine);
~EmojiSuggester() override;
// Suggester overrides:
void OnFocus(int context_id) override;
void OnBlur() override;
SuggestionStatus HandleKeyEvent(
const ::input_method::InputMethodEngineBase::KeyboardEvent& event)
override;
bool Suggest(const base::string16& text) override;
void DismissSuggestion() override;
AssistiveType GetProposeActionType() override;
private:
void ShowSuggestion(const std::string& text);
void LoadEmojiMap();
void OnEmojiDataLoaded(const std::string& emoji_data);
InputMethodEngine* const engine_;
// ID of the focused text field, 0 if none is focused.
int context_id_ = -1;
// If we are showing a suggestion right now.
bool suggestion_shown_ = false;
// The current list of candidates.
std::vector<InputMethodEngine::Candidate> candidates_;
// The current candidate_id chosen.
int candidate_id_;
// The map holding one-word-mapping to emojis.
std::map<std::string, std::vector<std::string>> emoji_map_;
// Pointer for callback, must be the last declared in the file.
base::WeakPtrFactory<EmojiSuggester> weak_factory_{this};
};
} // namespace chromeos
#endif // CHROME_BROWSER_CHROMEOS_INPUT_METHOD_EMOJI_SUGGESTER_H_
......@@ -19,13 +19,15 @@ enum class AssistiveType {
kPersonalAddress = 2,
kPersonalPhoneNumber = 3,
kPersonalName = 4,
kMaxValue = kPersonalName,
kEmoji = 5,
kMaxValue = kEmoji,
};
enum class SuggestionStatus {
kNotHandled = 0,
kAccept = 1,
kDismiss = 2,
kBrowsing = 3,
};
} // namespace chromeos
......
......@@ -33445,6 +33445,7 @@ Called by update_gpu_driver_bug_workaround_entries.py.-->
<int value="2" label="PERSONAL_ADDRESS"/>
<int value="3" label="PERSONAL_PHONE_NUMBER"/>
<int value="4" label="PERSONAL_NAME"/>
<int value="5" label="EMOJI"/>
</enum>
<enum name="IMECommitType">
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment