// 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