blob: b915aa838e737c735cbd9cd93b914b5ea2297573 [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/emoji_suggester.h"
#include "base/files/file_util.h"
#include "base/i18n/number_formatting.h"
#include "base/strings/string_split.h"
#include "base/strings/string_util.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 if (event.key == "Esc") {
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() || !base::IsAsciiWhitespace(text[text.length() - 1]))
return false;
std::string last_word = GetLastWord(base::UTF16ToUTF8(text));
if (!last_word.empty() && emoji_map_.count(last_word)) {
ShowSuggestion(last_word);
return true;
}
return false;
}
void EmojiSuggester::ShowSuggestion(const std::string& text) {
std::string error;
suggestion_shown_ = true;
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;
suggestion_shown_ = false;
engine_->SetCandidateWindowVisible(false, &error);
if (!error.empty()) {
LOG(ERROR) << "Failed to dismiss suggestion. " << error;
}
}
AssistiveType EmojiSuggester::GetProposeActionType() {
return AssistiveType::kEmoji;
}
} // namespace chromeos