blob: 146cf6a34a1bc01ed39ebc42483e164d15c40d42 [file] [log] [blame]
// Copyright (c) 2011 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "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