| // 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 "views/ime/input_method_gtk.h" |
| |
| #include <gdk/gdk.h> |
| #include <gdk/gdkkeysyms.h> |
| |
| #include <algorithm> |
| |
| #include "base/basictypes.h" |
| #include "base/logging.h" |
| #include "base/string_util.h" |
| #include "base/third_party/icu/icu_utf.h" |
| #include "base/utf_string_conversions.h" |
| #include "ui/base/gtk/event_synthesis_gtk.h" |
| #include "ui/base/gtk/gtk_im_context_util.h" |
| #include "ui/base/keycodes/keyboard_codes.h" |
| #include "ui/base/keycodes/keyboard_code_conversion_gtk.h" |
| #include "views/events/event.h" |
| #include "views/widget/widget.h" |
| |
| namespace views { |
| |
| InputMethodGtk::InputMethodGtk(internal::InputMethodDelegate* delegate) |
| : context_(NULL), |
| context_simple_(NULL), |
| widget_realize_id_(0), |
| widget_unrealize_id_(0), |
| context_focused_(false), |
| handling_key_event_(false), |
| composing_text_(false), |
| composition_changed_(false), |
| suppress_next_result_(false) { |
| set_delegate(delegate); |
| } |
| |
| InputMethodGtk::~InputMethodGtk() { |
| if (widget()) { |
| GtkWidget* native_view = widget()->GetNativeView(); |
| if (native_view) { |
| g_signal_handler_disconnect(native_view, widget_realize_id_); |
| g_signal_handler_disconnect(native_view, widget_unrealize_id_); |
| } |
| } |
| if (context_) { |
| g_object_unref(context_); |
| context_ = NULL; |
| } |
| if (context_simple_) { |
| g_object_unref(context_simple_); |
| context_simple_ = NULL; |
| } |
| } |
| |
| void InputMethodGtk::Init(Widget* widget) { |
| DCHECK(GTK_IS_WIDGET(widget->GetNativeView())); |
| |
| widget_realize_id_ = |
| g_signal_connect(widget->GetNativeView(), "realize", |
| G_CALLBACK(OnWidgetRealizeThunk), this); |
| widget_unrealize_id_ = |
| g_signal_connect(widget->GetNativeView(), "unrealize", |
| G_CALLBACK(OnWidgetUnrealizeThunk), this); |
| |
| context_ = gtk_im_multicontext_new(); |
| context_simple_ = gtk_im_context_simple_new(); |
| |
| // context_ and context_simple_ share the same callback handlers. |
| // All data come from them are treated equally. |
| // context_ is for full input method support. |
| // context_simple_ is for supporting dead/compose keys when input method is |
| // disabled, eg. in password input box. |
| g_signal_connect(context_, "commit", |
| G_CALLBACK(OnCommitThunk), this); |
| g_signal_connect(context_, "preedit_start", |
| G_CALLBACK(OnPreeditStartThunk), this); |
| g_signal_connect(context_, "preedit_end", |
| G_CALLBACK(OnPreeditEndThunk), this); |
| g_signal_connect(context_, "preedit_changed", |
| G_CALLBACK(OnPreeditChangedThunk), this); |
| |
| g_signal_connect(context_simple_, "commit", |
| G_CALLBACK(OnCommitThunk), this); |
| g_signal_connect(context_simple_, "preedit_start", |
| G_CALLBACK(OnPreeditStartThunk), this); |
| g_signal_connect(context_simple_, "preedit_end", |
| G_CALLBACK(OnPreeditEndThunk), this); |
| g_signal_connect(context_simple_, "preedit_changed", |
| G_CALLBACK(OnPreeditChangedThunk), this); |
| |
| // Set client window if the widget is already realized. |
| OnWidgetRealize(widget->GetNativeView()); |
| |
| InputMethodBase::Init(widget); |
| } |
| |
| void InputMethodGtk::OnFocus() { |
| DCHECK(!widget_focused()); |
| InputMethodBase::OnFocus(); |
| UpdateContextFocusState(); |
| } |
| |
| void InputMethodGtk::OnBlur() { |
| DCHECK(widget_focused()); |
| ConfirmCompositionText(); |
| InputMethodBase::OnBlur(); |
| UpdateContextFocusState(); |
| } |
| |
| void InputMethodGtk::DispatchKeyEvent(const KeyEvent& key) { |
| DCHECK(key.type() == ui::ET_KEY_PRESSED || key.type() == ui::ET_KEY_RELEASED); |
| suppress_next_result_ = false; |
| |
| // We should bypass |context_| and |context_simple_| only if there is no |
| // text input client focused. Otherwise, always send the key event to either |
| // |context_| or |context_simple_| even if the text input type is |
| // ui::TEXT_INPUT_TYPE_NONE, to make sure we can get correct character result. |
| if (!GetTextInputClient()) { |
| DispatchKeyEventPostIME(key); |
| return; |
| } |
| |
| handling_key_event_ = true; |
| composition_changed_ = false; |
| result_text_.clear(); |
| |
| // If it's a fake key event, then we need to synthesize a GdkEventKey. |
| GdkEvent* event = key.gdk_event() ? key.gdk_event() : |
| SynthesizeGdkEventKey(key); |
| gboolean filtered = gtk_im_context_filter_keypress( |
| context_focused_ ? context_ : context_simple_, &event->key); |
| |
| handling_key_event_ = false; |
| |
| const View* old_focused_view = focused_view(); |
| if (key.type() == ui::ET_KEY_PRESSED && filtered) |
| ProcessFilteredKeyPressEvent(key); |
| |
| // Ensure no focus change from processing the key event. |
| if (old_focused_view == focused_view()) { |
| if (HasInputMethodResult()) |
| ProcessInputMethodResult(key, filtered); |
| // Ensure no focus change sending input method results to the focused View. |
| if (old_focused_view == focused_view()) { |
| if (key.type() == ui::ET_KEY_PRESSED && !filtered) |
| ProcessUnfilteredKeyPressEvent(key); |
| else if (key.type() == ui::ET_KEY_RELEASED) |
| DispatchKeyEventPostIME(key); |
| } |
| } |
| |
| // Free the synthesized event if there was no underlying native event. |
| if (event != key.gdk_event()) |
| gdk_event_free(event); |
| } |
| |
| void InputMethodGtk::OnTextInputTypeChanged(View* view) { |
| if (IsViewFocused(view)) { |
| DCHECK(!composing_text_); |
| UpdateContextFocusState(); |
| } |
| InputMethodBase::OnTextInputTypeChanged(view); |
| } |
| |
| void InputMethodGtk::OnCaretBoundsChanged(View* view) { |
| gfx::Rect rect; |
| if (!IsViewFocused(view) || !GetCaretBoundsInWidget(&rect)) |
| return; |
| |
| GdkRectangle gdk_rect = rect.ToGdkRectangle(); |
| gtk_im_context_set_cursor_location(context_, &gdk_rect); |
| } |
| |
| void InputMethodGtk::CancelComposition(View* view) { |
| if (IsViewFocused(view)) |
| ResetContext(); |
| } |
| |
| std::string InputMethodGtk::GetInputLocale() { |
| // Not supported. |
| return std::string(""); |
| } |
| |
| base::i18n::TextDirection InputMethodGtk::GetInputTextDirection() { |
| // Not supported. |
| return base::i18n::UNKNOWN_DIRECTION; |
| } |
| |
| bool InputMethodGtk::IsActive() { |
| // We always need to send keyboard events to either |context_| or |
| // |context_simple_|, so just return true here. |
| return true; |
| } |
| |
| void InputMethodGtk::FocusedViewWillChange() { |
| ConfirmCompositionText(); |
| } |
| |
| void InputMethodGtk::FocusedViewDidChange() { |
| UpdateContextFocusState(); |
| |
| // Force to update caret bounds, in case the View thinks that the caret |
| // bounds has not changed. |
| if (context_focused_) |
| OnCaretBoundsChanged(focused_view()); |
| } |
| |
| void InputMethodGtk::ConfirmCompositionText() { |
| TextInputClient* client = GetTextInputClient(); |
| if (client && client->HasCompositionText()) |
| client->ConfirmCompositionText(); |
| |
| ResetContext(); |
| } |
| |
| void InputMethodGtk::ResetContext() { |
| if (!GetTextInputClient()) |
| return; |
| |
| DCHECK(widget_focused()); |
| DCHECK(focused_view()); |
| DCHECK(!handling_key_event_); |
| |
| // To prevent any text from being committed when resetting the |context_|; |
| handling_key_event_ = true; |
| suppress_next_result_ = true; |
| |
| gtk_im_context_reset(context_); |
| gtk_im_context_reset(context_simple_); |
| |
| // Some input methods may not honour the reset call. Focusing out/in the |
| // |context_| to make sure it gets reset correctly. |
| if (context_focused_) { |
| gtk_im_context_focus_out(context_); |
| gtk_im_context_focus_in(context_); |
| } |
| |
| composition_.Clear(); |
| result_text_.clear(); |
| handling_key_event_ = false; |
| composing_text_ = false; |
| composition_changed_ = false; |
| } |
| |
| void InputMethodGtk::UpdateContextFocusState() { |
| bool old_context_focused = context_focused_; |
| // Use switch here in case we are going to add more text input types. |
| switch (GetTextInputType()) { |
| case ui::TEXT_INPUT_TYPE_NONE: |
| case ui::TEXT_INPUT_TYPE_PASSWORD: |
| context_focused_ = false; |
| break; |
| default: |
| context_focused_ = true; |
| break; |
| } |
| |
| // We only focus in |context_| when the focus is in a normal textfield. |
| if (old_context_focused && !context_focused_) |
| gtk_im_context_focus_out(context_); |
| else if (!old_context_focused && context_focused_) |
| gtk_im_context_focus_in(context_); |
| |
| // |context_simple_| can be used in any textfield, including password box, and |
| // even if the focused text input client's text input type is |
| // ui::TEXT_INPUT_TYPE_NONE. |
| if (GetTextInputClient()) |
| gtk_im_context_focus_in(context_simple_); |
| else |
| gtk_im_context_focus_out(context_simple_); |
| } |
| |
| void InputMethodGtk::ProcessFilteredKeyPressEvent(const KeyEvent& key) { |
| if (NeedInsertChar()) { |
| DispatchKeyEventPostIME(key); |
| } else { |
| KeyEvent key(ui::ET_KEY_PRESSED, ui::VKEY_PROCESSKEY, key.flags()); |
| DispatchKeyEventPostIME(key); |
| } |
| } |
| |
| void InputMethodGtk::ProcessUnfilteredKeyPressEvent(const KeyEvent& key) { |
| const View* old_focused_view = focused_view(); |
| DispatchKeyEventPostIME(key); |
| |
| // We shouldn't dispatch the character anymore if the key event caused focus |
| // change. |
| if (old_focused_view != focused_view()) |
| return; |
| |
| // If a key event was not filtered by |context_| or |context_simple_|, then |
| // it means the key event didn't generate any result text. For some cases, |
| // the key event may still generate a valid character, eg. a control-key |
| // event (ctrl-a, return, tab, etc.). We need to send the character to the |
| // focused text input client by calling TextInputClient::InsertChar(). |
| char16 ch = key.GetCharacter(); |
| TextInputClient* client = GetTextInputClient(); |
| if (ch && client) |
| client->InsertChar(ch, key.flags()); |
| } |
| |
| void InputMethodGtk::ProcessInputMethodResult(const KeyEvent& key, |
| bool filtered) { |
| TextInputClient* client = GetTextInputClient(); |
| DCHECK(client); |
| |
| if (result_text_.length()) { |
| if (filtered && NeedInsertChar()) { |
| for (string16::const_iterator i = result_text_.begin(); |
| i != result_text_.end(); ++i) { |
| client->InsertChar(*i, key.flags()); |
| } |
| } else { |
| client->InsertText(result_text_); |
| composing_text_ = false; |
| } |
| } |
| |
| if (composition_changed_ && !IsTextInputTypeNone()) { |
| if (composition_.text.length()) { |
| composing_text_ = true; |
| client->SetCompositionText(composition_); |
| } else if (result_text_.empty()) { |
| client->ClearCompositionText(); |
| } |
| } |
| } |
| |
| bool InputMethodGtk::NeedInsertChar() const { |
| return IsTextInputTypeNone() || |
| (!composing_text_ && result_text_.length() == 1); |
| } |
| |
| bool InputMethodGtk::HasInputMethodResult() const { |
| return result_text_.length() || composition_changed_; |
| } |
| |
| void InputMethodGtk::SendFakeProcessKeyEvent(bool pressed) const { |
| KeyEvent key(pressed ? ui::ET_KEY_PRESSED : ui::ET_KEY_RELEASED, |
| ui::VKEY_PROCESSKEY, 0); |
| DispatchKeyEventPostIME(key); |
| } |
| |
| GdkEvent* InputMethodGtk::SynthesizeGdkEventKey(const KeyEvent& key) const { |
| guint keyval = |
| ui::GdkKeyCodeForWindowsKeyCode(key.key_code(), key.IsShiftDown()); |
| guint state = 0; |
| state |= key.IsShiftDown() ? GDK_SHIFT_MASK : 0; |
| state |= key.IsControlDown() ? GDK_CONTROL_MASK : 0; |
| state |= key.IsAltDown() ? GDK_MOD1_MASK : 0; |
| state |= key.IsCapsLockDown() ? GDK_LOCK_MASK : 0; |
| |
| DCHECK(widget()->GetNativeView()->window); |
| return ui::SynthesizeKeyEvent(widget()->GetNativeView()->window, |
| key.type() == ui::ET_KEY_PRESSED, |
| keyval, state); |
| } |
| |
| void InputMethodGtk::OnCommit(GtkIMContext* context, gchar* text) { |
| if (suppress_next_result_) { |
| suppress_next_result_ = false; |
| return; |
| } |
| |
| // We need to receive input method result even if the text input type is |
| // ui::TEXT_INPUT_TYPE_NONE, to make sure we can always send correct |
| // character for each key event to the focused text input client. |
| if (!GetTextInputClient()) |
| return; |
| |
| string16 utf16_text(UTF8ToUTF16(text)); |
| |
| // Append the text to the buffer, because commit signal might be fired |
| // multiple times when processing a key event. |
| result_text_.append(utf16_text); |
| |
| // If we are not handling key event, do not bother sending text result if the |
| // focused text input client does not support text input. |
| if (!handling_key_event_ && !IsTextInputTypeNone()) { |
| SendFakeProcessKeyEvent(true); |
| GetTextInputClient()->InsertText(utf16_text); |
| SendFakeProcessKeyEvent(false); |
| } |
| } |
| |
| void InputMethodGtk::OnPreeditStart(GtkIMContext* context) { |
| if (suppress_next_result_ || IsTextInputTypeNone()) |
| return; |
| |
| composing_text_ = true; |
| } |
| |
| void InputMethodGtk::OnPreeditChanged(GtkIMContext* context) { |
| if (suppress_next_result_ || IsTextInputTypeNone()) |
| return; |
| |
| gchar* text = NULL; |
| PangoAttrList* attrs = NULL; |
| gint cursor_position = 0; |
| gtk_im_context_get_preedit_string(context, &text, &attrs, &cursor_position); |
| |
| ui::ExtractCompositionTextFromGtkPreedit(text, attrs, cursor_position, |
| &composition_); |
| composition_changed_ = true; |
| |
| g_free(text); |
| pango_attr_list_unref(attrs); |
| |
| if (composition_.text.length()) |
| composing_text_ = true; |
| |
| if (!handling_key_event_ && !IsTextInputTypeNone()) { |
| SendFakeProcessKeyEvent(true); |
| GetTextInputClient()->SetCompositionText(composition_); |
| SendFakeProcessKeyEvent(false); |
| } |
| } |
| |
| void InputMethodGtk::OnPreeditEnd(GtkIMContext* context) { |
| if (composition_.text.empty() || IsTextInputTypeNone()) |
| return; |
| |
| composition_changed_ = true; |
| composition_.Clear(); |
| |
| if (!handling_key_event_) { |
| TextInputClient* client = GetTextInputClient(); |
| if (client && client->HasCompositionText()) |
| client->ClearCompositionText(); |
| } |
| } |
| |
| void InputMethodGtk::OnWidgetRealize(GtkWidget* widget) { |
| // We should only set im context's client window once, because when setting |
| // client window, im context may destroy and recreate its internal states and |
| // objects. |
| if (widget->window) { |
| gtk_im_context_set_client_window(context_, widget->window); |
| gtk_im_context_set_client_window(context_simple_, widget->window); |
| } |
| } |
| |
| void InputMethodGtk::OnWidgetUnrealize(GtkWidget* widget) { |
| gtk_im_context_set_client_window(context_, NULL); |
| gtk_im_context_set_client_window(context_simple_, NULL); |
| } |
| |
| } // namespace views |