x11_input_method_context_impl_gtk2.cc revision 1e9bf3e0803691d0a228da41fc608347b6db4340
1// Copyright 2013 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5#include "chrome/browser/ui/libgtk2ui/x11_input_method_context_impl_gtk2.h"
6
7#include <gdk/gdk.h>
8#include <gdk/gdkkeysyms.h>
9#include <gdk/gdkx.h>
10
11#include <gtk/gtk.h>
12
13#include <X11/X.h>
14#include <X11/Xlib.h>
15
16#include "base/environment.h"
17#include "base/message_loop/message_loop.h"
18#include "base/strings/utf_string_conversions.h"
19#include "ui/base/ime/composition_text.h"
20#include "ui/base/ime/composition_text_util_pango.h"
21#include "ui/base/ime/text_input_client.h"
22
23namespace {
24
25// Constructs a GdkEventKey from a XKeyEvent and returns it.  Otherwise,
26// returns NULL.  The returned GdkEvent must be freed by gdk_event_free.
27GdkEvent* GdkEventFromXKeyEvent(XKeyEvent& xkey, bool is_modifier) {
28  DCHECK(xkey.type == KeyPress || xkey.type == KeyRelease);
29
30  // Get a GdkDisplay.
31  GdkDisplay* display = gdk_x11_lookup_xdisplay(xkey.display);
32  if (!display) {
33    // Fall back to the default display.
34    display = gdk_display_get_default();
35  }
36  if (!display) {
37    LOG(ERROR) << "Cannot get a GdkDisplay for a key event.";
38    return NULL;
39  }
40  // Get a keysym and group.
41  KeySym keysym = NoSymbol;
42  guint8 keyboard_group = 0;
43  XLookupString(&xkey, NULL, 0, &keysym, NULL);
44  GdkKeymap* keymap = gdk_keymap_get_for_display(display);
45  GdkKeymapKey* keys = NULL;
46  guint* keyvals = NULL;
47  gint n_entries = 0;
48  if (keymap &&
49      gdk_keymap_get_entries_for_keycode(keymap, xkey.keycode,
50                                         &keys, &keyvals, &n_entries)) {
51    for (gint i = 0; i < n_entries; ++i) {
52      if (keyvals[i] == keysym) {
53        keyboard_group = keys[i].group;
54        break;
55      }
56    }
57  }
58  g_free(keys);
59  keys = NULL;
60  g_free(keyvals);
61  keyvals = NULL;
62  // Get a GdkWindow.
63  GdkWindow* window = gdk_x11_window_lookup_for_display(display, xkey.window);
64  if (window)
65    g_object_ref(window);
66  else
67    window = gdk_x11_window_foreign_new_for_display(display, xkey.window);
68  if (!window) {
69    LOG(ERROR) << "Cannot get a GdkWindow for a key event.";
70    return NULL;
71  }
72
73  // Create a GdkEvent.
74  GdkEventType event_type = xkey.type == KeyPress ?
75                            GDK_KEY_PRESS : GDK_KEY_RELEASE;
76  GdkEvent* event = gdk_event_new(event_type);
77  event->key.type = event_type;
78  event->key.window = window;
79  // GdkEventKey and XKeyEvent share the same definition for time and state.
80  event->key.send_event = xkey.send_event;
81  event->key.time = xkey.time;
82  event->key.state = xkey.state;
83  event->key.keyval = keysym;
84  event->key.length = 0;
85  event->key.string = NULL;
86  event->key.hardware_keycode = xkey.keycode;
87  event->key.group = keyboard_group;
88  event->key.is_modifier = is_modifier;
89  return event;
90}
91
92}  // namespace
93
94namespace libgtk2ui {
95
96X11InputMethodContextImplGtk2::X11InputMethodContextImplGtk2(
97    ui::LinuxInputMethodContextDelegate* delegate)
98    : delegate_(delegate),
99      gtk_context_simple_(NULL),
100      gtk_multicontext_(NULL),
101      gtk_context_(NULL) {
102  CHECK(delegate_);
103
104  {
105    // Force a IBus IM context to run in synchronous mode.
106    //
107    // Background: IBus IM context runs by default in asynchronous mode.  In
108    // this mode, gtk_im_context_filter_keypress() consumes all the key events
109    // and returns true while asynchronously sending the event to an underlying
110    // IME implementation.  When the event has not actually been consumed by
111    // the underlying IME implementation, the context pushes the event back to
112    // the GDK event queue marking the event as already handled by the IBus IM
113    // context.
114    //
115    // The problem here is that those pushed-back GDK events are never handled
116    // when base::MessagePumpX11 is used, which only handles X events.  So, we
117    // make a IBus IM context run in synchronous mode by setting an environment
118    // variable.  This is only the interface to change the mode.
119    //
120    // Another possible solution is to use GDK event loop instead of X event
121    // loop.
122    scoped_ptr<base::Environment> env(base::Environment::Create());
123    env->SetVar("IBUS_ENABLE_SYNC_MODE", "1");
124  }
125
126  {
127    XModifierKeymap* keymap = XGetModifierMapping(
128        base::MessagePumpForUI::GetDefaultXDisplay());
129    for (int i = 0; i < 8 * keymap->max_keypermod; ++i) {
130      if (keymap->modifiermap[i])
131        modifier_keycodes_.insert(keymap->modifiermap[i]);
132    }
133    XFreeModifiermap(keymap);
134  }
135
136  gtk_context_simple_ = gtk_im_context_simple_new();
137  gtk_multicontext_ = gtk_im_multicontext_new();
138
139  GtkIMContext* contexts[] = {gtk_context_simple_, gtk_multicontext_};
140  for (size_t i = 0; i < arraysize(contexts); ++i) {
141    g_signal_connect(contexts[i], "commit",
142                     G_CALLBACK(OnCommitThunk), this);
143    g_signal_connect(contexts[i], "preedit-changed",
144                     G_CALLBACK(OnPreeditChangedThunk), this);
145    g_signal_connect(contexts[i], "preedit-end",
146                     G_CALLBACK(OnPreeditEndThunk), this);
147    g_signal_connect(contexts[i], "preedit-start",
148                     G_CALLBACK(OnPreeditStartThunk), this);
149    // TODO(yukishiino): Handle operations on surrounding text.
150    // "delete-surrounding" and "retrieve-surrounding" signals should be
151    // handled.
152  }
153}
154
155X11InputMethodContextImplGtk2::~X11InputMethodContextImplGtk2() {
156  gtk_context_ = NULL;
157  if (gtk_context_simple_) {
158    g_object_unref(gtk_context_simple_);
159    gtk_context_simple_ = NULL;
160  }
161  if (gtk_multicontext_) {
162    g_object_unref(gtk_multicontext_);
163    gtk_multicontext_ = NULL;
164  }
165}
166
167// Overriden from ui::LinuxInputMethodContext
168
169bool X11InputMethodContextImplGtk2::DispatchKeyEvent(
170    const base::NativeEvent& native_key_event) {
171  // The caller must call Focus() first.
172  if (!gtk_context_)
173    return false;
174
175  // Translate a XKeyEvent to a GdkEventKey.
176  GdkEvent* event = GdkEventFromXKeyEvent(
177      native_key_event->xkey,
178      IsKeycodeModifierKey(native_key_event->xkey.keycode));
179  if (!event) {
180    LOG(ERROR) << "Cannot translate a XKeyEvent to a GdkEvent.";
181    return false;
182  }
183
184  // Set the client window and cursor location.
185  gtk_im_context_set_client_window(gtk_context_, event->key.window);
186  // Convert the last known caret bounds relative to the screen coordinates
187  // to a GdkRectangle relative to the client window.
188  gint x = 0;
189  gint y = 0;
190  gdk_window_get_origin(event->key.window, &x, &y);
191  GdkRectangle rect = {last_caret_bounds_.x() - x,
192                       last_caret_bounds_.y() - y,
193                       last_caret_bounds_.width(),
194                       last_caret_bounds_.height()};
195  gtk_im_context_set_cursor_location(gtk_context_, &rect);
196
197  // Let an IME handle the key event.
198  commit_signal_trap_.StartTrap(event->key.keyval);
199  const gboolean handled = gtk_im_context_filter_keypress(gtk_context_,
200                                                          &event->key);
201  commit_signal_trap_.StopTrap();
202  gdk_event_free(event);
203
204  return handled && !commit_signal_trap_.IsSignalCaught();
205}
206
207void X11InputMethodContextImplGtk2::Reset() {
208  // Reset all the states of the context, not only preedit, caret but also
209  // focus.
210  gtk_context_ = NULL;
211  gtk_im_context_reset(gtk_context_simple_);
212  gtk_im_context_reset(gtk_multicontext_);
213  gtk_im_context_focus_out(gtk_context_simple_);
214  gtk_im_context_focus_out(gtk_multicontext_);
215}
216
217base::i18n::TextDirection X11InputMethodContextImplGtk2::GetInputTextDirection()
218    const {
219  switch (gdk_keymap_get_direction(gdk_keymap_get_default())) {
220    case PANGO_DIRECTION_LTR:
221    case PANGO_DIRECTION_TTB_LTR:
222    case PANGO_DIRECTION_WEAK_LTR:
223      return base::i18n::LEFT_TO_RIGHT;
224    case PANGO_DIRECTION_RTL:
225    case PANGO_DIRECTION_TTB_RTL:
226    case PANGO_DIRECTION_WEAK_RTL:
227      return base::i18n::RIGHT_TO_LEFT;
228    case PANGO_DIRECTION_NEUTRAL:
229    default:
230      return base::i18n::UNKNOWN_DIRECTION;
231  }
232}
233
234void X11InputMethodContextImplGtk2::OnTextInputTypeChanged(
235    ui::TextInputType text_input_type) {
236  switch (text_input_type) {
237    case ui::TEXT_INPUT_TYPE_NONE:
238    case ui::TEXT_INPUT_TYPE_PASSWORD:
239      gtk_context_ = gtk_context_simple_;
240      break;
241    default:
242      gtk_context_ = gtk_multicontext_;
243  }
244  gtk_im_context_focus_in(gtk_context_);
245}
246
247void X11InputMethodContextImplGtk2::OnCaretBoundsChanged(
248    const gfx::Rect& caret_bounds) {
249  // Remember the caret bounds so that we can set the cursor location later.
250  // gtk_im_context_set_cursor_location() takes the location relative to the
251  // client window, which is unknown at this point.  So we'll call
252  // gtk_im_context_set_cursor_location() later in ProcessKeyEvent() where
253  // (and only where) we know the client window.
254  last_caret_bounds_ = caret_bounds;
255}
256
257// private:
258
259bool X11InputMethodContextImplGtk2::IsKeycodeModifierKey(
260    unsigned int keycode) const {
261  return modifier_keycodes_.find(keycode) != modifier_keycodes_.end();
262}
263
264// GtkIMContext event handlers.
265
266void X11InputMethodContextImplGtk2::OnCommit(GtkIMContext* context,
267                                             gchar* text) {
268  if (context != gtk_context_)
269    return;
270
271  const base::string16& text_in_utf16 = UTF8ToUTF16(text);
272  // If an underlying IME is emitting the "commit" signal to insert a character
273  // for a direct input key event, ignores the insertion of the character at
274  // this point, because we have to call DispatchKeyEventPostIME() for direct
275  // input key events.  DispatchKeyEvent() takes care of the trapped character
276  // and calls DispatchKeyEventPostIME().
277  if (commit_signal_trap_.Trap(text_in_utf16))
278    return;
279
280  delegate_->OnCommit(text_in_utf16);
281}
282
283void X11InputMethodContextImplGtk2::OnPreeditChanged(GtkIMContext* context) {
284  if (context != gtk_context_)
285    return;
286
287  gchar* str = NULL;
288  PangoAttrList* attrs = NULL;
289  gint cursor_pos = 0;
290  gtk_im_context_get_preedit_string(context, &str, &attrs, &cursor_pos);
291  ui::CompositionText composition_text;
292  ui::ExtractCompositionTextFromGtkPreedit(str, attrs, cursor_pos,
293                                           &composition_text);
294  g_free(str);
295  pango_attr_list_unref(attrs);
296
297  delegate_->OnPreeditChanged(composition_text);
298}
299
300void X11InputMethodContextImplGtk2::OnPreeditEnd(GtkIMContext* context) {
301  if (context != gtk_context_)
302    return;
303
304  delegate_->OnPreeditEnd();
305}
306
307void X11InputMethodContextImplGtk2::OnPreeditStart(GtkIMContext* context) {
308  if (context != gtk_context_)
309    return;
310
311  delegate_->OnPreeditStart();
312}
313
314// GtkCommitSignalTrap
315
316X11InputMethodContextImplGtk2::GtkCommitSignalTrap::GtkCommitSignalTrap()
317    : is_trap_enabled_(false),
318      gdk_event_key_keyval_(GDK_KEY_VoidSymbol),
319      is_signal_caught_(false) {}
320
321void X11InputMethodContextImplGtk2::GtkCommitSignalTrap::StartTrap(
322    guint keyval) {
323  is_signal_caught_ = false;
324  gdk_event_key_keyval_ = keyval;
325  is_trap_enabled_ = true;
326}
327
328void X11InputMethodContextImplGtk2::GtkCommitSignalTrap::StopTrap() {
329  is_trap_enabled_ = false;
330}
331
332bool X11InputMethodContextImplGtk2::GtkCommitSignalTrap::Trap(
333    const base::string16& text) {
334  DCHECK(!is_signal_caught_);
335  if (is_trap_enabled_ &&
336      text.length() == 1 &&
337      text[0] == gdk_keyval_to_unicode(gdk_event_key_keyval_)) {
338    is_signal_caught_ = true;
339    return true;
340  } else {
341    return false;
342  }
343}
344
345}  // namespace libgtk2ui
346