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