diff options
author | estade@chromium.org <estade@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2009-07-21 19:51:24 +0000 |
---|---|---|
committer | estade@chromium.org <estade@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2009-07-21 19:51:24 +0000 |
commit | c159467e0fe94e676213d98c3428c6b611b7d4f4 (patch) | |
tree | 5e8bd985a052c4d2119ff96219c855883cb2e294 /chrome/browser | |
parent | 99050a88a4fe4f4acaa7b98e94cea7f0a84b4f18 (diff) | |
download | chromium_src-c159467e0fe94e676213d98c3428c6b611b7d4f4.zip chromium_src-c159467e0fe94e676213d98c3428c6b611b7d4f4.tar.gz chromium_src-c159467e0fe94e676213d98c3428c6b611b7d4f4.tar.bz2 |
This change list improves IME support on Linux. Many corner cases that were not
handled in original code are addressed, for example the input method in password
box case.
The most important change in this CL is the change to key event processing flow.
In old code, a key event will first be sent to webkit then dispatched to IME
for filtering. With this CL, a key event will first be dispatched to IME for
filtering, then how to send the event to webkit is decided by the filtering
result.
This CL tries to emulate the keyboard input behavior on Windows as much as
possible. For example, if a keydown event is filtered by IME, then its virtual
key code will be changed to VK_PROCESSKEY(0xE5) to prevent webkit from
processing it again. This behavior can workaround bug 16281.
To test this CL, you may cut and save following html code into a file and open
it in chrome.
------------------8<----cut here----->8---------------------
<html><head>
<meta http-equiv="content-type" content="text/html; charset=utf-8">
<script>
function keyEventHandler(event) {
var props = [ "type", "charCode", "keyCode", "altKey",
"ctrlKey", "shiftKey", "metaKey" ];
var info = document.getElementById('info');
var s = '';
for (var i in props) {
s += props[i] + ':' + event[props[i]] + ' ';
}
info.value += s + '\n';
}
function textEventHandler(event) {
info.value += "type:" + event['type'] + " data:" + event['data'] + '\n';
}
function passwordChangeEventHandler(event) {
var input2 = document.getElementById('input2');
info.value += "password:" + input2.value + '\n';
}
function onLoad() {
var input = document.getElementById('input');
input.addEventListener('keydown', keyEventHandler, false);
input.addEventListener('keyup', keyEventHandler, false);
input.addEventListener('keypress', keyEventHandler, false);
input.addEventListener('textInput', textEventHandler, false);
var input2 = document.getElementById('input2');
input2.addEventListener('change', passwordChangeEventHandler, false);
}
</script>
</head><body onload="onLoad()">
<input id="input" size="20">
<input id="input2" type="password" size="20">
<p>
<textarea id="info" rows="40" cols="150"></textarea>
</p></body></html>
------------------8<----cut here----->8---------------------
This CL was confirmed to fix following issues:
BUG=16281 "arrow keys and backspace/delete keys move/delete two characters at a
time when xim immodule is used"
BUG=16282 "Disable IMEs in a password input"
BUG=16596 "fcitx (chinese input method) not working in ubuntu 9.04"
BUG=16659 "Crash near RenderWidgetHostViewGtk::IMEUpdateStatus"
BUG=16699 "Can't move cursor to omnibox and use input method if cursor is
currently in a text input box in web page."
BUG=16796 "Input method issue: When inputting text in a text box, if there is a
composition string then pressing Backspace or Delete will cause the composition
string be cleared and the text box refuses to accept any further input.
All tests assume above html code is used.
TEST=Start scim with "scim -d" and start chrome with GTK_IM_MODULE=xim and
XMODIFIERS=@im=SCIM. Type something in input box, eg. "hello", then press
backspace, to see if only one character is deleted.
TEST=Move cursor to password input box, press ctrl-space to see if input method
is not activated. Switch keyboard layout to Canadian-French then type a'[{' key
and an 'a' key, then press enter, to see if a Latin character "U+00E2" is
inputted in password box.
TEST=Install fcitx with "sudo apt-get install fcitx" (assume you are using
Ubuntu/Debian). Open a terminal, export XMODIFIERS=@im=fcitx and
GTK_IM_MODULE=xim then run fcitx, then start chrome. Move cursor into an input
box, press ctrl-space and input something, eg. "nihao" then press space. Check
if some Chinese characters are inputted.
TEST=Start chrome with GTK_IM_MODULE=scim. Move cursor into a text input box
then move into a password box, chrome should not crash.
TEST=Move cursor into a text input box, then click omnibox, and see if the text
input box has lost focus. Press ctrl-space to activate input method, then type
something, and see of the input goes into omnibox.
TEST=Move cursor into a text input box and enable a Chinese Pinyin input method,
then type something, eg. "dajiahao", make sure a composition string is displayed
in the text input box, then press backspace and see if there is still a
composition string and backspace is handled by input method.
patch by James Su <james.su [at] gmail>
original review URL: <http://codereview.chromium.org/149755>
Review URL: http://codereview.chromium.org/155869
git-svn-id: svn://svn.chromium.org/chrome/trunk/src@21203 0039d316-1c4b-4281-b951-d872f2087c98
Diffstat (limited to 'chrome/browser')
-rw-r--r-- | chrome/browser/renderer_host/render_widget_host_view_gtk.cc | 739 | ||||
-rw-r--r-- | chrome/browser/renderer_host/render_widget_host_view_gtk.h | 35 |
2 files changed, 594 insertions, 180 deletions
diff --git a/chrome/browser/renderer_host/render_widget_host_view_gtk.cc b/chrome/browser/renderer_host/render_widget_host_view_gtk.cc index 7be0f39..d70c601 100644 --- a/chrome/browser/renderer_host/render_widget_host_view_gtk.cc +++ b/chrome/browser/renderer_host/render_widget_host_view_gtk.cc @@ -30,6 +30,569 @@ static const int kMaxWindowHeight = 4000; using WebKit::WebInputEventFactory; +// This class is a conveience wrapper for GtkIMContext. +class RenderWidgetHostViewGtkIMContext { + public: + explicit RenderWidgetHostViewGtkIMContext(RenderWidgetHostViewGtk* host_view) + : host_view_(host_view), + context_(gtk_im_multicontext_new()), + context_simple_(gtk_im_context_simple_new()), + is_focused_(false), + is_composing_text_(false), + is_enabled_(false), + is_in_key_event_handler_(false), + preedit_cursor_position_(0), + is_preedit_changed_(false) { + DCHECK(context_); + DCHECK(context_simple_); + + // 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 by webkit, eg. in password input box. + g_signal_connect(context_, "preedit_start", + G_CALLBACK(HandlePreeditStartThunk), this); + g_signal_connect(context_, "preedit_end", + G_CALLBACK(HandlePreeditEndThunk), this); + g_signal_connect(context_, "preedit_changed", + G_CALLBACK(HandlePreeditChangedThunk), this); + g_signal_connect(context_, "commit", + G_CALLBACK(HandleCommitThunk), this); + + g_signal_connect(context_simple_, "preedit_start", + G_CALLBACK(HandlePreeditStartThunk), this); + g_signal_connect(context_simple_, "preedit_end", + G_CALLBACK(HandlePreeditEndThunk), this); + g_signal_connect(context_simple_, "preedit_changed", + G_CALLBACK(HandlePreeditChangedThunk), this); + g_signal_connect(context_simple_, "commit", + G_CALLBACK(HandleCommitThunk), this); + } + + ~RenderWidgetHostViewGtkIMContext() { + if (context_) + g_object_unref(context_); + if (context_simple_) + g_object_unref(context_simple_); + } + + void ProcessKeyEvent(GdkEventKey* event) { + // Indicates preedit-changed and commit signal handlers that we are + // processing a key event. + is_in_key_event_handler_ = true; + // Reset this flag so that we can know if preedit is changed after + // processing this key event. + is_preedit_changed_ = false; + // Clear it so that we can know if something needs committing after + // processing this key event. + commit_text_.clear(); + + // According to Document Object Model (DOM) Level 3 Events Specification + // Appendix A: Keyboard events and key identifiers + // http://www.w3.org/TR/DOM-Level-3-Events/keyset.html: + // The event sequence would be: + // 1. keydown + // 2. textInput + // 3. keyup + // + // So keydown must be sent to webkit before sending input method result, + // while keyup must be sent afterwards. + // However on Windows, if a keydown event has been processed by IME, its + // virtual keycode will be changed to VK_PROCESSKEY(0xE5) before being sent + // to application. + // To emulate the windows behavior as much as possible, we need to send the + // key event to the GtkIMContext object first, and decide whether or not to + // send the original key event to webkit according to the result from IME. + // + // If IME is enabled by WebKit, this event will be dispatched to context_ + // to get full IME support. Otherwise it'll be dispatched to + // context_simple_, so that dead/compose keys can still work. + // + // It sends a "commit" signal when it has a character to be inserted + // even when we use a US keyboard so that we can send a Char event + // (or an IME event) to the renderer in our "commit"-signal handler. + // We should send a KeyDown (or a KeyUp) event before dispatching this + // event to the GtkIMContext object (and send a Char event) so that WebKit + // can dispatch the JavaScript events in the following order: onkeydown(), + // onkeypress(), and onkeyup(). (Many JavaScript pages assume this.) + gboolean filtered = false; + if (is_enabled_) { + filtered = gtk_im_context_filter_keypress(context_, event); + } else { + filtered = gtk_im_context_filter_keypress(context_simple_, event); + } + + NativeWebKeyboardEvent wke(event); + + // Send filtered keydown event before sending IME result. + if (event->type == GDK_KEY_PRESS && filtered) + ProcessFilteredKeyPressEvent(&wke); + + // Send IME results. In most cases, it's only available if the key event + // is filtered by IME. But in rare cases, an unfiltered key event may also + // generate IME results. + // Any IME results generated by a unfiltered key down event must be sent + // before the key down event, to avoid some tricky issues. For example, + // when using latin-post input method, pressing 'a' then Backspace, may + // generate following events in sequence: + // 1. keydown 'a' (filtered) + // 2. preedit changed to "a" + // 3. keyup 'a' (unfiltered) + // 4. keydown Backspace (unfiltered) + // 5. commit "a" + // 6. preedit end + // 7. keyup Backspace (unfiltered) + // + // In this case, the input box will be in a strange state if keydown + // Backspace is sent to webkit before commit "a" and preedit end. + ProcessInputMethodResult(event, filtered); + + // Send unfiltered keydown and keyup events after sending IME result. + if (event->type == GDK_KEY_PRESS && !filtered) + ProcessUnfilteredKeyPressEvent(&wke); + else if (event->type == GDK_KEY_RELEASE) + host_view_->GetRenderWidgetHost()->ForwardKeyboardEvent(wke); + + // End of key event processing. + is_in_key_event_handler_ = false; + } + + void UpdateStatus(int control, const gfx::Rect& caret_rect) { + // The renderer has updated its IME status. + // Control the GtkIMContext object according to this status. + if (!context_ || !is_focused_) + return; + + DCHECK(!is_in_key_event_handler_); + + // TODO(james.su@gmail.com): Following code causes a side effect: + // When trying to move cursor from one text input box to another while + // composition text is still not confirmed, following CompleteComposition() + // calls will prevent the cursor from moving outside the first input box. + if (control == IME_DISABLE) { + if (is_enabled_) { + CompleteComposition(); + gtk_im_context_reset(context_simple_); + gtk_im_context_focus_out(context_); + is_enabled_ = false; + } + } else { + // Enable the GtkIMContext object if it's not enabled yet. + if (!is_enabled_) { + // Reset context_simple_ to its initial state, in case it's currently + // in middle of a composition session inside a password box. + gtk_im_context_reset(context_simple_); + gtk_im_context_focus_in(context_); + // It might be true when switching from a password box in middle of a + // composition session. + is_composing_text_ = false; + is_enabled_ = true; + } else if (control == IME_COMPLETE_COMPOSITION) { + CompleteComposition(); + } + + // Updates the position of the IME candidate window. + // The position sent from the renderer is a relative one, so we need to + // attach the GtkIMContext object to this window before changing the + // position. + GdkRectangle cursor_rect(caret_rect.ToGdkRectangle()); + gtk_im_context_set_cursor_location(context_, &cursor_rect); + } + } + + void OnFocusIn() { + if (is_focused_) + return; + + // Tracks the focused state so that we can give focus to the + // GtkIMContext object correctly later when IME is enabled by WebKit. + is_focused_ = true; + + // We should call gtk_im_context_set_client_window() only when this window + // gain (or release) the window focus because an immodule may reset its + // internal status when processing this function. + gtk_im_context_set_client_window(context_, + host_view_->native_view()->window); + + // Notify the GtkIMContext object of this focus-in event only if IME is + // enabled by WebKit. + if (is_enabled_) + gtk_im_context_focus_in(context_); + + // Actually current GtkIMContextSimple implementation doesn't care about + // client window. This line is just for safe. + gtk_im_context_set_client_window(context_simple_, + host_view_->native_view()->window); + + // context_simple_ is always enabled. + // Actually it doesn't care focus state at all. + gtk_im_context_focus_in(context_simple_); + + // Enables RenderWidget's IME related events, so that we can be notified + // when WebKit wants to enable or disable IME. + host_view_->GetRenderWidgetHost()->ImeSetInputMode(true); + } + + void OnFocusOut() { + if (!is_focused_) + return; + + // Tracks the focused state so that we won't give focus to the + // GtkIMContext object unexpectly. + is_focused_ = false; + + // Notify the GtkIMContext object of this focus-out event only if IME is + // enabled by WebKit. + if (is_enabled_) { + // To reset the GtkIMContext object and prevent data loss. + CompleteComposition(); + gtk_im_context_focus_out(context_); + } + + // Detach this GtkIMContext object from this window. + gtk_im_context_set_client_window(context_, NULL); + + // To make sure it'll be in correct state when focused in again. + gtk_im_context_reset(context_simple_); + gtk_im_context_focus_out(context_simple_); + gtk_im_context_set_client_window(context_simple_, NULL); + + // Reset stored IME status. + is_composing_text_ = false; + preedit_text_.clear(); + preedit_cursor_position_ = 0; + + // Disable RenderWidget's IME related events to save bandwidth. + host_view_->GetRenderWidgetHost()->ImeSetInputMode(false); + } + + private: + // Check if a text needs commit by forwarding a char event instead of + // by confirming as a composition text. + bool NeedCommitByForwardingCharEvent() { + // If there is no composition text and has only one character to be + // committed, then the character will be send to webkit as a Char event + // instead of a confirmed composition text. + // It should be fine to handle BMP character only, as non-BMP characters + // can always be committed as confirmed composition text. + return !is_composing_text_ && commit_text_.length() == 1; + } + + void ProcessFilteredKeyPressEvent(NativeWebKeyboardEvent* wke) { + // Copied from third_party/WebKit/WebCore/page/EventHandler.cpp + // + // Match key code of composition keydown event on windows. + // IE sends VK_PROCESSKEY which has value 229; + // + // Please refer to following documents for detals: + // - Virtual-Key Codes + // http://msdn.microsoft.com/en-us/library/ms645540(VS.85).aspx + // - How the IME System Works + // http://msdn.microsoft.com/en-us/library/cc194848.aspx + // - ImmGetVirtualKey Function + // http://msdn.microsoft.com/en-us/library/dd318570(VS.85).aspx + const int kCompositionEventKeyCode = 229; + + // If IME has filtered this event, then replace virtual key code with + // VK_PROCESSKEY. See comment in ProcessKeyEvent() for details. + // It's only required for keydown events. + // To emulate windows behavior, when input method is enabled, if the commit + // text can be emulated by a Char event, then don't do this replacement. + if (!NeedCommitByForwardingCharEvent()) { + wke->windowsKeyCode = kCompositionEventKeyCode; + // Prevent RenderView::UnhandledKeyboardEvent() from processing it. + // Otherwise unexpected result may occur. For example if it's a + // Backspace key event, the browser may go back to previous page. + if (wke->os_event) { + wke->os_event->keyval = GDK_VoidSymbol; + wke->os_event->state = 0; + } + } + host_view_->GetRenderWidgetHost()->ForwardKeyboardEvent(*wke); + } + + void ProcessUnfilteredKeyPressEvent(NativeWebKeyboardEvent* wke) { + RenderWidgetHost* host = host_view_->GetRenderWidgetHost(); + + // Send keydown event as it, because it's not filtered by IME. + host->ForwardKeyboardEvent(*wke); + + // IME is disabled by WebKit or the GtkIMContext object cannot handle + // this key event. + // This case is caused by two reasons: + // 1. The given key event is a control-key event, (e.g. return, page up, + // page down, tab, arrows, etc.) or; + // 2. The given key event is not a control-key event but printable + // characters aren't assigned to the event, (e.g. alt+d, etc.) + // Create a Char event manually from this key event and send it to the + // renderer when this Char event contains a printable character which + // should be processed by WebKit. + // isSystemKey will be set to true if this key event has Alt modifier, + // see WebInputEventFactory::keyboardEvent() for details. + if (wke->text[0]) { + wke->type = WebKit::WebInputEvent::Char; + host->ForwardKeyboardEvent(*wke); + } + } + + // Processes result returned from input method after filtering a key event. + // |filtered| indicates if the key event was filtered by the input method. + void ProcessInputMethodResult(const GdkEventKey* event, bool filtered) { + RenderWidgetHost* host = host_view_->GetRenderWidgetHost(); + bool committed = false; + // We do commit before preedit change, so that we can optimize some + // unnecessary preedit changes. + if (commit_text_.length()) { + if (filtered && NeedCommitByForwardingCharEvent()) { + // Send a Char event when we input a composed character without IMEs + // so that this event is to be dispatched to onkeypress() handlers, + // autofill, etc. + // Only commit text generated by a filtered key down event can be sent + // as a Char event, because a unfiltered key down event will probably + // generate another Char event. + // TODO(james.su@gmail.com): Is it necessary to support non BMP chars + // here? + NativeWebKeyboardEvent char_event(commit_text_[0], + event->state, + base::Time::Now().ToDoubleT()); + host->ForwardKeyboardEvent(char_event); + } else { + committed = true; + // Send an IME event. + // Unlike a Char event, an IME event is NOT dispatched to onkeypress() + // handlers or autofill. + host->ImeConfirmComposition(commit_text_); + // Set this flag to false, as this composition session has been + // finished. + is_composing_text_ = false; + } + } + + // Send preedit text only if it's changed. + // If a text has been committed, then we don't need to send the empty + // preedit text again. + if (is_preedit_changed_) { + if (preedit_text_.length()) { + host->ImeSetComposition(preedit_text_, preedit_cursor_position_, + -1, -1); + } else if (!committed) { + host->ImeCancelComposition(); + } + } + } + + void CompleteComposition() { + if (!is_enabled_) + return; + + // If WebKit requires to complete current composition, then we need commit + // existing preedit text and reset the GtkIMContext object. + + // Backup existing preedit text to avoid it's being cleared when resetting + // the GtkIMContext object. + string16 old_preedit_text = preedit_text_; + + // Clear it so that we can know if anything is committed by following + // line. + commit_text_.clear(); + + // Resetting the GtkIMContext. Input method may commit something at this + // point. In this case, we shall not commit the preedit text again. + gtk_im_context_reset(context_); + + // If nothing was committed by above line, then commit stored preedit text + // to prevent data loss. + if (old_preedit_text.length() && commit_text_.length() == 0) { + host_view_->GetRenderWidgetHost()->ImeConfirmComposition( + old_preedit_text); + } + + is_composing_text_ = false; + preedit_text_.clear(); + preedit_cursor_position_ = 0; + } + + // Real code of "commit" signal handler. + void HandleCommit(const string16& text) { + // Append the text to the buffer, because commit signal might be fired + // multiple times when processing a key event. + commit_text_.append(text); + // Nothing needs to do, if it's currently in ProcessKeyEvent() + // handler, which will send commit text to webkit later. Otherwise, + // we need send it here. + // It's possible that commit signal is fired without a key event, for + // example when user input via a voice or handwriting recognition software. + // In this case, the text must be committed directly. + if (!is_in_key_event_handler_) { + host_view_->GetRenderWidgetHost()->ImeConfirmComposition(text); + } + } + + // Real code of "preedit-start" signal handler. + void HandlePreeditStart() { + is_composing_text_ = true; + } + + // Real code of "preedit-changed" signal handler. + void HandlePreeditChanged(const string16& text, int cursor_position) { + bool changed = false; + // If preedit text or cursor position is not changed since last time, + // then it's not necessary to update it again. + // Preedit text is always stored, so that we can commit it when webkit + // requires. + // Don't set is_preedit_changed_ to false if there is no change, because + // this handler might be called multiple times with the same data. + if (cursor_position != preedit_cursor_position_ || text != preedit_text_) { + preedit_text_ = text; + preedit_cursor_position_ = cursor_position; + is_preedit_changed_ = true; + changed = true; + } + + // In case we are using a buggy input method which doesn't fire + // "preedit_start" signal. + if (text.length()) + is_composing_text_ = true; + + // Nothing needs to do, if it's currently in ProcessKeyEvent() + // handler, which will send preedit text to webkit later. + // Otherwise, we need send it here if it's been changed. + if (!is_in_key_event_handler_ && changed) { + if (text.length()) { + host_view_->GetRenderWidgetHost()->ImeSetComposition( + text, cursor_position, -1, -1); + } else { + host_view_->GetRenderWidgetHost()->ImeCancelComposition(); + } + } + } + + // Real code of "preedit-end" signal handler. + void HandlePreeditEnd() { + bool changed = false; + if (preedit_text_.length()) { + // The composition session has been finished. + preedit_text_.clear(); + preedit_cursor_position_ = 0; + is_preedit_changed_ = true; + changed = true; + } + + // If there is still a preedit text when firing "preedit-end" signal, + // we need inform webkit to clear it. + // It's only necessary when it's not in ProcessKeyEvent (). + if (!is_in_key_event_handler_ && changed) { + host_view_->GetRenderWidgetHost()->ImeCancelComposition(); + } + + // Don't set is_composing_text_ to false here, because "preedit_end" + // signal may be fired before "commit" signal. + } + + private: + // Signal handlers of GtkIMContext object. + static void HandleCommitThunk(GtkIMContext* context, gchar* text, + RenderWidgetHostViewGtkIMContext* self) { + self->HandleCommit(UTF8ToUTF16(text)); + } + + static void HandlePreeditStartThunk(GtkIMContext* context, + RenderWidgetHostViewGtkIMContext* self) { + self->HandlePreeditStart(); + } + + static void HandlePreeditChangedThunk( + GtkIMContext* context, RenderWidgetHostViewGtkIMContext* self) { + gchar* text = NULL; + gint cursor_position = 0; + gtk_im_context_get_preedit_string(context, &text, NULL, &cursor_position); + self->HandlePreeditChanged(UTF8ToUTF16(text), cursor_position); + g_free(text); + } + + static void HandlePreeditEndThunk(GtkIMContext* context, + RenderWidgetHostViewGtkIMContext* self) { + self->HandlePreeditEnd(); + } + + private: + // The parent object. + RenderWidgetHostViewGtk* host_view_; + + // The GtkIMContext object. + // In terms of the DOM event specification Appendix A + // <http://www.w3.org/TR/DOM-Level-3-Events/keyset.html>, + // GTK uses a GtkIMContext object for the following two purposes: + // 1. Composing Latin characters (A.1.2), and; + // 2. Composing CJK characters with an IME (A.1.3). + // Many JavaScript pages assume composed Latin characters are dispatched to + // their onkeypress() handlers but not dispatched CJK characters composed + // with an IME. To emulate this behavior, we should monitor the status of + // this GtkIMContext object and prevent sending Char events when a + // GtkIMContext object sends a "commit" signal with the CJK characters + // composed by an IME. + GtkIMContext* context_; + + // A GtkIMContextSimple object, for supporting dead/compose keys when input + // method is disabled, eg. in password input box. + GtkIMContext* context_simple_; + + // Whether or not this widget is focused. + bool is_focused_; + + // Whether or not the above GtkIMContext is composing a text with an IME. + // This flag is used in "commit" signal handler of the GtkIMContext object, + // which determines how to submit the result text to WebKit according to this + // flag. + // If this flag is true or there are more than one characters in the result, + // then the result text will be committed to WebKit as a confirmed + // composition. Otherwise, it'll be forwarded as a key event. + // + // The GtkIMContext object sends a "preedit_start" before it starts composing + // a text and a "preedit_end" signal after it finishes composing it. + // "preedit_start" signal is monitored to turn it on. + // We don't monitor "preedit_end" signal to turn it off, because an input + // method may fire "preedit_end" signal before "commit" signal. + // A buggy input method may not fire "preedit_start" and/or "preedit_end" + // at all, so this flag will also be set to true when "preedit_changed" signal + // is fired with non-empty preedit text. + bool is_composing_text_; + + // Whether or not the IME is enabled. + // This flag is actually controlled by RenderWidget. + // It shall be set to false when an ImeUpdateStatus message with control == + // IME_DISABLE is received, and shall be set to true if control == + // IME_COMPLETE_COMPOSITION or IME_MOVE_WINDOWS. + // When this flag is false, keyboard events shall be dispatched directly + // instead of sending to context_. + bool is_enabled_; + + // Whether or not it's currently running inside key event handler. + // If it's true, then preedit-changed and commit handler will backup the + // preedit or commit text instead of sending them down to webkit. + // key event handler will send them later. + bool is_in_key_event_handler_; + + // Stores a copy of the most recent preedit text retrieved from context_. + // When an ImeUpdateStatus message with control == IME_COMPLETE_COMPOSITION + // is received, this stored preedit text (if not empty) shall be committed, + // and context_ shall be reset. + string16 preedit_text_; + + // Stores the cursor position in the stored preedit text. + int preedit_cursor_position_; + + // Whether or not the preedit has been changed since last key event. + bool is_preedit_changed_; + + // Stores a copy of the most recent commit text received by commit signal + // handler. + string16 commit_text_; + + DISALLOW_COPY_AND_ASSIGN(RenderWidgetHostViewGtkIMContext); +}; + // This class is a simple convenience wrapper for Gtk functions. It has only // static methods. class RenderWidgetHostViewGtkWidget { @@ -78,16 +641,9 @@ class RenderWidgetHostViewGtkWidget { g_signal_connect_after(widget, "scroll-event", G_CALLBACK(MouseScrollEvent), host_view); - // Create a GtkIMContext instance and attach its signal handlers. - host_view->im_context_ = gtk_im_multicontext_new(); - g_signal_connect(host_view->im_context_, "preedit_start", - G_CALLBACK(InputMethodPreeditStart), host_view); - g_signal_connect(host_view->im_context_, "preedit_end", - G_CALLBACK(InputMethodPreeditEnd), host_view); - g_signal_connect(host_view->im_context_, "preedit_changed", - G_CALLBACK(InputMethodPreeditChanged), host_view); - g_signal_connect(host_view->im_context_, "commit", - G_CALLBACK(InputMethodCommit), host_view); + // Create GtkIMContext wrapper object. + host_view->im_context_.reset( + new RenderWidgetHostViewGtkIMContext(host_view)); return widget; } @@ -114,44 +670,8 @@ class RenderWidgetHostViewGtkWidget { // allows us to release our keyboard grab. host_view->host_->Shutdown(); } else { - NativeWebKeyboardEvent wke(event); - host_view->GetRenderWidgetHost()->ForwardKeyboardEvent(wke); - } - - // Save the current modifier-key state before dispatching this event to the - // GtkIMContext object so its event handlers can use this state to create - // Char events. - host_view->im_modifier_state_ = event->state; - - // Dispatch this event to the GtkIMContext object. - // It sends a "commit" signal when it has a character to be inserted - // even when we use a US keyboard so that we can send a Char event - // (or an IME event) to the renderer in our "commit"-signal handler. - // We should send a KeyDown (or a KeyUp) event before dispatching this - // event to the GtkIMContext object (and send a Char event) so that WebKit - // can dispatch the JavaScript events in the following order: onkeydown(), - // onkeypress(), and onkeyup(). (Many JavaScript pages assume this.) - // TODO(hbono): we should not dispatch a key event when the input focus - // is in a password input? - if (!gtk_im_context_filter_keypress(host_view->im_context_, event)) { - // The GtkIMContext object cannot handle this key event. - // This case is caused by two reasons: - // 1. The given key event is a control-key event, (e.g. return, page up, - // page down, tab, arrows, etc.) or; - // 2. The given key event is not a control-key event but printable - // characters aren't assigned to the event, (e.g. alt+d, etc.) - // Create a Char event manually from this key event and send it to the - // renderer when this Char event contains a printable character which - // should be processed by WebKit. - // TODO(hbono): Windows Chrome sends a Char event with its isSystemKey - // value true for the above case 2. We should emulate this behavior? - if (event->type == GDK_KEY_PRESS && - !gdk_keyval_to_unicode(event->keyval)) { - NativeWebKeyboardEvent wke(event); - wke.type = WebKit::WebInputEvent::Char; - if (wke.text[0]) - host_view->GetRenderWidgetHost()->ForwardKeyboardEvent(wke); - } + // Send key event to input method. + host_view->im_context_->ProcessKeyEvent(event); } // We return TRUE because we did handle the event. If it turns out webkit @@ -188,14 +708,10 @@ class RenderWidgetHostViewGtkWidget { host_view->ShowCurrentCursor(); host_view->GetRenderWidgetHost()->Focus(); - // Notify the GtkIMContext object of this focus-in event and - // attach this GtkIMContext object to this window. - // We should call gtk_im_context_set_client_window() only when this window - // gain (or release) the window focus because an immodule may reset its - // internal status when processing this function. - gtk_im_context_focus_in(host_view->im_context_); - gtk_im_context_set_client_window(host_view->im_context_, - host_view->native_view()->window); + // The only way to enable a GtkIMContext object is to call its focus in + // handler. + host_view->im_context_->OnFocusIn(); + return FALSE; } @@ -211,10 +727,9 @@ class RenderWidgetHostViewGtkWidget { if (!host_view->is_showing_context_menu_) host_view->GetRenderWidgetHost()->Blur(); - // Notify the GtkIMContext object of this focus-in event and - // detach this GtkIMContext object from this window. - gtk_im_context_focus_out(host_view->im_context_); - gtk_im_context_set_client_window(host_view->im_context_, NULL); + // Disable the GtkIMContext object. + host_view->im_context_->OnFocusOut(); + return FALSE; } @@ -224,10 +739,14 @@ class RenderWidgetHostViewGtkWidget { // shown, and must rely on this signal instead. static void OnGrabNotify(GtkWidget* widget, gboolean was_grabbed, RenderWidgetHostViewGtk* host_view) { - if (was_grabbed) - OnFocusIn(widget, NULL, host_view); - else - OnFocusOut(widget, NULL, host_view); + if (was_grabbed) { + if (host_view->was_focused_before_grab_) + OnFocusIn(widget, NULL, host_view); + } else { + host_view->was_focused_before_grab_ = host_view->HasFocus(); + if (host_view->was_focused_before_grab_) + OnFocusOut(widget, NULL, host_view); + } } static gboolean ButtonPressReleaseEvent( @@ -309,69 +828,6 @@ class RenderWidgetHostViewGtkWidget { return FALSE; } - static void InputMethodCommit(GtkIMContext* im_context, - gchar* text, - RenderWidgetHostViewGtk* host_view) { - const string16& im_text = UTF8ToUTF16(text); - if (!host_view->im_is_composing_cjk_text_ && im_text.length() == 1) { - // Send a Char event when we input a composed character without IMEs so - // that this event is to be dispatched to onkeypress() handlers, - // autofill, etc. - ForwardCharEvent(host_view, im_text[0]); - } else { - // Send an IME event. - // Unlike a Char event, an IME event is NOT dispatched to onkeypress() - // handlers or autofill. - host_view->GetRenderWidgetHost()->ImeConfirmComposition(im_text); - } - } - - static void InputMethodPreeditStart(GtkIMContext* im_context, - RenderWidgetHostViewGtk* host_view) { - // Start monitoring IME events of the renderer. - // TODO(hbono): a renderer sends these IME events not only for sending the - // caret position, but also for enabling/disabling IMEs. If we need to - // enable/disable IMEs, we should move this code to a better place. - // (This signal handler is called only when an IME is enabled. So, once - // we disable an IME, we cannot receive any IME events from the renderer, - // i.e. we cannot re-enable the IME any longer.) - host_view->GetRenderWidgetHost()->ImeSetInputMode(true); - host_view->im_is_composing_cjk_text_ = true; - } - - static void InputMethodPreeditEnd(GtkIMContext* im_context, - RenderWidgetHostViewGtk* host_view) { - // End monitoring IME events. - host_view->GetRenderWidgetHost()->ImeSetInputMode(false); - host_view->im_is_composing_cjk_text_ = false; - } - - static void InputMethodPreeditChanged(GtkIMContext* im_context, - RenderWidgetHostViewGtk* host_view) { - // Send an IME event to update the composition node of the renderer. - // TODO(hbono): an IME intercepts all key events while composing a text, - // i.e. we cannot receive any GDK_KEY_PRESS (or GDK_KEY_UP) events. - // Should we send pseudo KeyDown (and KeyUp) events to emulate Windows? - gchar* preedit_text = NULL; - gint cursor_position = 0; - gtk_im_context_get_preedit_string(im_context, &preedit_text, NULL, - &cursor_position); - host_view->GetRenderWidgetHost()->ImeSetComposition( - UTF8ToUTF16(preedit_text), cursor_position, -1, -1); - g_free(preedit_text); - } - - static void ForwardCharEvent(RenderWidgetHostViewGtk* host_view, - wchar_t im_character) { - if (!im_character) - return; - - NativeWebKeyboardEvent char_event(im_character, - host_view->im_modifier_state_, - base::Time::Now().ToDoubleT()); - host_view->GetRenderWidgetHost()->ForwardKeyboardEvent(char_event); - } - DISALLOW_IMPLICIT_CONSTRUCTORS(RenderWidgetHostViewGtkWidget); }; @@ -390,15 +846,11 @@ RenderWidgetHostViewGtk::RenderWidgetHostViewGtk(RenderWidgetHost* widget_host) parent_host_view_(NULL), parent_(NULL), is_popup_first_mouse_release_(true), - im_context_(NULL), - im_is_composing_cjk_text_(false), - im_modifier_state_(0) { + was_focused_before_grab_(false) { host_->set_view(this); } RenderWidgetHostViewGtk::~RenderWidgetHostViewGtk() { - if (im_context_) - g_object_unref(im_context_); view_.Destroy(); } @@ -559,28 +1011,7 @@ void RenderWidgetHostViewGtk::SetIsLoading(bool is_loading) { void RenderWidgetHostViewGtk::IMEUpdateStatus(int control, const gfx::Rect& caret_rect) { - // The renderer has updated its IME status. - // Control the GtkIMContext object according to this status. - if (!im_context_) - return; - - if (control == IME_DISABLE) { - // TODO(hbono): this code just resets the GtkIMContext object. - // Should we prevent sending key events to the GtkIMContext object - // (or unref it) when we disable IMEs? - gtk_im_context_reset(im_context_); - gtk_im_context_set_cursor_location(im_context_, NULL); - } else { - // TODO(hbono): we should finish (not reset) an ongoing composition - // when |control| is IME_COMPLETE_COMPOSITION. - - // Updates the position of the IME candidate window. - // The position sent from the renderer is a relative one, so we need to - // attach the GtkIMContext object to this window before changing the - // position. - GdkRectangle cursor_rect(caret_rect.ToGdkRectangle()); - gtk_im_context_set_cursor_location(im_context_, &cursor_rect); - } + im_context_->UpdateStatus(control, caret_rect); } void RenderWidgetHostViewGtk::DidPaintRect(const gfx::Rect& rect) { diff --git a/chrome/browser/renderer_host/render_widget_host_view_gtk.h b/chrome/browser/renderer_host/render_widget_host_view_gtk.h index d4a40ef..eccea4a 100644 --- a/chrome/browser/renderer_host/render_widget_host_view_gtk.h +++ b/chrome/browser/renderer_host/render_widget_host_view_gtk.h @@ -9,6 +9,7 @@ #include <vector> #include "base/gfx/native_widget_types.h" +#include "base/scoped_ptr.h" #include "chrome/browser/renderer_host/render_widget_host_view.h" #include "chrome/common/owned_widget_gtk.h" #include "chrome/common/render_messages.h" @@ -16,6 +17,8 @@ #include "webkit/glue/webcursor.h" class RenderWidgetHost; +// A conveience wrapper class for GtkIMContext; +class RenderWidgetHostViewGtkIMContext; typedef struct _GtkClipboard GtkClipboard; typedef struct _GtkSelectionData GtkSelectionData; @@ -116,32 +119,12 @@ class RenderWidgetHostViewGtk : public RenderWidgetHostView { // stay open. bool is_popup_first_mouse_release_; - // The GtkIMContext object. - // In terms of the DOM event specification Appendix A - // <http://www.w3.org/TR/DOM-Level-3-Events/keyset.html>, - // GTK uses a GtkIMContext object for the following two purposes: - // 1. Composing Latin characters (A.1.2), and; - // 2. Composing CJK characters with an IME (A.1.3). - // Many JavaScript pages assume composed Latin characters are dispatched to - // their onkeypress() handlers but not dispatched CJK characters composed - // with an IME. To emulate this behavior, we should monitor the status of - // this GtkIMContext object and prevent sending Char events when a - // GtkIMContext object sends a "commit" signal with the CJK characters - // composed by an IME. - GtkIMContext* im_context_; - - // Whether or not the above GtkIMContext is composing a CJK text with an IME. - // The GtkIMContext object sends a "preedit_start" before it starts composing - // a CJK text and a "preedit_end" signal after it finishes composing it. - // On the other hand, the GtkIMContext object doesn't send them when - // composing Latin texts. So, we monitor the above signals to check whether - // or not the GtkIMContext object is composing a CJK text. - bool im_is_composing_cjk_text_; - - // Represents the current modifier-key state. - // This state is used when GtkIMContext signal handlers create Char events - // because they don't use the GdkEventKey objects and cannot get the state. - int im_modifier_state_; + // Whether or not this widget was focused before shadowed by another widget. + // Used in OnGrabNotify() handler to track the focused state correctly. + bool was_focused_before_grab_; + + // A conveience wrapper object for GtkIMContext; + scoped_ptr<RenderWidgetHostViewGtkIMContext> im_context_; // Helper class that lets us allocate plugin containers and move them. GtkPluginContainerManager plugin_container_manager_; |