| // Copyright (c) 2010 The Chromium OS 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 "chromeos_input_method.h" |
| |
| #include <dbus/dbus-glib-lowlevel.h> // for dbus_g_connection_get_connection. |
| #include <ibus.h> |
| |
| #include <algorithm> // for std::reverse. |
| #include <cstring> // for std::strcmp. |
| #include <sstream> |
| #include <stack> |
| #include <utility> |
| |
| #include "chromeos/dbus/dbus.h" |
| #include "chromeos/glib/object.h" |
| |
| namespace { |
| |
| const char kCandidateWindowService[] = "org.freedesktop.IBus.Panel"; |
| const char kCandidateWindowObjectPath[] = "/org/chromium/Chrome/LanguageBar"; |
| const char kCandidateWindowInterface[] = "org.freedesktop.IBus.Panel"; |
| |
| // The list of input method IDs that we handle. This filtering is necessary |
| // since some input methods are definitely unnecessary for us. For example, we |
| // should disable "ja:anthy", "zh:cangjie", and "zh:pinyin" engines in |
| // ibus-m17n since we (will) have better equivalents outside of ibus-m17n. |
| const char* kInputMethodIdsWhitelist[] = { |
| "anthy", // ibus-anthy (for libcros debugging on Ubuntu 9.10) - Japanese |
| "bopomofo", // Bopomofo engine in ibus-pinyin - Traditional Chinese |
| "chewing", // ibus-chewing - Traditional Chinese |
| "hangul", // ibus-hangul - Korean |
| "mozc", // ibus-mozc - Japanese (with English keyboard) |
| "mozc-jp", // ibus-mozc - Japanese (with Japanese keyboard) |
| "pinyin", // Pinyin engine in ibus-pinyin - Simplified Chinese |
| // TODO(yusukes): re-enable chewing once we resolve issue 1253. |
| |
| // ibus-table input methods. |
| "cangjie3", // ibus-table-cangjie - Traditional Chinese |
| "cangjie5", // ibus-table-cangjie - Traditional Chinese |
| // TODO(yusukes): Add additional ibus-table modules here once they're ready. |
| |
| // ibus-m17n input methods (language neutral ones). |
| "m17n:t:latn-pre", |
| "m17n:t:latn-post", |
| |
| // ibus-m17n input methods. |
| "m17n:ar:kbd", // Arabic |
| "m17n:he:kbd", // Hebrew |
| "m17n:hi:itrans", // Hindi |
| // Note: the m17n-contrib package has some more Hindi definitions. |
| "m17n:fa:isiri", // Persian |
| "m17n:th:kesmanee", // Thai (simulate the Kesmanee keyboard) |
| "m17n:th:pattachote", // Thai (simulate the Pattachote keyboard) |
| "m17n:th:tis820", // Thai (simulate the TIS-820.2538 keyboard) |
| "m17n:vi:tcvn", // Vietnames (TCVN6064 sequence) |
| "m17n:vi:telex", // Vietnames (TELEX key sequence) |
| "m17n:vi:viqr", // Vietnames (VIQR key sequence) |
| "m17n:vi:vni", // Vietnames (VNI key sequence) |
| // Note: Since ibus-m17n does not support "get-surrounding-text" feature yet, |
| // Vietnames input methods, except 4 input methods above, in m17n-db should |
| // not work fine. The 4 input methods in m17n-db (>= 1.6.0) don't require the |
| // feature. |
| |
| // ibux-xkb-layouts input methods (keyboard layouts). |
| "xkb:be::fra", // Belgium - French |
| "xkb:br::por", // Brazil - Portuguese |
| "xkb:bg::bul", // Bulgaria - Bulgarian |
| "xkb:cz::cze", // Czech Republic - Czech |
| "xkb:de::ger", // Germany - German |
| "xkb:ee::est", // Estonia - Estonian |
| "xkb:es::spa", // Spain - Spanish |
| "xkb:es:cat:cat", // Spain - Catalan |
| "xkb:dk::dan", // Denmark - Danish |
| "xkb:gr::gre", // Greece - Greek |
| "xkb:lt::lit", // Lithuania - Lithuanian |
| "xkb:lv::lav", // Latvia - Latvian |
| "xkb:hr::scr", // Croatia - Croatian |
| "xkb:nl::nld", // Netherlands - Dutch |
| "xkb:gb::eng", // United Kingdom - English |
| "xkb:fi::fin", // Finland - Finnish |
| "xkb:fr::fra", // France - French |
| "xkb:hu::hun", // Hungary - Hungarian |
| "xkb:it::ita", // Italy - Italian |
| "xkb:jp::jpn", // Japan - Japanese |
| "xkb:no::nor", // Norway - Norwegian |
| "xkb:pl::pol", // Poland - Polish |
| "xkb:pt::por", // Portugal - Portuguese |
| "xkb:ro::rum", // Romania - Romanian |
| "xkb:se::swe", // Sweden - Swedish |
| "xkb:sk::slo", // Slovakia - Slovak |
| "xkb:si::slv", // Slovenia - Slovene (Slovenian) |
| "xkb:rs::srp", // Serbia - Serbian |
| "xkb:ch::ger", // Switzerland - German |
| "xkb:ru::rus", // Russia - Russian |
| "xkb:tr::tur", // Turkey - Turkish |
| "xkb:ua::ukr", // Ukraine - Ukrainian |
| "xkb:us::eng", // US - English |
| "xkb:us:dvorak:eng", // US - Dvorak - English |
| // TODO(satorux): Add more keyboard layouts. |
| }; |
| |
| // The list of input method property keys that we don't handle. |
| const char* kInputMethodPropertyKeysBlacklist[] = { |
| "setup", // menu for showing setup dialog used in anthy and hangul. |
| "chewing_settings_prop", // menu for showing setup dialog used in chewing. |
| "status", // used in m17n. |
| }; |
| |
| const char* Or(const char* str1, const char* str2) { |
| return str1 ? str1 : str2; |
| } |
| |
| // Returns true if |key| is blacklisted. |
| bool PropertyKeyIsBlacklisted(const char* key) { |
| for (size_t i = 0; i < arraysize(kInputMethodPropertyKeysBlacklist); ++i) { |
| if (!std::strcmp(key, kInputMethodPropertyKeysBlacklist[i])) { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| // Returns true if |input_method_id| is whitelisted. |
| bool InputMethodIdIsWhitelisted(const char* input_method_id) { |
| // TODO(yusukes): Use hash_set if necessary. |
| const std::string id = input_method_id; |
| for (size_t i = 0; i < arraysize(kInputMethodIdsWhitelist); ++i) { |
| // Older version of m17n-db which is used in Ubuntu 9.10 |
| // doesn't add the "m17n:" prefix. We support both. |
| if ((id == kInputMethodIdsWhitelist[i]) || |
| ("m17n:" + id == kInputMethodIdsWhitelist[i])) { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| // Frees input method names in |engines| and the list itself. Please make sure |
| // that |engines| points the head of the list. |
| void FreeInputMethodNames(GList* engines) { |
| if (engines) { |
| for (GList* cursor = engines; cursor; cursor = g_list_next(cursor)) { |
| g_object_unref(IBUS_ENGINE_DESC(cursor->data)); |
| } |
| g_list_free(engines); |
| } |
| } |
| |
| // Copies input method names in |engines| to |out|. |
| void AddInputMethodNames( |
| const GList* engines, chromeos::InputMethodDescriptors* out) { |
| DCHECK(out); |
| for (; engines; engines = g_list_next(engines)) { |
| IBusEngineDesc* engine_desc = IBUS_ENGINE_DESC(engines->data); |
| if (InputMethodIdIsWhitelisted(engine_desc->name)) { |
| out->push_back(chromeos::InputMethodDescriptor(engine_desc->name, |
| engine_desc->longname, |
| engine_desc->layout, |
| engine_desc->language)); |
| DLOG(INFO) << engine_desc->name << " (SUPPORTED)"; |
| } |
| } |
| } |
| |
| // Returns IBusInputContext for |input_context_path|. NULL on errors. |
| IBusInputContext* GetInputContext( |
| const std::string& input_context_path, IBusBus* ibus) { |
| IBusInputContext* context = ibus_input_context_get_input_context( |
| input_context_path.c_str(), ibus_bus_get_connection(ibus)); |
| if (!context) { |
| LOG(ERROR) << "IBusInputContext is null: " << input_context_path; |
| } |
| return context; |
| } |
| |
| // Returns true if |prop| has children. |
| bool PropertyHasChildren(IBusProperty* prop) { |
| return prop && prop->sub_props && ibus_prop_list_get(prop->sub_props, 0); |
| } |
| |
| // This function is called by and FlattenProperty() and converts IBus |
| // representation of a property, |ibus_prop|, to our own and push_back the |
| // result to |out_prop_list|. This function returns true on success, and |
| // returns false if sanity checks for |ibus_prop| fail. |
| bool ConvertProperty(IBusProperty* ibus_prop, |
| int selection_item_id, |
| chromeos::ImePropertyList* out_prop_list) { |
| DCHECK(ibus_prop); |
| DCHECK(ibus_prop->key); |
| DCHECK(out_prop_list); |
| |
| // Sanity checks. |
| const bool has_sub_props = PropertyHasChildren(ibus_prop); |
| if (has_sub_props && (ibus_prop->type != PROP_TYPE_MENU)) { |
| LOG(ERROR) << "The property has sub properties, " |
| << "but the type of the property is not PROP_TYPE_MENU"; |
| return false; |
| } |
| if ((!has_sub_props) && (ibus_prop->type == PROP_TYPE_MENU)) { |
| // This is usually not an error. ibus-daemon sometimes sends empty props. |
| DLOG(INFO) << "Property list is empty"; |
| return false; |
| } |
| if (ibus_prop->type == PROP_TYPE_SEPARATOR || |
| ibus_prop->type == PROP_TYPE_MENU) { |
| // This is not an error, but we don't push an item for these types. |
| return true; |
| } |
| |
| const bool is_selection_item = (ibus_prop->type == PROP_TYPE_RADIO); |
| selection_item_id |
| = is_selection_item ? selection_item_id : kInvalidSelectionItemId; |
| |
| bool is_selection_item_checked = false; |
| if (ibus_prop->state == PROP_STATE_INCONSISTENT) { |
| LOG(WARNING) << "The property is in PROP_STATE_INCONSISTENT, " |
| << "which is not supported."; |
| } else if ((!is_selection_item) && (ibus_prop->state == PROP_STATE_CHECKED)) { |
| LOG(WARNING) << "PROP_STATE_CHECKED is meaningful only if the type is " |
| << "PROP_TYPE_RADIO."; |
| } else { |
| is_selection_item_checked = (ibus_prop->state == PROP_STATE_CHECKED); |
| } |
| |
| if (!ibus_prop->key) { |
| LOG(ERROR) << "key is NULL"; |
| } |
| if (ibus_prop->tooltip && (!ibus_prop->tooltip->text)) { |
| LOG(ERROR) << "tooltip is NOT NULL, but tooltip->text IS NULL: key=" |
| << Or(ibus_prop->key, ""); |
| } |
| if (ibus_prop->label && (!ibus_prop->label->text)) { |
| LOG(ERROR) << "label is NOT NULL, but label->text IS NULL: key=" |
| << Or(ibus_prop->key, ""); |
| } |
| |
| // This label will be localized on Chrome side. |
| // See src/chrome/browser/chromeos/status/language_menu_l10n_util.h. |
| std::string label = |
| ((ibus_prop->tooltip && |
| ibus_prop->tooltip->text) ? ibus_prop->tooltip->text : ""); |
| if (label.empty()) { |
| // Usually tooltips are more descriptive than labels. |
| label = (ibus_prop->label && ibus_prop->label->text) |
| ? ibus_prop->label->text : ""; |
| } |
| if (label.empty()) { |
| // ibus-pinyin has a property whose label and tooltip are empty. Fall back |
| // to the key. |
| label = Or(ibus_prop->key, ""); |
| } |
| |
| out_prop_list->push_back(chromeos::ImeProperty(ibus_prop->key, |
| label, |
| is_selection_item, |
| is_selection_item_checked, |
| selection_item_id)); |
| return true; |
| } |
| |
| // Converts |ibus_prop| to |out_prop_list|. Please note that |ibus_prop| |
| // may or may not have children. See the comment for FlattenPropertyList |
| // for details. Returns true if no error is found. |
| bool FlattenProperty( |
| IBusProperty* ibus_prop, chromeos::ImePropertyList* out_prop_list) { |
| // TODO(yusukes): Find a good way to write unit tests for this function. |
| DCHECK(ibus_prop); |
| DCHECK(out_prop_list); |
| |
| int selection_item_id = -1; |
| std::stack<std::pair<IBusProperty*, int> > prop_stack; |
| prop_stack.push(std::make_pair(ibus_prop, selection_item_id)); |
| |
| while (!prop_stack.empty()) { |
| IBusProperty* prop = prop_stack.top().first; |
| const int current_selection_item_id = prop_stack.top().second; |
| prop_stack.pop(); |
| |
| // Filter out unnecessary properties. |
| if (PropertyKeyIsBlacklisted(prop->key)) { |
| continue; |
| } |
| |
| // Convert |prop| to ImeProperty and push it to |out_prop_list|. |
| if (!ConvertProperty(prop, current_selection_item_id, out_prop_list)) { |
| return false; |
| } |
| |
| // Process childrens iteratively (if any). Push all sub properties to the |
| // stack. |
| if (PropertyHasChildren(prop)) { |
| ++selection_item_id; |
| for (int i = 0;; ++i) { |
| IBusProperty* sub_prop = ibus_prop_list_get(prop->sub_props, i); |
| if (!sub_prop) { |
| break; |
| } |
| prop_stack.push(std::make_pair(sub_prop, selection_item_id)); |
| } |
| ++selection_item_id; |
| } |
| } |
| std::reverse(out_prop_list->begin(), out_prop_list->end()); |
| |
| return true; |
| } |
| |
| // Converts IBus representation of a property list, |ibus_prop_list| to our |
| // own. This function also flatten the original list (actually it's a tree). |
| // Returns true if no error is found. The conversion to our own type is |
| // necessary since our language switcher in Chrome tree don't (or can't) know |
| // IBus types. Here is an example: |
| // |
| // ====================================================================== |
| // Input: |
| // |
| // --- Item-1 |
| // |- Item-2 |
| // |- SubMenuRoot --- Item-3-1 |
| // | |- Item-3-2 |
| // | |- Item-3-3 |
| // |- Item-4 |
| // |
| // (Note: Item-3-X is a selection item since they're on a sub menu.) |
| // |
| // Output: |
| // |
| // Item-1, Item-2, Item-3-1, Item-3-2, Item-3-3, Item-4 |
| // (Note: SubMenuRoot does not appear in the output.) |
| // ====================================================================== |
| bool FlattenPropertyList( |
| IBusPropList* ibus_prop_list, chromeos::ImePropertyList* out_prop_list) { |
| // TODO(yusukes): Find a good way to write unit tests for this function. |
| DCHECK(ibus_prop_list); |
| DCHECK(out_prop_list); |
| |
| IBusProperty* fake_root_prop = ibus_property_new("Dummy.Key", |
| PROP_TYPE_MENU, |
| NULL, /* label */ |
| "", /* icon */ |
| NULL, /* tooltip */ |
| FALSE, /* sensitive */ |
| FALSE, /* visible */ |
| PROP_STATE_UNCHECKED, |
| ibus_prop_list); |
| g_return_val_if_fail(fake_root_prop, false); |
| // Increase the ref count so it won't get deleted when |fake_root_prop| |
| // is deleted. |
| g_object_ref(ibus_prop_list); |
| const bool result = FlattenProperty(fake_root_prop, out_prop_list); |
| g_object_unref(fake_root_prop); |
| |
| return result; |
| } |
| |
| // Debug print function. |
| const char* PropTypeToString(int prop_type) { |
| switch (static_cast<IBusPropType>(prop_type)) { |
| case PROP_TYPE_NORMAL: |
| return "NORMAL"; |
| case PROP_TYPE_TOGGLE: |
| return "TOGGLE"; |
| case PROP_TYPE_RADIO: |
| return "RADIO"; |
| case PROP_TYPE_MENU: |
| return "MENU"; |
| case PROP_TYPE_SEPARATOR: |
| return "SEPARATOR"; |
| } |
| return "UNKNOWN"; |
| } |
| |
| // Debug print function. |
| const char* PropStateToString(int prop_state) { |
| switch (static_cast<IBusPropState>(prop_state)) { |
| case PROP_STATE_UNCHECKED: |
| return "UNCHECKED"; |
| case PROP_STATE_CHECKED: |
| return "CHECKED"; |
| case PROP_STATE_INCONSISTENT: |
| return "INCONSISTENT"; |
| } |
| return "UNKNOWN"; |
| } |
| |
| // Debug print function. |
| std::string Spacer(int n) { |
| return std::string(n, ' '); |
| } |
| |
| std::string PrintPropList(IBusPropList *prop_list, int tree_level); |
| // Debug print function. |
| std::string PrintProp(IBusProperty *prop, int tree_level) { |
| if (!prop) { |
| return ""; |
| } |
| |
| std::stringstream stream; |
| stream << Spacer(tree_level) << "=========================" << std::endl; |
| stream << Spacer(tree_level) << "key: " << Or(prop->key, "<none>") |
| << std::endl; |
| stream << Spacer(tree_level) << "icon: " << Or(prop->icon, "<none>") |
| << std::endl; |
| stream << Spacer(tree_level) << "label: " |
| << ((prop->label && prop->label->text) ? prop->label->text : "<none>") |
| << std::endl; |
| stream << Spacer(tree_level) << "tooptip: " |
| << ((prop->tooltip && prop->tooltip->text) |
| ? prop->tooltip->text : "<none>") << std::endl; |
| stream << Spacer(tree_level) << "sensitive: " |
| << (prop->sensitive ? "YES" : "NO") << std::endl; |
| stream << Spacer(tree_level) << "visible: " << (prop->visible ? "YES" : "NO") |
| << std::endl; |
| stream << Spacer(tree_level) << "type: " << PropTypeToString(prop->type) |
| << std::endl; |
| stream << Spacer(tree_level) << "state: " << PropStateToString(prop->state) |
| << std::endl; |
| stream << Spacer(tree_level) << "sub_props: " |
| << (PropertyHasChildren(prop) ? "" : "<none>") << std::endl; |
| stream << PrintPropList(prop->sub_props, tree_level + 1); |
| stream << Spacer(tree_level) << "=========================" << std::endl; |
| |
| return stream.str(); |
| } |
| |
| // Debug print function. |
| std::string PrintPropList(IBusPropList *prop_list, int tree_level) { |
| if (!prop_list) { |
| return ""; |
| } |
| |
| std::stringstream stream; |
| for (int i = 0;; ++i) { |
| IBusProperty* prop = ibus_prop_list_get(prop_list, i); |
| if (!prop) { |
| break; |
| } |
| stream << PrintProp(prop, tree_level); |
| } |
| return stream.str(); |
| } |
| |
| } // namespace |
| |
| namespace chromeos { |
| |
| // A class that holds IBus and DBus connections. |
| class InputMethodStatusConnection { |
| public: |
| InputMethodStatusConnection( |
| void* language_library, |
| LanguageCurrentInputMethodMonitorFunction current_input_method_changed, |
| LanguageRegisterImePropertiesFunction register_ime_properties, |
| LanguageUpdateImePropertyFunction update_ime_property, |
| LanguageFocusChangeMonitorFunction focus_changed) |
| : current_input_method_changed_(current_input_method_changed), |
| register_ime_properties_(register_ime_properties), |
| update_ime_property_(update_ime_property), |
| focus_changed_(focus_changed), |
| language_library_(language_library), |
| ibus_(NULL), |
| dbus_proxy_is_alive_(false), |
| input_context_path_("") { |
| DCHECK(current_input_method_changed), |
| DCHECK(register_ime_properties_); |
| DCHECK(update_ime_property_); |
| DCHECK(focus_changed_); |
| DCHECK(language_library_); |
| } |
| |
| ~InputMethodStatusConnection() { |
| if (dbus_proxy_.get() && dbus_proxy_->gproxy()) { |
| g_signal_handlers_disconnect_by_func( |
| dbus_proxy_->gproxy(), |
| reinterpret_cast<gpointer>(G_CALLBACK(DBusProxyDestroyCallback)), |
| this); |
| } |
| |
| // We don't close |dbus_connection_| since it actually shares the same |
| // socket file descriptor with the connection used in IBusBus. If we |
| // close |dbus_connection_| here, the connection used in the IBus IM |
| // module (im-ibus.so) and |ibus_| will also be closed, that causes |
| // input methods to malfunction in Chrome. |
| // |
| // Note that not closing |dbus_connection_| produces DBus warnings |
| // like below, but it's ok as warnings are not critical (See also |
| // crosbug.com/3596): |
| // |
| // The last reference on a connection was dropped without closing the |
| // connection. |
| |
| if (ibus_) { |
| // Destruct IBus object. |
| g_signal_handlers_disconnect_by_func( |
| ibus_, |
| reinterpret_cast<gpointer>( |
| G_CALLBACK(IBusBusGlobalEngineChangedCallback)), |
| this); |
| // Since the connection which ibus_ has is a "shared connection". We |
| // should not close the connection. Just call g_object_unref. |
| g_object_unref(ibus_); |
| } |
| // |dbus_connection_| and |dbus_proxy_| will be destructed by ~scoped_ptr(). |
| } |
| |
| // Initializes IBus and DBus connections. |
| bool Init() { |
| ibus_init(); |
| |
| // Establish a DBus connection between the candidate_window process for |
| // Chromium OS to handle signals (e.g. "FocusIn") from the process. |
| const char* address = ibus_get_address(); |
| if (!address) { |
| LOG(ERROR) << "ibus_get_address returned NULL"; |
| return false; |
| } |
| dbus_connection_.reset( |
| new dbus::BusConnection(dbus::GetPrivateBusConnection(address))); |
| LOG(INFO) << "Established private D-Bus connection to: '" << address << "'"; |
| |
| // Connect to the candidate_window. Note that dbus_connection_add_filter() |
| // does not work without calling dbus::Proxy(). |
| // TODO(yusukes): Investigate how we can eliminate the call. |
| const bool kConnectToNameOwner = true; |
| dbus_proxy_.reset(new dbus::Proxy(*dbus_connection_, |
| kCandidateWindowService, |
| kCandidateWindowObjectPath, |
| kCandidateWindowInterface, |
| kConnectToNameOwner)); |
| // The operator bool checks if a D-Bus connection is established or not.. |
| if (!(dbus_proxy_->operator bool())) { |
| LOG(ERROR) << "Failed to connect to the candidate_window."; |
| return false; |
| } |
| |
| // Register the callback function for "destroy". |
| dbus_proxy_is_alive_ = true; |
| g_signal_connect(dbus_proxy_->gproxy(), |
| "destroy", |
| G_CALLBACK(DBusProxyDestroyCallback), |
| this); |
| |
| // Establish IBus connection between ibus-daemon to retrieve the list of |
| // available input method engines, change the current input method engine, |
| // and so on. |
| ibus_ = ibus_bus_new(); |
| |
| // Check the IBus connection status. |
| if (!ibus_) { |
| LOG(ERROR) << "ibus_bus_new() failed"; |
| return false; |
| } |
| if (!ibus_bus_is_connected(ibus_)) { |
| DLOG(INFO) << "ibus_bus_is_connected() failed"; |
| return false; |
| } |
| |
| // Register the callback function for "global-engine-changed". |
| g_signal_connect(ibus_, |
| "global-engine-changed", |
| G_CALLBACK(IBusBusGlobalEngineChangedCallback), |
| this); |
| |
| // Register DBus signal handler. |
| dbus_connection_add_filter( |
| dbus_g_connection_get_connection(dbus_connection_->g_connection()), |
| &InputMethodStatusConnection::DispatchSignalFromCandidateWindow, |
| this, NULL); |
| |
| // TODO(yusukes): Investigate what happens if candidate_window process is |
| // restarted. I'm not sure but we should use dbus_g_proxy_new_for_name(), |
| // not dbus_g_proxy_new_for_name_owner()? |
| return true; |
| } |
| |
| // GetInputMethodMode is used for GetInputMethods(). |
| enum GetInputMethodMode { |
| kActiveInputMethods, // Get active input methods. |
| kSupportedInputMethods, // Get supported input methods. |
| }; |
| |
| // Returns a list of input methods that are currently active or supported |
| // depending on |mode|. Returns NULL on error. |
| InputMethodDescriptors* GetInputMethods(GetInputMethodMode mode) { |
| GList* engines = NULL; |
| if (mode == kActiveInputMethods) { |
| DLOG(INFO) << "GetInputMethods (kActiveInputMethods)"; |
| engines = ibus_bus_list_active_engines(ibus_); |
| } else if (mode == kSupportedInputMethods) { |
| DLOG(INFO) << "GetInputMethods (kSupportedInputMethods)"; |
| engines = ibus_bus_list_engines(ibus_); |
| } else { |
| NOTREACHED(); |
| return NULL; |
| } |
| // Note that it's not an error for |engines| to be NULL. |
| // NULL simply means an empty GList. |
| |
| InputMethodDescriptors* input_methods = new InputMethodDescriptors; |
| AddInputMethodNames(engines, input_methods); |
| |
| FreeInputMethodNames(engines); |
| return input_methods; |
| } |
| |
| // Called by cros API ChromeOS(Activate|Deactive)ImeProperty(). |
| void SetImePropertyActivated(const char* key, bool activated) { |
| if (input_context_path_.empty()) { |
| LOG(ERROR) << "Input context is unknown"; |
| return; |
| } |
| |
| IBusInputContext* context = GetInputContext(input_context_path_, ibus_); |
| if (!context) { |
| return; |
| } |
| ibus_input_context_property_activate( |
| context, key, (activated ? PROP_STATE_CHECKED : PROP_STATE_UNCHECKED)); |
| g_object_unref(context); |
| |
| UpdateUI(); |
| } |
| |
| // Called by cros API ChromeOSChangeInputMethod(). |
| bool ChangeInputMethod(const char* name) { |
| if (input_context_path_.empty()) { |
| LOG(ERROR) << "Input context is unknown"; |
| return false; |
| } |
| |
| IBusInputContext* context = GetInputContext(input_context_path_, ibus_); |
| if (!context) { |
| LOG(ERROR) << "Input context is unknown"; |
| return false; |
| } |
| |
| // Clear all input method properties unconditionally. |
| // |
| // When switching to another input method and no text area is focused, |
| // RegisterProperties signal for the new input method will NOT be sent |
| // until a text area is focused. Therefore, we have to clear the old input |
| // method properties here to keep the input method switcher status |
| // consistent. |
| RegisterProperties(NULL); |
| |
| ibus_input_context_set_engine(context, name); |
| g_object_unref(context); |
| UpdateUI(); |
| return true; |
| } |
| |
| // Get a configuration of ibus-daemon or IBus engines and stores it on |
| // |gvalue|. Returns true if |gvalue| is successfully updated. |
| // |
| // For more information, please read a comment for GetImeConfig() function |
| // in chromeos_language.h. |
| bool GetImeConfig(const char* section, |
| const char* config_name, |
| GValue* gvalue) { |
| DCHECK(section); |
| DCHECK(config_name); |
| |
| IBusConfig* ibus_config = CreateConfigObject(); |
| g_return_val_if_fail(ibus_config, false); |
| |
| // TODO(yusukes): make sure the get_value() function does not abort even |
| // when the ibus-memconf process does not exist. |
| const gboolean success = ibus_config_get_value(ibus_config, |
| section, |
| config_name, |
| gvalue); |
| g_object_unref(ibus_config); |
| return (success == TRUE); |
| } |
| |
| // Updates a configuration of ibus-daemon or IBus engines with |gvalue|. |
| // Returns true if the configuration is successfully updated. |
| // |
| // For more information, please read a comment for SetImeConfig() function |
| // in chromeos_language.h. |
| bool SetImeConfig(const char* section, |
| const char* config_name, |
| const GValue* gvalue) { |
| DCHECK(section); |
| DCHECK(config_name); |
| |
| IBusConfig* ibus_config = CreateConfigObject(); |
| g_return_val_if_fail(ibus_config, false); |
| const gboolean success = ibus_config_set_value(ibus_config, |
| section, |
| config_name, |
| gvalue); |
| g_object_unref(ibus_config); |
| |
| DLOG(INFO) << "SetImeConfig: " << section << "/" << config_name |
| << ": result=" << success; |
| return (success == TRUE); |
| } |
| |
| // Checks if IBus connection is alive. |
| bool ConnectionIsAlive() { |
| // Note: Since the IBus connection automatically recovers even if |
| // ibus-daemon reboots, ibus_bus_is_connected() usually returns true. |
| return dbus_proxy_is_alive_ && ibus_ && ibus_bus_is_connected(ibus_); |
| } |
| |
| private: |
| // Creates IBusConfig object. Caller have to call g_object_unref() for the |
| // returned object. |
| IBusConfig* CreateConfigObject() { |
| // Get the IBus connection which is owned by ibus_. ibus_bus_get_connection |
| // nor ibus_config_new don't ref() the |ibus_connection| object. This means |
| // that the |ibus_connection| object could be destructed, regardless of |
| // whether an IBusConfig object exists, when ibus-daemon reboots. For this |
| // reason, it seems to be safer to create IBusConfig object every time |
| // using a fresh pointer which is returned from ibus_bus_get_connection(), |
| // rather than holding it as a member varibale of this class. |
| IBusConnection* ibus_connection = ibus_bus_get_connection(ibus_); |
| if (!ibus_connection) { |
| LOG(ERROR) << "ibus_bus_get_connection() failed"; |
| return NULL; |
| } |
| |
| IBusConfig* ibus_config = ibus_config_new(ibus_connection); |
| if (!ibus_config) { |
| LOG(ERROR) << "ibus_config_new() failed"; |
| return NULL; |
| } |
| |
| return ibus_config; |
| } |
| |
| // Handles "FocusIn" signal from the candidate_window process. |
| void FocusIn(const char* input_context_path) { |
| DCHECK(input_context_path) << "NULL context passed"; |
| if (!input_context_path) { |
| LOG(ERROR) << "NULL context passed"; |
| } |
| DLOG(INFO) << "FocusIn: " << input_context_path; |
| |
| // Remember the current ic path. |
| input_context_path_ = Or(input_context_path, ""); |
| |
| // Force to enable IBus. IBus should always be enabled since "current input |
| // method is xkb:us::eng (for example) and it is enabled" and "current one |
| // is xkb:us::eng but it's disabled" are almost indistinguishable for users. |
| // Plus, if IBus is disabled, the "Next Engine" hot-key, which could be used |
| // to switch e.g. English (xkb:us::eng) <-> Chinese (pinyin), does not work. |
| // Note: When we click a password field in a web page, the FocusIn signal |
| // is not sent. So we can type passwords without using IBus. |
| // TODO(yusukes): This is kind of a hack and should be removed in the near |
| // future. Ask suzhe if we can move this kind of feature to ibus-daemon. |
| if (!input_context_path_.empty()) { |
| IBusInputContext* context = GetInputContext(input_context_path_, ibus_); |
| ibus_input_context_enable(context); |
| g_object_unref(context); |
| } |
| if (focus_changed_) { |
| focus_changed_(language_library_, true); |
| } |
| UpdateUI(); // This is necessary since input method status is held per ic. |
| } |
| |
| // Handles "FocusOut" signal from the candidate_window process. |
| void FocusOut(const char* input_context_path) { |
| DCHECK(input_context_path) << "NULL context passed"; |
| DLOG(INFO) << "FocusOut: " << input_context_path; |
| if (focus_changed_) { |
| focus_changed_(language_library_, false); |
| } |
| } |
| |
| // Handles "StateChanged" signal from the candidate_window process. |
| void StateChanged() { |
| DLOG(INFO) << "StateChanged"; |
| UpdateUI(); |
| } |
| |
| // Handles "RegisterProperties" signal from the candidate_window process. |
| void RegisterProperties(IBusPropList* ibus_prop_list) { |
| DLOG(INFO) << "RegisterProperties" << (ibus_prop_list ? "" : " (clear)"); |
| |
| ImePropertyList prop_list; // our representation. |
| if (ibus_prop_list) { |
| // You can call |
| // LOG(INFO) << "\n" << PrintPropList(ibus_prop_list, 0); |
| // here to dump |ibus_prop_list|. |
| if (!FlattenPropertyList(ibus_prop_list, &prop_list)) { |
| // Clear properties on errors. |
| RegisterProperties(NULL); |
| return; |
| } |
| } |
| // Notify the change. |
| register_ime_properties_(language_library_, prop_list); |
| } |
| |
| // Handles "UpdateProperty" signal from the candidate_window process. |
| void UpdateProperty(IBusProperty* ibus_prop) { |
| DLOG(INFO) << "UpdateProperty"; |
| DCHECK(ibus_prop); |
| |
| // You can call |
| // LOG(INFO) << "\n" << PrintProp(ibus_prop, 0); |
| // here to dump |ibus_prop|. |
| |
| ImePropertyList prop_list; // our representation. |
| if (!FlattenProperty(ibus_prop, &prop_list)) { |
| // Don't update the UI on errors. |
| LOG(ERROR) << "Malformed properties are detected"; |
| return; |
| } |
| // Notify the change. |
| if (!prop_list.empty()) { |
| update_ime_property_(language_library_, prop_list); |
| } |
| } |
| |
| // Retrieve input method status and notify them to the UI. |
| void UpdateUI() { |
| IBusEngineDesc* engine_desc = ibus_bus_get_global_engine(ibus_); |
| if (!engine_desc) { |
| LOG(ERROR) << "Global engine is not set"; |
| return; |
| } |
| |
| InputMethodDescriptor current_input_method(engine_desc->name, |
| engine_desc->longname, |
| engine_desc->layout, |
| engine_desc->language); |
| DLOG(INFO) << "Updating the UI. ID:" << current_input_method.id |
| << ", display_name:" << current_input_method.display_name |
| << ", keyboard_layout:" << current_input_method.keyboard_layout; |
| |
| // Notify the change to update UI. |
| current_input_method_changed_(language_library_, current_input_method); |
| g_object_unref(engine_desc); |
| } |
| |
| static void DBusProxyDestroyCallback(DBusGProxy* proxy, gpointer user_data) { |
| InputMethodStatusConnection* self |
| = static_cast<InputMethodStatusConnection*>(user_data); |
| if (self) { |
| self->dbus_proxy_is_alive_ = false; |
| } |
| LOG(ERROR) << "D-Bus connection to candidate_window is terminated!"; |
| } |
| |
| static void IBusBusGlobalEngineChangedCallback( |
| IBusBus* bus, gpointer user_data) { |
| InputMethodStatusConnection* self |
| = static_cast<InputMethodStatusConnection*>(user_data); |
| if (self) { |
| self->UpdateUI(); |
| } |
| DLOG(INFO) << "Global engine is changed"; |
| } |
| |
| // Dispatches signals from candidate_window. In this function, we use the |
| // IBus's DBus binding (rather than the dbus-glib and its C++ wrapper). |
| // This is because arguments of "RegisterProperties" and "UpdateProperty" |
| // are fairly complex IBus types, and thus it's probably not a good idea |
| // to write a deserializer for these types from scratch using dbus-glib. |
| static DBusHandlerResult DispatchSignalFromCandidateWindow( |
| DBusConnection* connection, DBusMessage* message, void* object) { |
| DCHECK(message); |
| DCHECK(object); |
| |
| InputMethodStatusConnection* self |
| = static_cast<InputMethodStatusConnection*>(object); |
| IBusError* error = NULL; |
| |
| if (ibus_message_is_signal(message, |
| kCandidateWindowInterface, |
| "FocusIn")) { |
| gchar* input_context_path = NULL; |
| const gboolean retval = ibus_message_get_args(message, &error, |
| G_TYPE_STRING, |
| &input_context_path, |
| G_TYPE_INVALID); |
| if (!retval) { |
| LOG(ERROR) << "FocusIn"; |
| return DBUS_HANDLER_RESULT_NOT_YET_HANDLED; |
| } |
| self->FocusIn(input_context_path); |
| return DBUS_HANDLER_RESULT_HANDLED; |
| } |
| |
| if (ibus_message_is_signal(message, |
| kCandidateWindowInterface, |
| "FocusOut")) { |
| gchar* input_context_path = NULL; |
| const gboolean retval = ibus_message_get_args(message, &error, |
| G_TYPE_STRING, |
| &input_context_path, |
| G_TYPE_INVALID); |
| if (!retval) { |
| LOG(ERROR) << "FocusOut"; |
| return DBUS_HANDLER_RESULT_NOT_YET_HANDLED; |
| } |
| self->FocusOut(input_context_path); |
| return DBUS_HANDLER_RESULT_HANDLED; |
| } |
| |
| if (ibus_message_is_signal(message, |
| kCandidateWindowInterface, |
| "StateChanged")) { |
| const gboolean retval = ibus_message_get_args(message, &error, |
| /* no arguments */ |
| G_TYPE_INVALID); |
| if (!retval) { |
| LOG(ERROR) << "StateChanged"; |
| return DBUS_HANDLER_RESULT_NOT_YET_HANDLED; |
| } |
| self->StateChanged(); |
| return DBUS_HANDLER_RESULT_HANDLED; |
| } |
| |
| if (ibus_message_is_signal(message, |
| kCandidateWindowInterface, |
| "RegisterProperties")) { |
| IBusPropList* prop_list = NULL; |
| |
| // The ibus_message_get_args() function automagically deserializes the |
| // complex IBus structure. |
| const gboolean retval = ibus_message_get_args(message, &error, |
| IBUS_TYPE_PROP_LIST, |
| &prop_list, |
| G_TYPE_INVALID); |
| |
| if (!retval) { |
| LOG(ERROR) << "RegisterProperties"; |
| return DBUS_HANDLER_RESULT_NOT_YET_HANDLED; |
| } |
| self->RegisterProperties(prop_list); |
| g_object_unref(prop_list); |
| return DBUS_HANDLER_RESULT_HANDLED; |
| } |
| |
| if (ibus_message_is_signal(message, |
| kCandidateWindowInterface, |
| "UpdateProperty")) { |
| IBusProperty* prop = NULL; |
| const gboolean retval = ibus_message_get_args(message, &error, |
| IBUS_TYPE_PROPERTY, |
| &prop, |
| G_TYPE_INVALID); |
| if (!retval) { |
| LOG(ERROR) << "UpdateProperty"; |
| return DBUS_HANDLER_RESULT_NOT_YET_HANDLED; |
| } |
| self->UpdateProperty(prop); |
| g_object_unref(prop); |
| return DBUS_HANDLER_RESULT_HANDLED; |
| } |
| |
| return DBUS_HANDLER_RESULT_NOT_YET_HANDLED; |
| } |
| |
| // A function pointers which point LanguageLibrary::XXXHandler functions. |
| // The functions are used when libcros receives signals from the |
| // candidate_window. |
| LanguageCurrentInputMethodMonitorFunction current_input_method_changed_; |
| LanguageRegisterImePropertiesFunction register_ime_properties_; |
| LanguageUpdateImePropertyFunction update_ime_property_; |
| LanguageFocusChangeMonitorFunction focus_changed_; |
| |
| // Points to a chromeos::LanguageLibrary object. |language_library_| is used |
| // as the first argument of the monitor functions above. |
| void* language_library_; |
| |
| // Connections to IBus and DBus. |
| IBusBus* ibus_; |
| scoped_ptr<dbus::BusConnection> dbus_connection_; |
| scoped_ptr<dbus::Proxy> dbus_proxy_; |
| bool dbus_proxy_is_alive_; |
| |
| // Current input context path. |
| std::string input_context_path_; |
| }; |
| |
| // |
| // cros APIs |
| // |
| |
| // The function will be bound to chromeos::MonitorInputMethodStatus with dlsym() |
| // in load.cc so it needs to be in the C linkage, so the symbol name does not |
| // get mangled. |
| extern "C" |
| InputMethodStatusConnection* ChromeOSMonitorInputMethodStatus( |
| void* language_library, |
| LanguageCurrentInputMethodMonitorFunction current_input_method_changed, |
| LanguageRegisterImePropertiesFunction register_ime_properties, |
| LanguageUpdateImePropertyFunction update_ime_property, |
| LanguageFocusChangeMonitorFunction focus_changed) { |
| DLOG(INFO) << "MonitorInputMethodStatus"; |
| InputMethodStatusConnection* connection = new InputMethodStatusConnection( |
| language_library, |
| current_input_method_changed, |
| register_ime_properties, |
| update_ime_property, |
| focus_changed); |
| if (!connection->Init()) { |
| DLOG(INFO) << "Failed to Init() InputMethodStatusConnection. " |
| << "Returning NULL"; |
| delete connection; |
| connection = NULL; |
| } |
| return connection; |
| } |
| |
| extern "C" |
| void ChromeOSDisconnectInputMethodStatus( |
| InputMethodStatusConnection* connection) { |
| LOG(INFO) << "DisconnectInputMethodStatus"; |
| delete connection; |
| } |
| |
| extern "C" |
| InputMethodDescriptors* ChromeOSGetActiveInputMethods( |
| InputMethodStatusConnection* connection) { |
| g_return_val_if_fail(connection, NULL); |
| // Pass ownership to a caller. Note: GetInputMethods() might return NULL. |
| return connection->GetInputMethods( |
| InputMethodStatusConnection::kActiveInputMethods); |
| } |
| |
| extern "C" |
| InputMethodDescriptors* ChromeOSGetSupportedInputMethods( |
| InputMethodStatusConnection* connection) { |
| g_return_val_if_fail(connection, NULL); |
| // Pass ownership to a caller. Note: GetInputMethods() might return NULL. |
| return connection->GetInputMethods( |
| InputMethodStatusConnection::kSupportedInputMethods); |
| } |
| |
| extern "C" |
| void ChromeOSSetImePropertyActivated( |
| InputMethodStatusConnection* connection, const char* key, bool activated) { |
| DLOG(INFO) << "SetImePropertyeActivated: " << key << ": " << activated; |
| DCHECK(key); |
| g_return_if_fail(connection); |
| connection->SetImePropertyActivated(key, activated); |
| } |
| |
| extern "C" |
| bool ChromeOSChangeInputMethod( |
| InputMethodStatusConnection* connection, const char* name) { |
| DCHECK(name); |
| DLOG(INFO) << "ChangeInputMethod: " << name; |
| g_return_val_if_fail(connection, false); |
| return connection->ChangeInputMethod(name); |
| } |
| |
| extern "C" |
| bool ChromeOSGetImeConfig(InputMethodStatusConnection* connection, |
| const char* section, |
| const char* config_name, |
| ImeConfigValue* out_value) { |
| DCHECK(out_value); |
| g_return_val_if_fail(connection, FALSE); |
| |
| GValue gvalue = { 0 }; // g_value_init() is not necessary. |
| if (!connection->GetImeConfig(section, config_name, &gvalue)) { |
| if (G_IS_VALUE(&gvalue)) { |
| g_value_unset(&gvalue); |
| } |
| return false; |
| } |
| |
| // Convert the type of the result from GValue to our structure. |
| bool success = true; |
| GType type = G_VALUE_TYPE(&gvalue); |
| if (type == G_TYPE_STRING) { |
| const char* value = g_value_get_string(&gvalue); |
| DCHECK(value); |
| out_value->type = ImeConfigValue::kValueTypeString; |
| out_value->string_value = value; |
| } else if (type == G_TYPE_INT) { |
| out_value->type = ImeConfigValue::kValueTypeInt; |
| out_value->int_value = g_value_get_int(&gvalue); |
| } else if (type == G_TYPE_BOOLEAN) { |
| out_value->type = ImeConfigValue::kValueTypeBool; |
| out_value->bool_value = g_value_get_boolean(&gvalue); |
| } else if (type == G_TYPE_VALUE_ARRAY) { |
| out_value->type = ImeConfigValue::kValueTypeStringList; |
| out_value->string_list_value.clear(); |
| GValueArray* array |
| = reinterpret_cast<GValueArray*>(g_value_get_boxed(&gvalue)); |
| for (guint i = 0; array && (i < array->n_values); ++i) { |
| const GType element_type = G_VALUE_TYPE(&(array->values[i])); |
| if (element_type != G_TYPE_STRING) { |
| LOG(ERROR) << "Array element type is not STRING: " << element_type; |
| g_value_unset(&gvalue); |
| return false; |
| } |
| const char* value = g_value_get_string(&(array->values[i])); |
| DCHECK(value); |
| out_value->string_list_value.push_back(value); |
| } |
| } else { |
| LOG(ERROR) << "Unsupported config type: " << G_VALUE_TYPE(&gvalue); |
| success = false; |
| } |
| |
| g_value_unset(&gvalue); |
| return success; |
| } |
| |
| extern "C" |
| bool ChromeOSSetImeConfig(InputMethodStatusConnection* connection, |
| const char* section, |
| const char* config_name, |
| const ImeConfigValue& value) { |
| g_return_val_if_fail(connection, FALSE); |
| |
| // Convert the type of |value| from our structure to GValue. |
| GValue gvalue = { 0 }; |
| switch (value.type) { |
| case ImeConfigValue::kValueTypeString: |
| g_value_init(&gvalue, G_TYPE_STRING); |
| g_value_set_string(&gvalue, value.string_value.c_str()); |
| break; |
| case ImeConfigValue::kValueTypeInt: |
| g_value_init(&gvalue, G_TYPE_INT); |
| g_value_set_int(&gvalue, value.int_value); |
| break; |
| case ImeConfigValue::kValueTypeBool: |
| g_value_init(&gvalue, G_TYPE_BOOLEAN); |
| g_value_set_boolean(&gvalue, value.bool_value); |
| break; |
| case ImeConfigValue::kValueTypeStringList: |
| g_value_init(&gvalue, G_TYPE_VALUE_ARRAY); |
| const size_t size = value.string_list_value.size(); |
| GValueArray* array = g_value_array_new(size); |
| for (size_t i = 0; i < size; ++i) { |
| GValue array_element = {0}; |
| g_value_init(&array_element, G_TYPE_STRING); |
| g_value_set_string(&array_element, value.string_list_value[i].c_str()); |
| g_value_array_append(array, &array_element); |
| } |
| g_value_take_boxed(&gvalue, array); |
| break; |
| } |
| |
| const bool success = connection->SetImeConfig(section, config_name, &gvalue); |
| g_value_unset(&gvalue); |
| |
| return success; |
| } |
| |
| extern "C" |
| bool ChromeOSInputMethodStatusConnectionIsAlive( |
| InputMethodStatusConnection* connection) { |
| g_return_val_if_fail(connection, false); |
| const bool is_connected = connection->ConnectionIsAlive(); |
| if (!is_connected) { |
| LOG(WARNING) << "ChromeOSInputMethodStatusConnectionIsAlive: NOT alive"; |
| } |
| return is_connected; |
| } |
| |
| } // namespace chromeos |