blob: 9b1b0f9f2771f9ee5c74f43f37143452ddfa0d0b [file] [log] [blame]
// Copyright (c) 2011 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/status/input_method_menu.h"
#include <string>
#include <vector>
#include "base/string_split.h"
#include "base/string_util.h"
#include "base/time.h"
#include "base/utf_string_conversions.h"
#include "chrome/browser/browser_process.h"
#include "chrome/browser/chromeos/cros/cros_library.h"
#include "chrome/browser/chromeos/input_method/input_method_util.h"
#include "chrome/browser/chromeos/language_preferences.h"
#include "chrome/browser/metrics/user_metrics.h"
#include "chrome/browser/prefs/pref_service.h"
#include "chrome/common/pref_names.h"
#include "content/common/notification_service.h"
#include "grit/generated_resources.h"
#include "grit/theme_resources.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/resource/resource_bundle.h"
// The language menu consists of 3 parts (in this order):
//
// (1) input method names. The size of the list is always >= 1.
// (2) input method properties. This list might be empty.
// (3) "Customize language and input..." button.
//
// Example of the menu (Japanese):
//
// ============================== (border of the popup window)
// [ ] English (|index| in the following functions is 0)
// [*] Japanese
// [ ] Chinese (Simplified)
// ------------------------------ (separator)
// [*] Hiragana (index = 5, The property has 2 radio groups)
// [ ] Katakana
// [ ] HalfWidthKatakana
// [*] Roman
// [ ] Kana
// ------------------------------ (separator)
// Customize language and input...(index = 11)
// ============================== (border of the popup window)
//
// Example of the menu (Simplified Chinese):
//
// ============================== (border of the popup window)
// [ ] English
// [ ] Japanese
// [*] Chinese (Simplified)
// ------------------------------ (separator)
// Switch to full letter mode (The property has 2 command buttons)
// Switch to half punctuation mode
// ------------------------------ (separator)
// Customize language and input...
// ============================== (border of the popup window)
//
namespace {
// Constants to specify the type of items in |model_|.
enum {
COMMAND_ID_INPUT_METHODS = 0, // English, Chinese, Japanese, Arabic, ...
COMMAND_ID_IME_PROPERTIES, // Hiragana, Katakana, ...
COMMAND_ID_CUSTOMIZE_LANGUAGE, // "Customize language and input..." button.
};
// A group ID for IME properties starts from 0. We use the huge value for the
// input method list to avoid conflict.
const int kRadioGroupLanguage = 1 << 16;
const int kRadioGroupNone = -1;
// A mapping from an input method id to a string for the language indicator. The
// mapping is necessary since some input methods belong to the same language.
// For example, both "xkb:us::eng" and "xkb:us:dvorak:eng" are for US English.
const struct {
const char* input_method_id;
const char* indicator_text;
} kMappingFromIdToIndicatorText[] = {
// To distinguish from "xkb:us::eng"
{ "xkb:us:altgr-intl:eng", "EXTD" },
{ "xkb:us:dvorak:eng", "DV" },
{ "xkb:us:intl:eng", "INTL" },
{ "xkb:us:colemak:eng", "CO" },
{ "xkb:de:neo:ger", "NEO" },
// To distinguish from "xkb:gb::eng"
{ "xkb:gb:dvorak:eng", "DV" },
// To distinguish from "xkb:jp::jpn"
{ "mozc", "\xe3\x81\x82" }, // U+3042, Japanese Hiragana letter A in UTF-8.
{ "mozc-dv", "\xe3\x81\x82" },
{ "mozc-jp", "\xe3\x81\x82" },
// For simplified Chinese input methods
{ "pinyin", "\xe6\x8b\xbc" }, // U+62FC
// For traditional Chinese input methods
{ "mozc-chewing", "\xe9\x85\xb7" }, // U+9177
{ "m17n:zh:cangjie", "\xe5\x80\x89" }, // U+5009
{ "m17n:zh:quick", "\xe9\x80\x9f" }, // U+901F
// For Hangul input method.
{ "hangul", "\xed\x95\x9c" }, // U+D55C
};
const size_t kMappingFromIdToIndicatorTextLen =
ARRAYSIZE_UNSAFE(kMappingFromIdToIndicatorText);
// Returns the language name for the given |language_code|.
std::wstring GetLanguageName(const std::string& language_code) {
const string16 language_name = l10n_util::GetDisplayNameForLocale(
language_code, g_browser_process->GetApplicationLocale(), true);
return UTF16ToWide(language_name);
}
} // namespace
namespace chromeos {
////////////////////////////////////////////////////////////////////////////////
// InputMethodMenu
InputMethodMenu::InputMethodMenu(PrefService* pref_service,
StatusAreaHost::ScreenMode screen_mode,
bool for_out_of_box_experience_dialog)
: input_method_descriptors_(CrosLibrary::Get()->GetInputMethodLibrary()->
GetActiveInputMethods()),
model_(NULL),
// Be aware that the constructor of |input_method_menu_| calls
// GetItemCount() in this class. Therefore, GetItemCount() have to return
// 0 when |model_| is NULL.
ALLOW_THIS_IN_INITIALIZER_LIST(input_method_menu_(this)),
minimum_input_method_menu_width_(0),
pref_service_(pref_service),
screen_mode_(screen_mode),
for_out_of_box_experience_dialog_(for_out_of_box_experience_dialog) {
DCHECK(input_method_descriptors_.get() &&
!input_method_descriptors_->empty());
// Sync current and previous input methods on Chrome prefs with ibus-daemon.
if (pref_service_ && (screen_mode_ == StatusAreaHost::kBrowserMode)) {
previous_input_method_pref_.Init(
prefs::kLanguagePreviousInputMethod, pref_service, this);
current_input_method_pref_.Init(
prefs::kLanguageCurrentInputMethod, pref_service, this);
}
InputMethodLibrary* library = CrosLibrary::Get()->GetInputMethodLibrary();
library->AddObserver(this); // FirstObserverIsAdded() might be called back.
if (screen_mode_ == StatusAreaHost::kLoginMode) {
// This button is for the login screen.
registrar_.Add(this,
NotificationType::LOGIN_USER_CHANGED,
NotificationService::AllSources());
}
}
InputMethodMenu::~InputMethodMenu() {
// RemoveObserver() is no-op if |this| object is already removed from the
// observer list.
CrosLibrary::Get()->GetInputMethodLibrary()->RemoveObserver(this);
}
////////////////////////////////////////////////////////////////////////////////
// ui::MenuModel implementation:
int InputMethodMenu::GetCommandIdAt(int index) const {
return index;
}
bool InputMethodMenu::IsItemDynamicAt(int index) const {
// Menu content for the language button could change time by time.
return true;
}
bool InputMethodMenu::GetAcceleratorAt(
int index, ui::Accelerator* accelerator) const {
// Views for Chromium OS does not support accelerators yet.
return false;
}
bool InputMethodMenu::IsItemCheckedAt(int index) const {
DCHECK_GE(index, 0);
DCHECK(input_method_descriptors_.get());
if (IndexIsInInputMethodList(index)) {
const InputMethodDescriptor& input_method
= input_method_descriptors_->at(index);
return input_method == CrosLibrary::Get()->GetInputMethodLibrary()->
current_input_method();
}
if (GetPropertyIndex(index, &index)) {
const ImePropertyList& property_list
= CrosLibrary::Get()->GetInputMethodLibrary()->current_ime_properties();
return property_list.at(index).is_selection_item_checked;
}
// Separator(s) or the "Customize language and input..." button.
return false;
}
int InputMethodMenu::GetGroupIdAt(int index) const {
DCHECK_GE(index, 0);
if (IndexIsInInputMethodList(index)) {
return for_out_of_box_experience_dialog_ ?
kRadioGroupNone : kRadioGroupLanguage;
}
if (GetPropertyIndex(index, &index)) {
const ImePropertyList& property_list
= CrosLibrary::Get()->GetInputMethodLibrary()->current_ime_properties();
return property_list.at(index).selection_item_id;
}
return kRadioGroupNone;
}
bool InputMethodMenu::HasIcons() const {
// We don't support icons on Chrome OS.
return false;
}
bool InputMethodMenu::GetIconAt(int index, SkBitmap* icon) {
return false;
}
ui::ButtonMenuItemModel* InputMethodMenu::GetButtonMenuItemAt(
int index) const {
return NULL;
}
bool InputMethodMenu::IsEnabledAt(int index) const {
// Just return true so all input method names and input method propertie names
// could be clicked.
return true;
}
ui::MenuModel* InputMethodMenu::GetSubmenuModelAt(int index) const {
// We don't use nested menus.
return NULL;
}
void InputMethodMenu::HighlightChangedTo(int index) {
// Views for Chromium OS does not support this interface yet.
}
void InputMethodMenu::MenuWillShow() {
// Views for Chromium OS does not support this interface yet.
}
void InputMethodMenu::SetMenuModelDelegate(ui::MenuModelDelegate* delegate) {
// Not needed for current usage.
}
int InputMethodMenu::GetItemCount() const {
if (!model_.get()) {
// Model is not constructed yet. This means that
// InputMethodMenu is being constructed. Return zero.
return 0;
}
return model_->GetItemCount();
}
ui::MenuModel::ItemType InputMethodMenu::GetTypeAt(int index) const {
DCHECK_GE(index, 0);
if (IndexPointsToConfigureImeMenuItem(index)) {
return ui::MenuModel::TYPE_COMMAND; // "Customize language and input"
}
if (IndexIsInInputMethodList(index)) {
return for_out_of_box_experience_dialog_ ?
ui::MenuModel::TYPE_COMMAND : ui::MenuModel::TYPE_RADIO;
}
if (GetPropertyIndex(index, &index)) {
const ImePropertyList& property_list
= CrosLibrary::Get()->GetInputMethodLibrary()->current_ime_properties();
if (property_list.at(index).is_selection_item) {
return ui::MenuModel::TYPE_RADIO;
}
return ui::MenuModel::TYPE_COMMAND;
}
return ui::MenuModel::TYPE_SEPARATOR;
}
string16 InputMethodMenu::GetLabelAt(int index) const {
DCHECK_GE(index, 0);
DCHECK(input_method_descriptors_.get());
// We use IDS_OPTIONS_SETTINGS_LANGUAGES_CUSTOMIZE here as the button
// opens the same dialog that is opened from the main options dialog.
if (IndexPointsToConfigureImeMenuItem(index)) {
return l10n_util::GetStringUTF16(IDS_OPTIONS_SETTINGS_LANGUAGES_CUSTOMIZE);
}
std::wstring name;
if (IndexIsInInputMethodList(index)) {
name = GetTextForMenu(input_method_descriptors_->at(index));
} else if (GetPropertyIndex(index, &index)) {
InputMethodLibrary* library = CrosLibrary::Get()->GetInputMethodLibrary();
const ImePropertyList& property_list = library->current_ime_properties();
const std::string& input_method_id = library->current_input_method().id;
return input_method::GetStringUTF16(
property_list.at(index).label, input_method_id);
}
return WideToUTF16(name);
}
void InputMethodMenu::ActivatedAt(int index) {
DCHECK_GE(index, 0);
DCHECK(input_method_descriptors_.get());
if (IndexPointsToConfigureImeMenuItem(index)) {
OpenConfigUI();
return;
}
if (IndexIsInInputMethodList(index)) {
// Inter-IME switching.
const InputMethodDescriptor& input_method
= input_method_descriptors_->at(index);
CrosLibrary::Get()->GetInputMethodLibrary()->ChangeInputMethod(
input_method.id);
UserMetrics::RecordAction(
UserMetricsAction("LanguageMenuButton_InputMethodChanged"));
return;
}
if (GetPropertyIndex(index, &index)) {
// Intra-IME switching (e.g. Japanese-Hiragana to Japanese-Katakana).
const ImePropertyList& property_list
= CrosLibrary::Get()->GetInputMethodLibrary()->current_ime_properties();
const std::string key = property_list.at(index).key;
if (property_list.at(index).is_selection_item) {
// Radio button is clicked.
const int id = property_list.at(index).selection_item_id;
// First, deactivate all other properties in the same radio group.
for (int i = 0; i < static_cast<int>(property_list.size()); ++i) {
if (i != index && id == property_list.at(i).selection_item_id) {
CrosLibrary::Get()->GetInputMethodLibrary()->SetImePropertyActivated(
property_list.at(i).key, false);
}
}
// Then, activate the property clicked.
CrosLibrary::Get()->GetInputMethodLibrary()->SetImePropertyActivated(
key, true);
} else {
// Command button like "Switch to half punctuation mode" is clicked.
// We can always use "Deactivate" for command buttons.
CrosLibrary::Get()->GetInputMethodLibrary()->SetImePropertyActivated(
key, false);
}
return;
}
LOG(ERROR) << "Unexpected index: " << index;
}
////////////////////////////////////////////////////////////////////////////////
// views::ViewMenuDelegate implementation:
void InputMethodMenu::RunMenu(
views::View* unused_source, const gfx::Point& pt) {
PrepareForMenuOpen();
input_method_menu_.RunMenuAt(pt, views::Menu2::ALIGN_TOPRIGHT);
}
////////////////////////////////////////////////////////////////////////////////
// InputMethodLibrary::Observer implementation:
void InputMethodMenu::InputMethodChanged(
InputMethodLibrary* obj,
const InputMethodDescriptor& current_input_method,
size_t num_active_input_methods) {
UpdateUIFromInputMethod(current_input_method, num_active_input_methods);
}
void InputMethodMenu::PreferenceUpdateNeeded(
InputMethodLibrary* obj,
const InputMethodDescriptor& previous_input_method,
const InputMethodDescriptor& current_input_method) {
if (screen_mode_ == StatusAreaHost::kBrowserMode) {
if (pref_service_) { // make sure we're not in unit tests.
// Sometimes (e.g. initial boot) |previous_input_method.id| is empty.
previous_input_method_pref_.SetValue(previous_input_method.id);
current_input_method_pref_.SetValue(current_input_method.id);
pref_service_->ScheduleSavePersistentPrefs();
}
} else if (screen_mode_ == StatusAreaHost::kLoginMode) {
if (g_browser_process && g_browser_process->local_state()) {
g_browser_process->local_state()->SetString(
language_prefs::kPreferredKeyboardLayout, current_input_method.id);
g_browser_process->local_state()->SavePersistentPrefs();
}
}
}
void InputMethodMenu::PropertyListChanged(
InputMethodLibrary* obj,
const ImePropertyList& current_ime_properties) {
// Usual order of notifications of input method change is:
// 1. RegisterProperties(empty)
// 2. RegisterProperties(list-of-new-properties)
// 3. GlobalInputMethodChanged
// However, due to the asynchronicity, we occasionally (but rarely) face to
// 1. RegisterProperties(empty)
// 2. GlobalInputMethodChanged
// 3. RegisterProperties(list-of-new-properties)
// this order. On this unusual case, we must rebuild the menu after the last
// RegisterProperties. For the other cases, no rebuild is needed. Actually
// it is better to be avoided. Otherwise users can sometimes observe the
// awkward clear-then-register behavior.
if (!current_ime_properties.empty()) {
InputMethodLibrary* library = CrosLibrary::Get()->GetInputMethodLibrary();
const InputMethodDescriptor& input_method = library->current_input_method();
size_t num_active_input_methods = library->GetNumActiveInputMethods();
UpdateUIFromInputMethod(input_method, num_active_input_methods);
}
}
void InputMethodMenu::FirstObserverIsAdded(InputMethodLibrary* obj) {
// NOTICE: Since this function might be called from the constructor of this
// class, it's better to avoid calling virtual functions.
if (pref_service_ && (screen_mode_ == StatusAreaHost::kBrowserMode)) {
// Get the input method name in the Preferences file which was in use last
// time, and switch to the method. We remember two input method names in the
// preference so that the Control+space hot-key could work fine from the
// beginning. InputMethodChanged() will be called soon and the indicator
// will be updated.
InputMethodLibrary* library = CrosLibrary::Get()->GetInputMethodLibrary();
const std::string previous_input_method_id =
previous_input_method_pref_.GetValue();
if (!previous_input_method_id.empty()) {
library->ChangeInputMethod(previous_input_method_id);
}
const std::string current_input_method_id =
current_input_method_pref_.GetValue();
if (!current_input_method_id.empty()) {
library->ChangeInputMethod(current_input_method_id);
}
}
}
void InputMethodMenu::PrepareForMenuOpen() {
UserMetrics::RecordAction(UserMetricsAction("LanguageMenuButton_Open"));
PrepareMenu();
}
void InputMethodMenu::PrepareMenu() {
input_method_descriptors_.reset(CrosLibrary::Get()->GetInputMethodLibrary()->
GetActiveInputMethods());
RebuildModel();
input_method_menu_.Rebuild();
if (minimum_input_method_menu_width_ > 0) {
input_method_menu_.SetMinimumWidth(minimum_input_method_menu_width_);
}
}
void InputMethodMenu::ActiveInputMethodsChanged(
InputMethodLibrary* obj,
const InputMethodDescriptor& current_input_method,
size_t num_active_input_methods) {
// Update the icon if active input methods are changed. See also
// comments in UpdateUI() in input_method_menu_button.cc.
UpdateUIFromInputMethod(current_input_method, num_active_input_methods);
}
void InputMethodMenu::UpdateUIFromInputMethod(
const InputMethodDescriptor& input_method,
size_t num_active_input_methods) {
const std::wstring name = GetTextForIndicator(input_method);
const std::wstring tooltip = GetTextForMenu(input_method);
UpdateUI(input_method.id, name, tooltip, num_active_input_methods);
}
void InputMethodMenu::RebuildModel() {
model_.reset(new ui::SimpleMenuModel(NULL));
string16 dummy_label = UTF8ToUTF16("");
// Indicates if separator's needed before each section.
bool need_separator = false;
if (!input_method_descriptors_->empty()) {
// We "abuse" the command_id and group_id arguments of AddRadioItem method.
// A COMMAND_ID_XXX enum value is passed as command_id, and array index of
// |input_method_descriptors_| or |property_list| is passed as group_id.
for (size_t i = 0; i < input_method_descriptors_->size(); ++i) {
model_->AddRadioItem(COMMAND_ID_INPUT_METHODS, dummy_label, i);
}
need_separator = true;
}
const ImePropertyList& property_list
= CrosLibrary::Get()->GetInputMethodLibrary()->current_ime_properties();
if (!property_list.empty()) {
if (need_separator) {
model_->AddSeparator();
}
for (size_t i = 0; i < property_list.size(); ++i) {
model_->AddRadioItem(COMMAND_ID_IME_PROPERTIES, dummy_label, i);
}
need_separator = true;
}
if (ShouldSupportConfigUI()) {
// Note: We use AddSeparator() for separators, and AddRadioItem() for all
// other items even if an item is not actually a radio item.
if (need_separator) {
model_->AddSeparator();
}
model_->AddRadioItem(COMMAND_ID_CUSTOMIZE_LANGUAGE, dummy_label,
0 /* dummy */);
}
}
bool InputMethodMenu::IndexIsInInputMethodList(int index) const {
DCHECK_GE(index, 0);
DCHECK(model_.get());
if (index >= model_->GetItemCount()) {
return false;
}
return ((model_->GetTypeAt(index) == ui::MenuModel::TYPE_RADIO) &&
(model_->GetCommandIdAt(index) == COMMAND_ID_INPUT_METHODS) &&
input_method_descriptors_.get() &&
(index < static_cast<int>(input_method_descriptors_->size())));
}
bool InputMethodMenu::GetPropertyIndex(int index, int* property_index) const {
DCHECK_GE(index, 0);
DCHECK(property_index);
DCHECK(model_.get());
if (index >= model_->GetItemCount()) {
return false;
}
if ((model_->GetTypeAt(index) == ui::MenuModel::TYPE_RADIO) &&
(model_->GetCommandIdAt(index) == COMMAND_ID_IME_PROPERTIES)) {
const int tmp_property_index = model_->GetGroupIdAt(index);
const ImePropertyList& property_list
= CrosLibrary::Get()->GetInputMethodLibrary()->current_ime_properties();
if (tmp_property_index < static_cast<int>(property_list.size())) {
*property_index = tmp_property_index;
return true;
}
}
return false;
}
bool InputMethodMenu::IndexPointsToConfigureImeMenuItem(int index) const {
DCHECK_GE(index, 0);
DCHECK(model_.get());
if (index >= model_->GetItemCount()) {
return false;
}
return ((model_->GetTypeAt(index) == ui::MenuModel::TYPE_RADIO) &&
(model_->GetCommandIdAt(index) == COMMAND_ID_CUSTOMIZE_LANGUAGE));
}
std::wstring InputMethodMenu::GetTextForIndicator(
const InputMethodDescriptor& input_method) {
// For the status area, we use two-letter, upper-case language code like
// "US" and "JP".
std::wstring text;
// Check special cases first.
for (size_t i = 0; i < kMappingFromIdToIndicatorTextLen; ++i) {
if (kMappingFromIdToIndicatorText[i].input_method_id == input_method.id) {
text = UTF8ToWide(kMappingFromIdToIndicatorText[i].indicator_text);
break;
}
}
// Display the keyboard layout name when using a keyboard layout.
if (text.empty() && input_method::IsKeyboardLayout(input_method.id)) {
const size_t kMaxKeyboardLayoutNameLen = 2;
const std::wstring keyboard_layout = UTF8ToWide(
input_method::GetKeyboardLayoutName(input_method.id));
text = StringToUpperASCII(keyboard_layout).substr(
0, kMaxKeyboardLayoutNameLen);
}
// TODO(yusukes): Some languages have two or more input methods. For example,
// Thai has 3, Vietnamese has 4. If these input methods could be activated at
// the same time, we should do either of the following:
// (1) Add mappings to |kMappingFromIdToIndicatorText|
// (2) Add suffix (1, 2, ...) to |text| when ambiguous.
if (text.empty()) {
const size_t kMaxLanguageNameLen = 2;
std::string language_code =
input_method::GetLanguageCodeFromDescriptor(input_method);
// Use "CN" for simplified Chinese and "TW" for traditonal Chinese,
// rather than "ZH".
if (StartsWithASCII(language_code, "zh-", false)) {
std::vector<std::string> portions;
base::SplitString(language_code, '-', &portions);
if (portions.size() >= 2 && !portions[1].empty()) {
language_code = portions[1];
}
}
text = StringToUpperASCII(UTF8ToWide(language_code)).substr(
0, kMaxLanguageNameLen);
}
DCHECK(!text.empty());
return text;
}
std::wstring InputMethodMenu::GetTextForMenu(
const InputMethodDescriptor& input_method) {
// We don't show language here. Name of keyboard layout or input method
// usually imply (or explicitly include) its language.
// Special case for Dutch, French and German: these languages have multiple
// keyboard layouts and share the same laout of keyboard (Belgian). We need to
// show explicitly the language for the layout.
// For Arabic and Hindi: they share "Standard Input Method".
const std::string language_code
= input_method::GetLanguageCodeFromDescriptor(input_method);
std::wstring text;
if (language_code == "ar" ||
language_code == "hi" ||
language_code == "nl" ||
language_code == "fr" ||
language_code == "de") {
text = GetLanguageName(language_code) + L" - ";
}
text += input_method::GetString(input_method.display_name, input_method.id);
DCHECK(!text.empty());
return text;
}
void InputMethodMenu::RegisterPrefs(PrefService* local_state) {
local_state->RegisterStringPref(language_prefs::kPreferredKeyboardLayout, "");
}
void InputMethodMenu::Observe(NotificationType type,
const NotificationSource& source,
const NotificationDetails& details) {
if (type == NotificationType::LOGIN_USER_CHANGED) {
// When a user logs in, we should remove |this| object from the observer
// list so that PreferenceUpdateNeeded() does not update the local state
// anymore.
CrosLibrary::Get()->GetInputMethodLibrary()->RemoveObserver(this);
}
}
void InputMethodMenu::SetMinimumWidth(int width) {
// On the OOBE network selection screen, fixed width menu would be preferable.
minimum_input_method_menu_width_ = width;
}
} // namespace chromeos