Chromium Code Reviews| Index: views/ime/input_method_gtk.cc |
| diff --git a/views/ime/input_method_gtk.cc b/views/ime/input_method_gtk.cc |
| new file mode 100644 |
| index 0000000000000000000000000000000000000000..906cbd09519df2b8f9e8ba74cc2c4f6e0b4e071a |
| --- /dev/null |
| +++ b/views/ime/input_method_gtk.cc |
| @@ -0,0 +1,501 @@ |
| +// 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) |
| + : delegate_(delegate), |
| + widget_(NULL), |
| + focused_view_(NULL), |
| + context_(NULL), |
| + context_simple_(NULL), |
| + widget_realize_id_(0), |
| + widget_unrealize_id_(0), |
| + widget_focus_in_id_(0), |
| + widget_focus_out_id_(0), |
| + widget_focused_(false), |
| + context_focused_(false), |
| + handling_key_event_(false), |
| + composing_text_(false), |
| + composition_changed_(false), |
| + suppress_next_result_(false) { |
| +} |
| + |
| +InputMethodGtk::~InputMethodGtk() { |
| + if (widget_) { |
| + widget_->GetFocusManager()->RemoveFocusChangeListener(this); |
| + 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_); |
| + g_signal_handler_disconnect(native_view, widget_focus_in_id_); |
| + g_signal_handler_disconnect(native_view, widget_focus_out_id_); |
| + } |
| + widget_ = NULL; |
| + } |
| + if (context_) { |
| + g_object_unref(context_); |
| + context_ = NULL; |
| + } |
| + if (context_simple_) { |
| + g_object_unref(context_simple_); |
| + context_simple_ = NULL; |
| + } |
| +} |
| + |
| +void InputMethodGtk::set_delegate(internal::InputMethodDelegate* delegate) { |
| + delegate_ = delegate; |
|
oshima
2011/03/21 20:30:56
can delegate be NULL? DCHECK if not.
James Su
2011/03/21 21:21:16
Done.
|
| +} |
| + |
| +void InputMethodGtk::Init(Widget* widget) { |
| + DCHECK(widget); |
| + DCHECK(GTK_IS_WIDGET(widget->GetNativeView())); |
| + DCHECK(widget->GetFocusManager()); |
| + |
| + if (widget_) { |
| + NOTREACHED() << "The input method is already initialized."; |
| + return; |
| + } |
| + |
| + widget_ = widget; |
| + widget->GetFocusManager()->AddFocusChangeListener(this); |
| + |
| + 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); |
| + widget_focus_in_id_ = |
| + g_signal_connect(widget->GetNativeView(), "focus-in-event", |
| + G_CALLBACK(OnWidgetFocusInThunk), this); |
| + widget_focus_out_id_ = |
| + g_signal_connect(widget->GetNativeView(), "focus-out-event", |
| + G_CALLBACK(OnWidgetFocusOutThunk), 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()); |
| +} |
| + |
| +void InputMethodGtk::DispatchKeyEvent(const KeyEvent& key) { |
| + suppress_next_result_ = false; |
| + |
| + if (!context_focused_) { |
| + DispatchKeyEventPostIME(key); |
| + return; |
| + } |
| + |
| + handling_key_event_ = true; |
| + composition_changed_ = false; |
| + result_text_.clear(); |
| + |
| + GdkEvent* event = key.native_event(); |
| + DCHECK(!event || event->type == GDK_KEY_PRESS || |
| + event->type == GDK_KEY_RELEASE); |
| + |
| + // If it's a fake key event, then we need to synthesis a GdkEventKey. |
| + bool need_free_event = false; |
| + if (!event) { |
| + event = SynthesizeGdkEventKey(key); |
| + need_free_event = true; |
| + } |
| + |
| + gboolean filtered = false; |
| + if (GetTextInputType() == ui::TEXT_INPUT_TYPE_TEXT) |
| + filtered = gtk_im_context_filter_keypress(context_, &event->key); |
| + else |
| + filtered = gtk_im_context_filter_keypress(context_simple_, &event->key); |
| + |
| + handling_key_event_ = false; |
| + |
| + View* old_focused_view = focused_view_; |
| + if (key.type() == ui::ET_KEY_PRESSED && filtered) |
| + ProcessFilteredKeyPressEvent(key); |
| + |
| + if (old_focused_view != focused_view_) |
| + return; |
| + |
| + if (HasInputMethodResult()) |
| + ProcessInputMethodResult(key, filtered); |
| + |
| + if (old_focused_view != focused_view_) |
| + return; |
| + |
| + if (key.type() == ui::ET_KEY_PRESSED && !filtered) |
| + ProcessUnfilteredKeyPressEvent(key); |
| + else if (key.type() == ui::ET_KEY_RELEASED) |
| + DispatchKeyEventPostIME(key); |
| + |
| + if (need_free_event) |
| + gdk_event_free(event); |
| +} |
| + |
| +void InputMethodGtk::OnTextInputTypeChanged(View* view) { |
| + if (IsViewFocused(view)) |
| + UpdateContextFocusState(); |
| +} |
| + |
| +void InputMethodGtk::OnCaretBoundsChanged(View* view) { |
| + if (!IsViewFocused(view)) |
| + return; |
| + |
| + TextInputClient* client = view->GetTextInputClient(); |
| + if (!client) |
| + return; |
| + |
| + gfx::Rect rect = client->GetCaretBounds(); |
| + gfx::Point origin = rect.origin(); |
| + gfx::Point end = gfx::Point(rect.right(), rect.bottom()); |
| + |
| + View::ConvertPointToWidget(view, &origin); |
| + View::ConvertPointToWidget(view, &end); |
| + |
| + GdkRectangle gdk_rect = |
| + { origin.x(), origin.y(), end.x() - origin.x(), end.y() - origin.y() }; |
| + |
| + // We need to translate the coordinates to the toplevel widget if the view is |
| + // inside a child widget. |
| + if (view->GetWidget() != widget_) { |
| + gboolean result = gtk_widget_translate_coordinates( |
| + view->GetWidget()->GetNativeView(), widget_->GetNativeView(), |
| + gdk_rect.x, gdk_rect.y, &gdk_rect.x, &gdk_rect.y); |
| + DCHECK(result); |
| + } |
| + |
| + 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::FocusWillChange(View* focused_before, View* focused) { |
| + DCHECK_EQ(focused_view_, focused_before); |
| + ConfirmComposition(); |
| + focused_view_ = focused; |
| + UpdateContextFocusState(); |
| +} |
| + |
| +void InputMethodGtk::ConfirmComposition() { |
| + TextInputClient* client = GetTextInputClient(); |
| + if (client && client->HasComposition()) |
| + client->ConfirmComposition(); |
| + |
| + ResetContext(); |
| +} |
| + |
| +void InputMethodGtk::ResetContext() { |
| + if (!context_focused_) |
| + return; |
| + |
| + DCHECK(widget_focused_); |
| + DCHECK(focused_view_); |
| + DCHECK(!handling_key_event_); |
| + |
| + // To prevent any text from being committed when resetting the |context_|; |
|
oshima
2011/03/21 20:30:56
Does this mean
filtered = gtk_im_context_filter_
James Su
2011/03/21 21:21:16
yes.
|
| + 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. |
| + gtk_im_context_focus_out(context_); |
| + gtk_im_context_focus_in(context_); |
|
oshima
2011/03/21 20:30:56
don't we have to do this for contet_simple_? And i
James Su
2011/03/21 21:21:16
context_simple_ doesn't have this problem, reset i
|
| + |
| + composition_.Clear(); |
| + result_text_.clear(); |
| + handling_key_event_ = false; |
| + composing_text_ = false; |
| + composition_changed_ = false; |
| +} |
| + |
| +void InputMethodGtk::UpdateContextFocusState() { |
| + bool old_context_focused = context_focused_; |
| + context_focused_ = (GetTextInputClient() != NULL); |
| + |
| + if (old_context_focused && !context_focused_) { |
| + gtk_im_context_focus_out(context_); |
| + gtk_im_context_focus_out(context_simple_); |
| + } else if (!old_context_focused && context_focused_) { |
| + gtk_im_context_focus_in(context_); |
| + gtk_im_context_focus_in(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) { |
| + DispatchKeyEventPostIME(key); |
| + |
| + // 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_ && GetTextInputType() != ui::TEXT_INPUT_TYPE_NONE) { |
| + if (composition_.text.length()) { |
| + composing_text_ = true; |
| + client->SetComposition(composition_); |
| + } else if (result_text_.empty()) { |
| + client->ClearComposition(); |
| + } |
| + } |
| +} |
| + |
| +TextInputClient* InputMethodGtk::GetTextInputClient() const { |
| + return (widget_focused_ && focused_view_) ? |
| + focused_view_->GetTextInputClient() : NULL; |
| +} |
| + |
| +ui::TextInputType InputMethodGtk::GetTextInputType() const { |
| + TextInputClient* client = GetTextInputClient(); |
| + return client ? client->GetTextInputType() : ui::TEXT_INPUT_TYPE_NONE; |
| +} |
| + |
| +bool InputMethodGtk::IsViewFocused(View* view) const { |
| + return widget_focused_ && view && focused_view_ == view; |
| +} |
| + |
| +bool InputMethodGtk::NeedInsertChar() const { |
| + return GetTextInputType() == ui::TEXT_INPUT_TYPE_NONE || |
| + (!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; |
| + if (key.IsShiftDown()) |
| + state |= GDK_SHIFT_MASK; |
| + if (key.IsControlDown()) |
| + state |= GDK_CONTROL_MASK; |
| + if (key.IsAltDown()) |
| + state |= GDK_MOD1_MASK; |
| + if (key.IsCapsLockDown()) |
| + state |= GDK_LOCK_MASK; |
| + |
| + DCHECK(widget_->GetNativeView()->window); |
| + return ui::SynthesizeKeyEvent(widget_->GetNativeView()->window, |
| + key.type() == ui::ET_KEY_PRESSED, |
| + keyval, state); |
| +} |
| + |
| +void InputMethodGtk::DispatchKeyEventPostIME(const KeyEvent& key) const { |
| + if (delegate_) |
| + delegate_->DispatchKeyEventPostIME(key); |
| +} |
| + |
| +void InputMethodGtk::OnCommit(GtkIMContext* context, gchar* text) { |
| + if (suppress_next_result_) { |
| + suppress_next_result_ = false; |
| + return; |
| + } |
| + |
| + if (!context_focused_) |
| + 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 (!handling_key_event_) { |
| + TextInputClient* client = GetTextInputClient(); |
| + if (client && client->GetTextInputType() != ui::TEXT_INPUT_TYPE_NONE) { |
| + SendFakeProcessKeyEvent(true); |
| + client->InsertText(utf16_text); |
| + SendFakeProcessKeyEvent(false); |
| + } |
| + } |
| +} |
| + |
| +void InputMethodGtk::OnPreeditStart(GtkIMContext* context) { |
| + if (suppress_next_result_ || !context_focused_) |
| + return; |
| + |
| + composing_text_ = true; |
| +} |
| + |
| +void InputMethodGtk::OnPreeditChanged(GtkIMContext* context) { |
| + if (suppress_next_result_ || !context_focused_) |
| + return; |
| + |
| + gchar* text = NULL; |
| + PangoAttrList* attrs = NULL; |
| + gint cursor_position = 0; |
| + gtk_im_context_get_preedit_string(context, &text, &attrs, &cursor_position); |
| + |
| + ui::ExtractCompositionInfoFromGtkPreedit(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_) { |
| + TextInputClient* client = GetTextInputClient(); |
| + if (client && client->GetTextInputType() != ui::TEXT_INPUT_TYPE_NONE) { |
| + SendFakeProcessKeyEvent(true); |
| + client->SetComposition(composition_); |
| + SendFakeProcessKeyEvent(false); |
| + } |
| + } |
| +} |
| + |
| +void InputMethodGtk::OnPreeditEnd(GtkIMContext* context) { |
| + if (!context_focused_ || composition_.text.empty()) |
| + return; |
| + |
| + composition_changed_ = true; |
| + composition_.Clear(); |
| + |
| + if (!handling_key_event_) { |
| + TextInputClient* client = GetTextInputClient(); |
| + if (client && client->HasComposition()) |
| + client->ClearComposition(); |
| + } |
| +} |
| + |
| +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); |
| +} |
| + |
| +gboolean InputMethodGtk::OnWidgetFocusIn(GtkWidget* widget, |
| + GdkEventFocus* event) { |
| + if (!widget_focused_) { |
| + widget_focused_ = true; |
| + UpdateContextFocusState(); |
| + } |
| + return false; |
| +} |
| + |
| +gboolean InputMethodGtk::OnWidgetFocusOut(GtkWidget* widget, |
| + GdkEventFocus* event) { |
| + if (widget_focused_) { |
| + ConfirmComposition(); |
| + widget_focused_ = false; |
| + UpdateContextFocusState(); |
| + } |
| + return false; |
| +} |
| + |
| +} // namespace views |