gtk_im_context_wrapper.cc revision 72a454cd3513ac24fbdd0e0cb9ad70b86a99b801
1// Copyright (c) 2011 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/renderer_host/gtk_im_context_wrapper.h" 6 7#include <gdk/gdk.h> 8#include <gdk/gdkkeysyms.h> 9#include <gtk/gtk.h> 10 11#include <algorithm> 12 13#include "base/logging.h" 14#include "base/string_util.h" 15#include "base/third_party/icu/icu_utf.h" 16#include "base/utf_string_conversions.h" 17#include "chrome/app/chrome_command_ids.h" 18#include "chrome/browser/ui/gtk/gtk_util.h" 19#if !defined(TOOLKIT_VIEWS) 20#include "chrome/browser/ui/gtk/menu_gtk.h" 21#endif 22#include "chrome/browser/renderer_host/render_widget_host.h" 23#include "chrome/browser/renderer_host/render_widget_host_view_gtk.h" 24#include "chrome/common/native_web_keyboard_event.h" 25#include "chrome/common/render_messages.h" 26#include "grit/generated_resources.h" 27#include "third_party/skia/include/core/SkColor.h" 28#include "ui/base/l10n/l10n_util.h" 29#include "ui/gfx/gtk_util.h" 30#include "ui/gfx/rect.h" 31 32namespace { 33// Copied from third_party/WebKit/Source/WebCore/page/EventHandler.cpp 34// 35// Match key code of composition keydown event on windows. 36// IE sends VK_PROCESSKEY which has value 229; 37// 38// Please refer to following documents for detals: 39// - Virtual-Key Codes 40// http://msdn.microsoft.com/en-us/library/ms645540(VS.85).aspx 41// - How the IME System Works 42// http://msdn.microsoft.com/en-us/library/cc194848.aspx 43// - ImmGetVirtualKey Function 44// http://msdn.microsoft.com/en-us/library/dd318570(VS.85).aspx 45const int kCompositionEventKeyCode = 229; 46} // namespace 47 48GtkIMContextWrapper::GtkIMContextWrapper(RenderWidgetHostViewGtk* host_view) 49 : host_view_(host_view), 50 context_(gtk_im_multicontext_new()), 51 context_simple_(gtk_im_context_simple_new()), 52 is_focused_(false), 53 is_composing_text_(false), 54 is_enabled_(false), 55 is_in_key_event_handler_(false), 56 preedit_selection_start_(0), 57 preedit_selection_end_(0), 58 is_preedit_changed_(false), 59 suppress_next_commit_(false), 60 last_key_code_(0), 61 last_key_was_up_(false), 62 last_key_filtered_no_result_(false) { 63 DCHECK(context_); 64 DCHECK(context_simple_); 65 66 // context_ and context_simple_ share the same callback handlers. 67 // All data come from them are treated equally. 68 // context_ is for full input method support. 69 // context_simple_ is for supporting dead/compose keys when input method is 70 // disabled by webkit, eg. in password input box. 71 g_signal_connect(context_, "preedit_start", 72 G_CALLBACK(HandlePreeditStartThunk), this); 73 g_signal_connect(context_, "preedit_end", 74 G_CALLBACK(HandlePreeditEndThunk), this); 75 g_signal_connect(context_, "preedit_changed", 76 G_CALLBACK(HandlePreeditChangedThunk), this); 77 g_signal_connect(context_, "commit", 78 G_CALLBACK(HandleCommitThunk), this); 79 80 g_signal_connect(context_simple_, "preedit_start", 81 G_CALLBACK(HandlePreeditStartThunk), this); 82 g_signal_connect(context_simple_, "preedit_end", 83 G_CALLBACK(HandlePreeditEndThunk), this); 84 g_signal_connect(context_simple_, "preedit_changed", 85 G_CALLBACK(HandlePreeditChangedThunk), this); 86 g_signal_connect(context_simple_, "commit", 87 G_CALLBACK(HandleCommitThunk), this); 88 89 GtkWidget* widget = host_view->native_view(); 90 DCHECK(widget); 91 92 g_signal_connect(widget, "realize", 93 G_CALLBACK(HandleHostViewRealizeThunk), this); 94 g_signal_connect(widget, "unrealize", 95 G_CALLBACK(HandleHostViewUnrealizeThunk), this); 96 97 // Set client window if the widget is already realized. 98 HandleHostViewRealize(widget); 99} 100 101GtkIMContextWrapper::~GtkIMContextWrapper() { 102 if (context_) 103 g_object_unref(context_); 104 if (context_simple_) 105 g_object_unref(context_simple_); 106} 107 108void GtkIMContextWrapper::ProcessKeyEvent(GdkEventKey* event) { 109 suppress_next_commit_ = false; 110 111 // Indicates preedit-changed and commit signal handlers that we are 112 // processing a key event. 113 is_in_key_event_handler_ = true; 114 // Reset this flag so that we can know if preedit is changed after 115 // processing this key event. 116 is_preedit_changed_ = false; 117 // Clear it so that we can know if something needs committing after 118 // processing this key event. 119 commit_text_.clear(); 120 121 // According to Document Object Model (DOM) Level 3 Events Specification 122 // Appendix A: Keyboard events and key identifiers 123 // http://www.w3.org/TR/DOM-Level-3-Events/keyset.html: 124 // The event sequence would be: 125 // 1. keydown 126 // 2. textInput 127 // 3. keyup 128 // 129 // So keydown must be sent to webkit before sending input method result, 130 // while keyup must be sent afterwards. 131 // However on Windows, if a keydown event has been processed by IME, its 132 // virtual keycode will be changed to VK_PROCESSKEY(0xE5) before being sent 133 // to application. 134 // To emulate the windows behavior as much as possible, we need to send the 135 // key event to the GtkIMContext object first, and decide whether or not to 136 // send the original key event to webkit according to the result from IME. 137 // 138 // If IME is enabled by WebKit, this event will be dispatched to context_ 139 // to get full IME support. Otherwise it'll be dispatched to 140 // context_simple_, so that dead/compose keys can still work. 141 // 142 // It sends a "commit" signal when it has a character to be inserted 143 // even when we use a US keyboard so that we can send a Char event 144 // (or an IME event) to the renderer in our "commit"-signal handler. 145 // We should send a KeyDown (or a KeyUp) event before dispatching this 146 // event to the GtkIMContext object (and send a Char event) so that WebKit 147 // can dispatch the JavaScript events in the following order: onkeydown(), 148 // onkeypress(), and onkeyup(). (Many JavaScript pages assume this.) 149 gboolean filtered = false; 150 if (is_enabled_) { 151 filtered = gtk_im_context_filter_keypress(context_, event); 152 } else { 153 filtered = gtk_im_context_filter_keypress(context_simple_, event); 154 } 155 156 // Reset this flag here, as it's only used in input method callbacks. 157 is_in_key_event_handler_ = false; 158 159 NativeWebKeyboardEvent wke(event); 160 161 // If the key event was handled by the input method, then we need to prevent 162 // RenderView::UnhandledKeyboardEvent() from processing it. 163 // Otherwise unexpected result may occur. For example if it's a 164 // Backspace key event, the browser may go back to previous page. 165 // We just send all keyup events to the browser to avoid breaking the 166 // browser's MENU key function, which is actually the only keyup event 167 // handled in the browser. 168 if (filtered && event->type == GDK_KEY_PRESS) 169 wke.skip_in_browser = true; 170 171 const int key_code = wke.windowsKeyCode; 172 const bool has_result = HasInputMethodResult(); 173 174 // Send filtered keydown event before sending IME result. 175 // In order to workaround http://crosbug.com/6582, we only send a filtered 176 // keydown event if it generated any input method result. 177 if (event->type == GDK_KEY_PRESS && filtered && has_result) 178 ProcessFilteredKeyPressEvent(&wke); 179 180 // Send IME results. In most cases, it's only available if the key event 181 // is filtered by IME. But in rare cases, an unfiltered key event may also 182 // generate IME results. 183 // Any IME results generated by a unfiltered key down event must be sent 184 // before the key down event, to avoid some tricky issues. For example, 185 // when using latin-post input method, pressing 'a' then Backspace, may 186 // generate following events in sequence: 187 // 1. keydown 'a' (filtered) 188 // 2. preedit changed to "a" 189 // 3. keyup 'a' (unfiltered) 190 // 4. keydown Backspace (unfiltered) 191 // 5. commit "a" 192 // 6. preedit end 193 // 7. keyup Backspace (unfiltered) 194 // 195 // In this case, the input box will be in a strange state if keydown 196 // Backspace is sent to webkit before commit "a" and preedit end. 197 if (has_result) 198 ProcessInputMethodResult(event, filtered); 199 200 // Send unfiltered keydown and keyup events after sending IME result. 201 if (event->type == GDK_KEY_PRESS && !filtered) { 202 ProcessUnfilteredKeyPressEvent(&wke); 203 } else if (event->type == GDK_KEY_RELEASE) { 204 // In order to workaround http://crosbug.com/6582, we need to suppress 205 // the keyup event if corresponding keydown event was suppressed, or 206 // the last key event was a keyup event with the same keycode. 207 const bool suppress = (last_key_code_ == key_code) && 208 (last_key_was_up_ || last_key_filtered_no_result_); 209 210 if (!suppress) 211 host_view_->ForwardKeyboardEvent(wke); 212 } 213 214 last_key_code_ = key_code; 215 last_key_was_up_ = (event->type == GDK_KEY_RELEASE); 216 last_key_filtered_no_result_ = (filtered && !has_result); 217} 218 219void GtkIMContextWrapper::UpdateInputMethodState(WebKit::WebTextInputType type, 220 const gfx::Rect& caret_rect) { 221 suppress_next_commit_ = false; 222 223 // The renderer has updated its IME status. 224 // Control the GtkIMContext object according to this status. 225 if (!context_ || !is_focused_) 226 return; 227 228 DCHECK(!is_in_key_event_handler_); 229 230 bool is_enabled = (type == WebKit::WebTextInputTypeText); 231 if (is_enabled_ != is_enabled) { 232 is_enabled_ = is_enabled; 233 if (is_enabled) 234 gtk_im_context_focus_in(context_); 235 else 236 gtk_im_context_focus_out(context_); 237 } 238 239 if (is_enabled) { 240 // Updates the position of the IME candidate window. 241 // The position sent from the renderer is a relative one, so we need to 242 // attach the GtkIMContext object to this window before changing the 243 // position. 244 GdkRectangle cursor_rect(caret_rect.ToGdkRectangle()); 245 gtk_im_context_set_cursor_location(context_, &cursor_rect); 246 } 247} 248 249void GtkIMContextWrapper::OnFocusIn() { 250 if (is_focused_) 251 return; 252 253 // Tracks the focused state so that we can give focus to the 254 // GtkIMContext object correctly later when IME is enabled by WebKit. 255 is_focused_ = true; 256 257 last_key_code_ = 0; 258 last_key_was_up_ = false; 259 last_key_filtered_no_result_ = false; 260 261 // Notify the GtkIMContext object of this focus-in event only if IME is 262 // enabled by WebKit. 263 if (is_enabled_) 264 gtk_im_context_focus_in(context_); 265 266 // context_simple_ is always enabled. 267 // Actually it doesn't care focus state at all. 268 gtk_im_context_focus_in(context_simple_); 269 270 // Enables RenderWidget's IME related events, so that we can be notified 271 // when WebKit wants to enable or disable IME. 272 if (host_view_->GetRenderWidgetHost()) 273 host_view_->GetRenderWidgetHost()->SetInputMethodActive(true); 274} 275 276void GtkIMContextWrapper::OnFocusOut() { 277 if (!is_focused_) 278 return; 279 280 // Tracks the focused state so that we won't give focus to the 281 // GtkIMContext object unexpectly. 282 is_focused_ = false; 283 284 // Notify the GtkIMContext object of this focus-out event only if IME is 285 // enabled by WebKit. 286 if (is_enabled_) { 287 // To reset the GtkIMContext object and prevent data loss. 288 ConfirmComposition(); 289 gtk_im_context_focus_out(context_); 290 } 291 292 // To make sure it'll be in correct state when focused in again. 293 gtk_im_context_reset(context_simple_); 294 gtk_im_context_focus_out(context_simple_); 295 296 is_composing_text_ = false; 297 298 // Disable RenderWidget's IME related events to save bandwidth. 299 if (host_view_->GetRenderWidgetHost()) 300 host_view_->GetRenderWidgetHost()->SetInputMethodActive(false); 301} 302 303#if !defined(TOOLKIT_VIEWS) 304// Not defined for views because the views context menu doesn't 305// implement input methods yet. 306void GtkIMContextWrapper::AppendInputMethodsContextMenu(MenuGtk* menu) { 307 gboolean show_input_method_menu = TRUE; 308 309 g_object_get(gtk_widget_get_settings(GTK_WIDGET(host_view_->native_view())), 310 "gtk-show-input-method-menu", &show_input_method_menu, NULL); 311 if (!show_input_method_menu) 312 return; 313 314 std::string label = gfx::ConvertAcceleratorsFromWindowsStyle( 315 l10n_util::GetStringUTF8(IDS_CONTENT_CONTEXT_INPUT_METHODS_MENU)); 316 GtkWidget* menuitem = gtk_menu_item_new_with_mnemonic(label.c_str()); 317 GtkWidget* submenu = gtk_menu_new(); 318 gtk_menu_item_set_submenu(GTK_MENU_ITEM(menuitem), submenu); 319 gtk_im_multicontext_append_menuitems(GTK_IM_MULTICONTEXT(context_), 320 GTK_MENU_SHELL(submenu)); 321 menu->AppendSeparator(); 322 menu->AppendMenuItem(IDC_INPUT_METHODS_MENU, menuitem); 323} 324#endif 325 326void GtkIMContextWrapper::CancelComposition() { 327 if (!is_enabled_) 328 return; 329 330 DCHECK(!is_in_key_event_handler_); 331 332 // To prevent any text from being committed when resetting the |context_|; 333 is_in_key_event_handler_ = true; 334 suppress_next_commit_ = true; 335 336 gtk_im_context_reset(context_); 337 gtk_im_context_reset(context_simple_); 338 339 if (is_focused_) { 340 // Some input methods may not honour the reset call. Focusing out/in the 341 // |context_| to make sure it gets reset correctly. 342 gtk_im_context_focus_out(context_); 343 gtk_im_context_focus_in(context_); 344 } 345 346 is_composing_text_ = false; 347 preedit_text_.clear(); 348 preedit_underlines_.clear(); 349 commit_text_.clear(); 350 351 is_in_key_event_handler_ = false; 352} 353 354bool GtkIMContextWrapper::NeedCommitByForwardingCharEvent() const { 355 // If there is no composition text and has only one character to be 356 // committed, then the character will be send to webkit as a Char event 357 // instead of a confirmed composition text. 358 // It should be fine to handle BMP character only, as non-BMP characters 359 // can always be committed as confirmed composition text. 360 return !is_composing_text_ && commit_text_.length() == 1; 361} 362 363bool GtkIMContextWrapper::HasInputMethodResult() const { 364 return commit_text_.length() || is_preedit_changed_; 365} 366 367void GtkIMContextWrapper::ProcessFilteredKeyPressEvent( 368 NativeWebKeyboardEvent* wke) { 369 // If IME has filtered this event, then replace virtual key code with 370 // VK_PROCESSKEY. See comment in ProcessKeyEvent() for details. 371 // It's only required for keydown events. 372 // To emulate windows behavior, when input method is enabled, if the commit 373 // text can be emulated by a Char event, then don't do this replacement. 374 if (!NeedCommitByForwardingCharEvent()) { 375 wke->windowsKeyCode = kCompositionEventKeyCode; 376 // keyidentifier must be updated accordingly, otherwise this key event may 377 // still be processed by webkit. 378 wke->setKeyIdentifierFromWindowsKeyCode(); 379 } 380 host_view_->ForwardKeyboardEvent(*wke); 381} 382 383void GtkIMContextWrapper::ProcessUnfilteredKeyPressEvent( 384 NativeWebKeyboardEvent* wke) { 385 // Send keydown event as it, because it's not filtered by IME. 386 host_view_->ForwardKeyboardEvent(*wke); 387 388 // IME is disabled by WebKit or the GtkIMContext object cannot handle 389 // this key event. 390 // This case is caused by two reasons: 391 // 1. The given key event is a control-key event, (e.g. return, page up, 392 // page down, tab, arrows, etc.) or; 393 // 2. The given key event is not a control-key event but printable 394 // characters aren't assigned to the event, (e.g. alt+d, etc.) 395 // Create a Char event manually from this key event and send it to the 396 // renderer when this Char event contains a printable character which 397 // should be processed by WebKit. 398 // isSystemKey will be set to true if this key event has Alt modifier, 399 // see WebInputEventFactory::keyboardEvent() for details. 400 if (wke->text[0]) { 401 wke->type = WebKit::WebInputEvent::Char; 402 wke->skip_in_browser = true; 403 host_view_->ForwardKeyboardEvent(*wke); 404 } 405} 406 407void GtkIMContextWrapper::ProcessInputMethodResult(const GdkEventKey* event, 408 bool filtered) { 409 RenderWidgetHost* host = host_view_->GetRenderWidgetHost(); 410 if (!host) 411 return; 412 413 bool committed = false; 414 // We do commit before preedit change, so that we can optimize some 415 // unnecessary preedit changes. 416 if (commit_text_.length()) { 417 if (filtered && NeedCommitByForwardingCharEvent()) { 418 // Send a Char event when we input a composed character without IMEs 419 // so that this event is to be dispatched to onkeypress() handlers, 420 // autofill, etc. 421 // Only commit text generated by a filtered key down event can be sent 422 // as a Char event, because a unfiltered key down event will probably 423 // generate another Char event. 424 // TODO(james.su@gmail.com): Is it necessary to support non BMP chars 425 // here? 426 NativeWebKeyboardEvent char_event(commit_text_[0], 427 event->state, 428 base::Time::Now().ToDoubleT()); 429 char_event.skip_in_browser = true; 430 host_view_->ForwardKeyboardEvent(char_event); 431 } else { 432 committed = true; 433 // Send an IME event. 434 // Unlike a Char event, an IME event is NOT dispatched to onkeypress() 435 // handlers or autofill. 436 host->ImeConfirmComposition(commit_text_); 437 // Set this flag to false, as this composition session has been 438 // finished. 439 is_composing_text_ = false; 440 } 441 } 442 443 // Send preedit text only if it's changed. 444 // If a text has been committed, then we don't need to send the empty 445 // preedit text again. 446 if (is_preedit_changed_) { 447 if (preedit_text_.length()) { 448 // Another composition session has been started. 449 is_composing_text_ = true; 450 host->ImeSetComposition(preedit_text_, preedit_underlines_, 451 preedit_selection_start_, preedit_selection_end_); 452 } else if (!committed) { 453 host->ImeCancelComposition(); 454 } 455 } 456} 457 458void GtkIMContextWrapper::ConfirmComposition() { 459 if (!is_enabled_) 460 return; 461 462 DCHECK(!is_in_key_event_handler_); 463 464 if (is_composing_text_) { 465 if (host_view_->GetRenderWidgetHost()) 466 host_view_->GetRenderWidgetHost()->ImeConfirmComposition(); 467 468 // Reset the input method. 469 CancelComposition(); 470 } 471} 472 473void GtkIMContextWrapper::HandleCommit(const string16& text) { 474 if (suppress_next_commit_) { 475 suppress_next_commit_ = false; 476 return; 477 } 478 479 // Append the text to the buffer, because commit signal might be fired 480 // multiple times when processing a key event. 481 commit_text_.append(text); 482 // Nothing needs to do, if it's currently in ProcessKeyEvent() 483 // handler, which will send commit text to webkit later. Otherwise, 484 // we need send it here. 485 // It's possible that commit signal is fired without a key event, for 486 // example when user input via a voice or handwriting recognition software. 487 // In this case, the text must be committed directly. 488 if (!is_in_key_event_handler_ && host_view_->GetRenderWidgetHost()) { 489 // Workaround http://crbug.com/45478 by sending fake key down/up events. 490 SendFakeCompositionKeyEvent(WebKit::WebInputEvent::RawKeyDown); 491 host_view_->GetRenderWidgetHost()->ImeConfirmComposition(text); 492 SendFakeCompositionKeyEvent(WebKit::WebInputEvent::KeyUp); 493 } 494} 495 496void GtkIMContextWrapper::HandlePreeditStart() { 497 // Ignore preedit related signals triggered by CancelComposition() method. 498 if (suppress_next_commit_) 499 return; 500 is_composing_text_ = true; 501} 502 503void GtkIMContextWrapper::HandlePreeditChanged(const gchar* text, 504 PangoAttrList* attrs, 505 int cursor_position) { 506 // Ignore preedit related signals triggered by CancelComposition() method. 507 if (suppress_next_commit_) 508 return; 509 510 // Don't set is_preedit_changed_ to false if there is no change, because 511 // this handler might be called multiple times with the same data. 512 is_preedit_changed_ = true; 513 preedit_text_.clear(); 514 preedit_underlines_.clear(); 515 preedit_selection_start_ = 0; 516 preedit_selection_end_ = 0; 517 518 ExtractCompositionInfo(text, attrs, cursor_position, &preedit_text_, 519 &preedit_underlines_, &preedit_selection_start_, 520 &preedit_selection_end_); 521 522 // In case we are using a buggy input method which doesn't fire 523 // "preedit_start" signal. 524 if (preedit_text_.length()) 525 is_composing_text_ = true; 526 527 // Nothing needs to do, if it's currently in ProcessKeyEvent() 528 // handler, which will send preedit text to webkit later. 529 // Otherwise, we need send it here if it's been changed. 530 if (!is_in_key_event_handler_ && is_composing_text_ && 531 host_view_->GetRenderWidgetHost()) { 532 // Workaround http://crbug.com/45478 by sending fake key down/up events. 533 SendFakeCompositionKeyEvent(WebKit::WebInputEvent::RawKeyDown); 534 host_view_->GetRenderWidgetHost()->ImeSetComposition( 535 preedit_text_, preedit_underlines_, preedit_selection_start_, 536 preedit_selection_end_); 537 SendFakeCompositionKeyEvent(WebKit::WebInputEvent::KeyUp); 538 } 539} 540 541void GtkIMContextWrapper::HandlePreeditEnd() { 542 if (preedit_text_.length()) { 543 // The composition session has been finished. 544 preedit_text_.clear(); 545 preedit_underlines_.clear(); 546 is_preedit_changed_ = true; 547 548 // If there is still a preedit text when firing "preedit-end" signal, 549 // we need inform webkit to clear it. 550 // It's only necessary when it's not in ProcessKeyEvent (). 551 if (!is_in_key_event_handler_ && host_view_->GetRenderWidgetHost()) 552 host_view_->GetRenderWidgetHost()->ImeCancelComposition(); 553 } 554 555 // Don't set is_composing_text_ to false here, because "preedit_end" 556 // signal may be fired before "commit" signal. 557} 558 559void GtkIMContextWrapper::HandleHostViewRealize(GtkWidget* widget) { 560 // We should only set im context's client window once, because when setting 561 // client window.im context may destroy and recreate its internal states and 562 // objects. 563 if (widget->window) { 564 gtk_im_context_set_client_window(context_, widget->window); 565 gtk_im_context_set_client_window(context_simple_, widget->window); 566 } 567} 568 569void GtkIMContextWrapper::HandleHostViewUnrealize() { 570 gtk_im_context_set_client_window(context_, NULL); 571 gtk_im_context_set_client_window(context_simple_, NULL); 572} 573 574void GtkIMContextWrapper::SendFakeCompositionKeyEvent( 575 WebKit::WebInputEvent::Type type) { 576 NativeWebKeyboardEvent fake_event; 577 fake_event.windowsKeyCode = kCompositionEventKeyCode; 578 fake_event.skip_in_browser = true; 579 fake_event.type = type; 580 host_view_->ForwardKeyboardEvent(fake_event); 581} 582 583void GtkIMContextWrapper::HandleCommitThunk( 584 GtkIMContext* context, gchar* text, GtkIMContextWrapper* self) { 585 self->HandleCommit(UTF8ToUTF16(text)); 586} 587 588void GtkIMContextWrapper::HandlePreeditStartThunk( 589 GtkIMContext* context, GtkIMContextWrapper* self) { 590 self->HandlePreeditStart(); 591} 592 593void GtkIMContextWrapper::HandlePreeditChangedThunk( 594 GtkIMContext* context, GtkIMContextWrapper* self) { 595 gchar* text = NULL; 596 PangoAttrList* attrs = NULL; 597 gint cursor_position = 0; 598 gtk_im_context_get_preedit_string(context, &text, &attrs, &cursor_position); 599 self->HandlePreeditChanged(text, attrs, cursor_position); 600 g_free(text); 601 pango_attr_list_unref(attrs); 602} 603 604void GtkIMContextWrapper::HandlePreeditEndThunk( 605 GtkIMContext* context, GtkIMContextWrapper* self) { 606 self->HandlePreeditEnd(); 607} 608 609void GtkIMContextWrapper::HandleHostViewRealizeThunk( 610 GtkWidget* widget, GtkIMContextWrapper* self) { 611 self->HandleHostViewRealize(widget); 612} 613 614void GtkIMContextWrapper::HandleHostViewUnrealizeThunk( 615 GtkWidget* widget, GtkIMContextWrapper* self) { 616 self->HandleHostViewUnrealize(); 617} 618 619void GtkIMContextWrapper::ExtractCompositionInfo( 620 const gchar* utf8_text, 621 PangoAttrList* attrs, 622 int cursor_position, 623 string16* utf16_text, 624 std::vector<WebKit::WebCompositionUnderline>* underlines, 625 int* selection_start, 626 int* selection_end) { 627 *utf16_text = UTF8ToUTF16(utf8_text); 628 629 if (utf16_text->empty()) 630 return; 631 632 // Gtk/Pango uses character index for cursor position and byte index for 633 // attribute range, but we use char16 offset for them. So we need to do 634 // conversion here. 635 std::vector<int> char16_offsets; 636 int length = static_cast<int>(utf16_text->length()); 637 for (int offset = 0; offset < length; ++offset) { 638 char16_offsets.push_back(offset); 639 if (CBU16_IS_SURROGATE((*utf16_text)[offset])) 640 ++offset; 641 } 642 643 // The text length in Unicode characters. 644 int char_length = static_cast<int>(char16_offsets.size()); 645 // Make sure we can convert the value of |char_length| as well. 646 char16_offsets.push_back(length); 647 648 int cursor_offset = 649 char16_offsets[std::max(0, std::min(char_length, cursor_position))]; 650 651 // TODO(suzhe): due to a bug of webkit, we currently can't use selection range 652 // with composition string. See: https://bugs.webkit.org/show_bug.cgi?id=40805 653 *selection_start = *selection_end = cursor_offset; 654 655 if (attrs) { 656 int utf8_length = strlen(utf8_text); 657 PangoAttrIterator* iter = pango_attr_list_get_iterator(attrs); 658 659 // We only care about underline and background attributes and convert 660 // background attribute into selection if possible. 661 do { 662 gint start, end; 663 pango_attr_iterator_range(iter, &start, &end); 664 665 start = std::min(start, utf8_length); 666 end = std::min(end, utf8_length); 667 if (start >= end) 668 continue; 669 670 start = g_utf8_pointer_to_offset(utf8_text, utf8_text + start); 671 end = g_utf8_pointer_to_offset(utf8_text, utf8_text + end); 672 673 // Double check, in case |utf8_text| is not a valid utf-8 string. 674 start = std::min(start, char_length); 675 end = std::min(end, char_length); 676 if (start >= end) 677 continue; 678 679 PangoAttribute* background_attr = 680 pango_attr_iterator_get(iter, PANGO_ATTR_BACKGROUND); 681 PangoAttribute* underline_attr = 682 pango_attr_iterator_get(iter, PANGO_ATTR_UNDERLINE); 683 684 if (background_attr || underline_attr) { 685 // Use a black thin underline by default. 686 WebKit::WebCompositionUnderline underline( 687 char16_offsets[start], char16_offsets[end], SK_ColorBLACK, false); 688 689 // Always use thick underline for a range with background color, which 690 // is usually the selection range. 691 if (background_attr) 692 underline.thick = true; 693 if (underline_attr) { 694 int type = reinterpret_cast<PangoAttrInt*>(underline_attr)->value; 695 if (type == PANGO_UNDERLINE_DOUBLE) 696 underline.thick = true; 697 else if (type == PANGO_UNDERLINE_ERROR) 698 underline.color = SK_ColorRED; 699 } 700 underlines->push_back(underline); 701 } 702 } while (pango_attr_iterator_next(iter)); 703 pango_attr_iterator_destroy(iter); 704 } 705 706 // Use a black thin underline by default. 707 if (underlines->empty()) { 708 underlines->push_back( 709 WebKit::WebCompositionUnderline(0, length, SK_ColorBLACK, false)); 710 } 711} 712