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