[GTK] Rework IME handling to fix bugs and prepare for WebKit2
authormrobinson@webkit.org <mrobinson@webkit.org@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Fri, 4 May 2012 16:49:39 +0000 (16:49 +0000)
committermrobinson@webkit.org <mrobinson@webkit.org@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Fri, 4 May 2012 16:49:39 +0000 (16:49 +0000)
https://bugs.webkit.org/show_bug.cgi?id=84556

Reviewed by Gustavo Noronha Silva.

Source/WebCore:

No new tests. This change is already covered by a suite of keyboard
handling unit tests in WebKitGTK+. There are some changes in behavior,
but they are difficult to test without mocking out an entire GtkIMContext.

Add a struct, CompositionResults, which is used by PlatformKeyboardEvent
to package composition information with a keyboard event. Also add some logic
to PlatformKeyboardEvent to give the right information when it has composition
results.

* GNUmakefile.list.am: Added new sources to the list.
* platform/PlatformKeyboardEvent.h:  Added a new CompositionResults member,
getter, and argument to the constructor.
* platform/gtk/CompositionResults.h: Added.
* platform/gtk/GtkInputMethodFilter.cpp: Added.
* platform/gtk/GtkInputMethodFilter.h: Added.
* platform/gtk/PlatformKeyboardEventGtk.cpp:
(WebCore::PlatformKeyboardEvent::windowsKeyCodeForGdkKeyCode): When
the key value is void return the VK_PROCESS keycode, which is the keycode
that web content expects with keystrokes that trigger composition events.
(WebCore::eventTypeForGdkKeyEvent): Abstract out this helper.
(WebCore::modifiersForGdkKeyEvent): Abstract out this helper.
(WebCore::PlatformKeyboardEvent::PlatformKeyboardEvent): When a PlatformKeyEvent
has composition results, use VK_PROCESS as the keycode for this event.
(WebCore::PlatformKeyboardEvent::disambiguateKeyDownEvent): When this event is
transformed into a Char event, the PlatformKeyboardEvent used for DOM keypress
events, and it has composition results clear the text members. This forces the
EventHandler code to drop the keypress event. Platform events that change the
composition states do not have corresponding keypress DOM events (only keydown
and keyup events), so this is necessary to ensure web compatibility.

Source/WebKit/gtk:

Rework input method handling logic into a class called GtkInputMethodFilter.
This filter now runs before WebCore event handling, allowing the code to more
easily fake simple compositions that should be seen as keystrokes. We can also
filter keypresses that should not go to web content at all, such as key up events
related to key down events that were filtered.

Also added is a WebViewInputMethodFilter which is a concrete implementation of
GtkInputMethodFilter. This class contains logic for actually sending events to
WebCore. In WebKit2 an implementation of GtkInputMethodFilter will send events
across the IPC channel.

* GNUmakefile.am: Add new files to the source list.
* WebCoreSupport/ContextMenuClientGtk.cpp:
(WebKit::inputMethodsMenuItem): Access the input method context via the filter.
* WebCoreSupport/EditorClientGtk.cpp: Remove the tricky logic of input method
events from this class, because it's now in the GtkInputMethodFilter.
(WebKit::EditorClient::setInputMethodState): Call into the filter.
(WebKit::EditorClient::shouldBeginEditing): We no longer need to update the composition here.
This is handled by the focus in and focus out logic in the filter.
(WebKit::EditorClient::shouldEndEditing): Ditto.
(WebKit::EditorClient::respondToChangedSelection): Call into the filter now.
(WebKit::EditorClient::handleInputMethodKeyboardEvent): Added this helper which executes
any pending composition confirmation or preedit update actions as the default action of
the keydown event.
(WebKit::EditorClient::handleKeyboardEvent): Call handleInputMethodKeyboardEvent to do
any pending composition action.
(WebKit::EditorClient::handleInputMethodKeydown): Remove all the logic from this method.
Keys are filtered before they are sent to WebCore now and the actual action of input method
events happens in the keydown default action to increase compatibility with other browsers.
(WebKit::EditorClient::EditorClient): Remove context signal management.
(WebKit::EditorClient::~EditorClient): Ditto.
* WebCoreSupport/EditorClientGtk.h:
(EditorClient): No longer has some members that tracked IME status.
* WebCoreSupport/WebViewInputMethodFilter.cpp: Added.
* WebCoreSupport/WebViewInputMethodFilter.h: Added.
* webkit/webkitwebview.cpp:
(webkit_web_view_get_property): Get the context from the filter now.
(webkit_web_view_key_press_event): Just send events straight to the filter.
The filter will decide whether or not to send them to WebCore.
(webkit_web_view_key_release_event): Ditto.
(webkit_web_view_button_press_event): Use the filter to handle button press
events related to IME.
(webkit_web_view_focus_in_event): Notify the filter now.
(webkit_web_view_focus_out_event): Ditto.
(webkit_web_view_realize): The filter takes care of listening for realize now.
(webkit_web_view_init): Set the WebView widget on the filter.
* webkit/webkitwebviewprivate.h: Change the GtkIMContext member to be a GtkInputMethodFilter member.

git-svn-id: https://svn.webkit.org/repository/webkit/trunk@116114 268f45cc-cd09-0410-ab3c-d52691b4dbfc

16 files changed:
Source/WebCore/ChangeLog
Source/WebCore/GNUmakefile.list.am
Source/WebCore/platform/PlatformKeyboardEvent.h
Source/WebCore/platform/gtk/CompositionResults.h [new file with mode: 0644]
Source/WebCore/platform/gtk/GtkInputMethodFilter.cpp [new file with mode: 0644]
Source/WebCore/platform/gtk/GtkInputMethodFilter.h [new file with mode: 0644]
Source/WebCore/platform/gtk/PlatformKeyboardEventGtk.cpp
Source/WebKit/gtk/ChangeLog
Source/WebKit/gtk/GNUmakefile.am
Source/WebKit/gtk/WebCoreSupport/ContextMenuClientGtk.cpp
Source/WebKit/gtk/WebCoreSupport/EditorClientGtk.cpp
Source/WebKit/gtk/WebCoreSupport/EditorClientGtk.h
Source/WebKit/gtk/WebCoreSupport/WebViewInputMethodFilter.cpp [new file with mode: 0644]
Source/WebKit/gtk/WebCoreSupport/WebViewInputMethodFilter.h [new file with mode: 0644]
Source/WebKit/gtk/webkit/webkitwebview.cpp
Source/WebKit/gtk/webkit/webkitwebviewprivate.h

index 149b313..f9174e1 100644 (file)
@@ -1,3 +1,40 @@
+2012-05-03  Martin Robinson  <mrobinson@igalia.com>
+
+        [GTK] Rework IME handling to fix bugs and prepare for WebKit2
+        https://bugs.webkit.org/show_bug.cgi?id=84556
+
+        Reviewed by Gustavo Noronha Silva.
+
+        No new tests. This change is already covered by a suite of keyboard
+        handling unit tests in WebKitGTK+. There are some changes in behavior,
+        but they are difficult to test without mocking out an entire GtkIMContext.
+
+        Add a struct, CompositionResults, which is used by PlatformKeyboardEvent
+        to package composition information with a keyboard event. Also add some logic
+        to PlatformKeyboardEvent to give the right information when it has composition
+        results.
+
+        * GNUmakefile.list.am: Added new sources to the list.
+        * platform/PlatformKeyboardEvent.h:  Added a new CompositionResults member,
+        getter, and argument to the constructor.
+        * platform/gtk/CompositionResults.h: Added.
+        * platform/gtk/GtkInputMethodFilter.cpp: Added.
+        * platform/gtk/GtkInputMethodFilter.h: Added.
+        * platform/gtk/PlatformKeyboardEventGtk.cpp:
+        (WebCore::PlatformKeyboardEvent::windowsKeyCodeForGdkKeyCode): When
+        the key value is void return the VK_PROCESS keycode, which is the keycode
+        that web content expects with keystrokes that trigger composition events.
+        (WebCore::eventTypeForGdkKeyEvent): Abstract out this helper.
+        (WebCore::modifiersForGdkKeyEvent): Abstract out this helper.
+        (WebCore::PlatformKeyboardEvent::PlatformKeyboardEvent): When a PlatformKeyEvent
+        has composition results, use VK_PROCESS as the keycode for this event.
+        (WebCore::PlatformKeyboardEvent::disambiguateKeyDownEvent): When this event is
+        transformed into a Char event, the PlatformKeyboardEvent used for DOM keypress
+        events, and it has composition results clear the text members. This forces the
+        EventHandler code to drop the keypress event. Platform events that change the
+        composition states do not have corresponding keypress DOM events (only keydown
+        and keyup events), so this is necessary to ensure web compatibility.
+
 2012-05-04  Jochen Eisinger  <jochen@chromium.org>
 
         Correctly update the outgoing referrer when navigating back from an history item created by pushState/replaceState
index 7b85fb4..3e95663 100644 (file)
@@ -4709,6 +4709,8 @@ webcoregtk_sources += \
        Source/WebCore/platform/gtk/GtkClickCounter.h \
        Source/WebCore/platform/gtk/GtkDragAndDropHelper.cpp \
        Source/WebCore/platform/gtk/GtkDragAndDropHelper.h \
+       Source/WebCore/platform/gtk/GtkInputMethodFilter.cpp \
+       Source/WebCore/platform/gtk/GtkInputMethodFilter.h \
        Source/WebCore/platform/gtk/GtkUtilities.cpp \
        Source/WebCore/platform/gtk/GtkUtilities.h \
        Source/WebCore/platform/gtk/GOwnPtrGtk.cpp \
index 1c1de65..42577f9 100644 (file)
@@ -43,6 +43,7 @@ typedef long LPARAM;
 
 #if PLATFORM(GTK)
 typedef struct _GdkEventKey GdkEventKey;
+#include "CompositionResults.h"
 #endif
 
 #if PLATFORM(QT)
@@ -151,8 +152,9 @@ namespace WebCore {
 #endif
 
 #if PLATFORM(GTK)
-        PlatformKeyboardEvent(GdkEventKey*);
-        GdkEventKey* gdkEventKey() const;
+        PlatformKeyboardEvent(GdkEventKey*, const CompositionResults&);
+        GdkEventKey* gdkEventKey() const { return m_gdkEventKey; }
+        const CompositionResults& compositionResults() const { return m_compositionResults; }
 
         // Used by WebKit2
         static String keyIdentifierForGdkKeyCode(unsigned);
@@ -200,6 +202,7 @@ namespace WebCore {
 #endif
 #if PLATFORM(GTK)
         GdkEventKey* m_gdkEventKey;
+        CompositionResults m_compositionResults;
 #endif
 #if PLATFORM(QT)
         QKeyEvent* m_qtEvent;
diff --git a/Source/WebCore/platform/gtk/CompositionResults.h b/Source/WebCore/platform/gtk/CompositionResults.h
new file mode 100644 (file)
index 0000000..87f5bf7
--- /dev/null
@@ -0,0 +1,71 @@
+/*
+ * Copyright (C) 2012 Igalia S.L.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in the
+ *    documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE COMPUTER, INC. ``AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL APPLE COMPUTER, INC. OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
+ * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 
+ */
+
+#ifndef CompositionResults_h
+#define CompositionResults_h
+
+#include <wtf/text/WTFString.h>
+
+namespace WebCore {
+
+struct CompositionResults {
+    CompositionResults()
+        : preeditCursorOffset(0)
+    {
+    }
+
+    CompositionResults(String simpleString)
+        : simpleString(simpleString)
+        , preeditCursorOffset(0)
+    {
+    }
+
+    CompositionResults(String confirmedComposition, String preedit, int preeditCursorOffset)
+        : confirmedComposition(confirmedComposition)
+        , preedit(preedit)
+        , preeditCursorOffset(preeditCursorOffset)
+    {
+    }
+
+    bool compositionUpdated() const
+    {
+        return !confirmedComposition.isNull() || !preedit.isNull();
+    }
+
+    // Some simple input methods return a string for all keyboard events. This
+    // value should be treated as the string representation of the keycode.
+    String simpleString;
+
+    // If the input method had a "real" composition it will be stored here.
+    String confirmedComposition;
+
+    // The event may have caused the preedit to update.
+    String preedit;
+    int preeditCursorOffset;
+};
+
+}
+
+#endif // CompositionResults_h
diff --git a/Source/WebCore/platform/gtk/GtkInputMethodFilter.cpp b/Source/WebCore/platform/gtk/GtkInputMethodFilter.cpp
new file mode 100644 (file)
index 0000000..969c1e9
--- /dev/null
@@ -0,0 +1,329 @@
+/*
+ *  Copyright (C) 2012 Igalia S.L.
+ *
+ *  This library is free software; you can redistribute it and/or
+ *  modify it under the terms of the GNU Lesser General Public
+ *  License as published by the Free Software Foundation; either
+ *  version 2 of the License, or (at your option) any later version.
+ *
+ *  This library is distributed in the hope that it will be useful,
+ *  but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ *  Lesser General Public License for more details.
+ *
+ *  You should have received a copy of the GNU Lesser General Public
+ *  License along with this library; if not, write to the Free Software
+ *  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
+ */
+
+#include "config.h"
+#include "GtkInputMethodFilter.h"
+
+#include "GOwnPtrGtk.h"
+#include "GtkVersioning.h"
+#include <gdk/gdkkeysyms.h>
+#include <gtk/gtk.h>
+#include <wtf/MathExtras.h>
+
+// The Windows composition key event code is 299 or VK_PROCESSKEY. We need to
+// emit this code for web compatibility reasons when key events trigger
+// composition results. GDK doesn't have an equivalent, so we send VoidSymbol
+// here to WebCore. PlatformKeyEvent knows to convert this code into
+// VK_PROCESSKEY.
+const int gCompositionEventKeyCode = GDK_KEY_VoidSymbol;
+
+namespace WebCore {
+
+static void handleCommitCallback(GtkIMContext*, const char* compositionString, GtkInputMethodFilter* filter)
+{
+    filter->handleCommit(compositionString);
+}
+
+static void handlePreeditStartCallback(GtkIMContext*, GtkInputMethodFilter* filter)
+{
+    filter->handlePreeditStart();
+}
+
+static void handlePreeditChangedCallback(GtkIMContext*, GtkInputMethodFilter* filter)
+{
+    filter->handlePreeditChanged();
+}
+
+static void handlePreeditEndCallback(GtkIMContext*, GtkInputMethodFilter* filter)
+{
+    filter->handlePreeditEnd();
+}
+
+static void handleWidgetRealize(GtkWidget* widget, GtkInputMethodFilter* filter)
+{
+    GdkWindow* window = gtk_widget_get_window(widget);
+    ASSERT(window);
+    gtk_im_context_set_client_window(filter->context(), window);
+}
+
+void GtkInputMethodFilter::setWidget(GtkWidget* widget)
+{
+    ASSERT(!m_widget);
+
+    m_widget = widget;
+    if (GdkWindow* window = gtk_widget_get_window(m_widget))
+        handleWidgetRealize(m_widget, this);
+    else
+        g_signal_connect_after(widget, "realize", G_CALLBACK(handleWidgetRealize), this);
+}
+
+GtkInputMethodFilter::GtkInputMethodFilter()
+    : m_cursorOffset(0)
+    , m_context(adoptGRef(gtk_im_multicontext_new()))
+    , m_widget(0)
+    , m_enabled(false)
+    , m_composingTextCurrently(false)
+    , m_filteringKeyEvent(false)
+    , m_preeditChanged(false)
+    , m_preventNextCommit(false)
+    , m_justSentFakeKeyUp(false)
+    , m_lastFilteredKeyPressCodeWithNoResults(GDK_KEY_VoidSymbol)
+{
+    g_signal_connect(m_context.get(), "commit", G_CALLBACK(handleCommitCallback), this);
+    g_signal_connect(m_context.get(), "preedit-start", G_CALLBACK(handlePreeditStartCallback), this);
+    g_signal_connect(m_context.get(), "preedit-changed", G_CALLBACK(handlePreeditChangedCallback), this);
+    g_signal_connect(m_context.get(), "preedit-end", G_CALLBACK(handlePreeditEndCallback), this);
+}
+
+GtkInputMethodFilter::~GtkInputMethodFilter()
+{
+    g_signal_handlers_disconnect_by_func(m_context.get(), reinterpret_cast<void*>(handleCommitCallback), this);
+    g_signal_handlers_disconnect_by_func(m_context.get(), reinterpret_cast<void*>(handlePreeditStartCallback), this);
+    g_signal_handlers_disconnect_by_func(m_context.get(), reinterpret_cast<void*>(handlePreeditChangedCallback), this);
+    g_signal_handlers_disconnect_by_func(m_context.get(), reinterpret_cast<void*>(handlePreeditEndCallback), this);
+    g_signal_handlers_disconnect_by_func(m_widget, reinterpret_cast<void*>(handleWidgetRealize), this);
+}
+
+void GtkInputMethodFilter::setEnabled(bool enabled)
+{
+    m_enabled = enabled;
+    if (enabled)
+        gtk_im_context_focus_in(m_context.get());
+    else
+        gtk_im_context_focus_out(m_context.get());
+}
+
+bool GtkInputMethodFilter::filterKeyEvent(GdkEventKey* event)
+{
+    if (!canEdit() || !m_enabled)
+        return sendSimpleKeyEvent(event);
+
+    m_preeditChanged = false;
+    m_filteringKeyEvent = true;
+
+    unsigned int lastFilteredKeyPressCodeWithNoResults = m_lastFilteredKeyPressCodeWithNoResults;
+    m_lastFilteredKeyPressCodeWithNoResults = GDK_KEY_VoidSymbol;
+
+    bool filtered = gtk_im_context_filter_keypress(m_context.get(), event);
+    m_filteringKeyEvent = false;
+
+    bool justSentFakeKeyUp = m_justSentFakeKeyUp;
+    m_justSentFakeKeyUp = false;
+    if (justSentFakeKeyUp && event->type == GDK_KEY_RELEASE)
+        return true;
+
+    // Simple input methods work such that even normal keystrokes fire the
+    // commit signal. We detect those situations and treat them as normal
+    // key events, supplying the commit string as the key character.
+    if (filtered && !m_composingTextCurrently && !m_preeditChanged && m_confirmedComposition.length() == 1) {
+        bool result = sendSimpleKeyEvent(event, m_confirmedComposition);
+        m_confirmedComposition = String();
+        return result;
+    }
+
+    if (filtered && event->type == GDK_KEY_PRESS) {
+        if (!m_preeditChanged && m_confirmedComposition.isNull()) {
+            m_composingTextCurrently = true;
+            m_lastFilteredKeyPressCodeWithNoResults = event->keyval;
+            return true;
+        }
+
+        bool result = sendKeyEventWithCompositionResults(event);
+        if (!m_confirmedComposition.isEmpty()) {
+            m_composingTextCurrently = false;
+            m_confirmedComposition = String();
+        }
+        return result;
+    }
+
+    // If we previously filtered a key press event and it yielded no results. Suppress
+    // the corresponding key release event to avoid confusing the web content.
+    if (event->type == GDK_KEY_RELEASE && lastFilteredKeyPressCodeWithNoResults == event->keyval)
+        return true;
+
+    // At this point a keystroke was either:
+    // 1. Unfiltered
+    // 2. A filtered keyup event. As the IME code in EditorClient.h doesn't
+    //    ever look at keyup events, we send any composition results before
+    //    the key event.
+    // Both might have composition results or not.
+    //
+    // It's important to send the composition results before the event
+    // because some IM modules operate that way. For example (taken from
+    // the Chromium source), the latin-post input method gives this sequence
+    // when you press 'a' and then backspace:
+    //  1. keydown 'a' (filtered)
+    //  2. preedit changed to "a"
+    //  3. keyup 'a' (unfiltered)
+    //  4. keydown Backspace (unfiltered)
+    //  5. commit "a"
+    //  6. preedit end
+    if (!m_confirmedComposition.isEmpty())
+        confirmComposition();
+    if (m_preeditChanged)
+        updatePreedit();
+    return sendSimpleKeyEvent(event);
+}
+
+void GtkInputMethodFilter::notifyMouseButtonPress()
+{
+    // Confirming the composition may trigger a selection change, which
+    // might trigger further unwanted actions on the context, so we prevent
+    // that by setting m_composingTextCurrently to false.
+    m_composingTextCurrently = false;
+    confirmCurrentComposition();
+    cancelContextComposition();
+}
+
+void GtkInputMethodFilter::resetContext()
+{
+
+    // We always cancel the current WebCore composition here, in case the
+    // composition was set outside the GTK+ IME path (via a script, for
+    // instance) and we aren't tracking it.
+    cancelCurrentComposition();
+
+    if (!m_composingTextCurrently)
+        return;
+    m_composingTextCurrently = false;
+    cancelContextComposition();
+}
+
+void GtkInputMethodFilter::cancelContextComposition()
+{
+    m_preventNextCommit = !m_preedit.isEmpty();
+
+    gtk_im_context_reset(m_context.get());
+
+    m_composingTextCurrently = false;
+    m_justSentFakeKeyUp = false;
+    m_preedit = String();
+    m_confirmedComposition = String();
+}
+
+void GtkInputMethodFilter::notifyFocusedIn()
+{
+    m_enabled = true;
+    gtk_im_context_focus_in(m_context.get());
+}
+
+void GtkInputMethodFilter::notifyFocusedOut()
+{
+    if (!m_enabled)
+        return;
+
+    m_composingTextCurrently = false;
+    confirmCurrentComposition();
+    cancelContextComposition();
+    gtk_im_context_focus_out(m_context.get());
+    m_enabled = false;
+}
+
+void GtkInputMethodFilter::confirmComposition()
+{
+    confirmCompositionText(m_confirmedComposition);
+    m_confirmedComposition = String();
+}
+
+void GtkInputMethodFilter::updatePreedit()
+{
+    setPreedit(m_preedit, m_cursorOffset);
+    m_preeditChanged = false;
+}
+
+void GtkInputMethodFilter::sendCompositionAndPreeditWithFakeKeyEvents(ResultsToSend resultsToSend)
+{
+    GOwnPtr<GdkEvent> event(gdk_event_new(GDK_KEY_PRESS));
+    event->key.time = GDK_CURRENT_TIME;
+    event->key.keyval = gCompositionEventKeyCode;
+    sendKeyEventWithCompositionResults(&event->key, resultsToSend);
+
+    m_confirmedComposition = String();
+    m_composingTextCurrently = false;
+
+    event->type = GDK_KEY_RELEASE;
+    sendSimpleKeyEvent(&event->key);
+    m_justSentFakeKeyUp = true;
+}
+
+void GtkInputMethodFilter::handleCommit(const char* compositionString)
+{
+    if (m_preventNextCommit) {
+        m_preventNextCommit = false;
+        return;
+    }
+
+    if (!m_enabled)
+        return;
+
+    m_confirmedComposition += String::fromUTF8(compositionString);
+
+    // If the commit was triggered outside of a key event, just send
+    // the IME event now. If we are handling a key event, we'll decide
+    // later how to handle this.
+    if (!m_filteringKeyEvent)
+        sendCompositionAndPreeditWithFakeKeyEvents(Composition);
+}
+
+void GtkInputMethodFilter::handlePreeditStart()
+{
+    if (m_preventNextCommit || !m_enabled)
+        return;
+    m_preeditChanged = true;
+    m_preedit = "";
+}
+
+void GtkInputMethodFilter::handlePreeditChanged()
+{
+    if (!m_enabled)
+        return;
+
+    GOwnPtr<gchar> newPreedit;
+    gtk_im_context_get_preedit_string(m_context.get(), &newPreedit.outPtr(), 0, &m_cursorOffset);
+
+    if (m_preventNextCommit) {
+        if (strlen(newPreedit.get()) > 0)
+            m_preventNextCommit = false;
+        else
+            return;
+    }
+
+    m_preedit = String::fromUTF8(newPreedit.get());
+    m_cursorOffset = std::min(std::max(m_cursorOffset, 0), static_cast<int>(m_preedit.length()));
+
+    m_composingTextCurrently = !m_preedit.isEmpty();
+    m_preeditChanged = true;
+
+    if (!m_filteringKeyEvent)
+        sendCompositionAndPreeditWithFakeKeyEvents(Preedit);
+}
+
+void GtkInputMethodFilter::handlePreeditEnd()
+{
+    if (m_preventNextCommit || !m_enabled)
+        return;
+
+    m_preedit = String();
+    m_cursorOffset = 0;
+    m_preeditChanged = true;
+
+    if (!m_filteringKeyEvent)
+        updatePreedit();
+}
+
+}
diff --git a/Source/WebCore/platform/gtk/GtkInputMethodFilter.h b/Source/WebCore/platform/gtk/GtkInputMethodFilter.h
new file mode 100644 (file)
index 0000000..2b06c35
--- /dev/null
@@ -0,0 +1,87 @@
+/*
+ * Copyright (C) 2012 Igalia S.L.
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Library General Public
+ * License as published by the Free Software Foundation; either
+ * version 2 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Library General Public License for more details.
+ *
+ * You should have received a copy of the GNU Library General Public License
+ * along with this library; see the file COPYING.LIB.  If not, write to
+ * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
+ * Boston, MA 02110-1301, USA.
+ */
+
+#include "GRefPtrGtk.h"
+#include <gdk/gdk.h>
+#include <wtf/text/WTFString.h>
+
+typedef struct _GtkIMContext GtkIMContext;
+typedef struct _GtkWidget GtkWidget;
+
+namespace WebCore {
+
+class GtkInputMethodFilter {
+public:
+    GtkInputMethodFilter();
+    ~GtkInputMethodFilter();
+
+    bool filterKeyEvent(GdkEventKey*);
+    void notifyMouseButtonPress();
+    void notifyFocusedIn();
+    void notifyFocusedOut();
+    void resetContext();
+    void setEnabled(bool);
+
+    void handleCommit(const char* compositionString);
+    void handlePreeditChanged();
+    void handlePreeditStart();
+    void handlePreeditEnd();
+
+    void confirmComposition();
+    void cancelContextComposition();
+    void updatePreedit();
+
+    GtkIMContext* context() { return m_context.get(); }
+
+protected:
+    enum ResultsToSend {
+        Preedit = 1 << 1,
+        Composition = 1 << 2,
+        PreeditAndComposition = Preedit + Composition
+    };
+
+    void setWidget(GtkWidget*);
+    virtual bool canEdit() = 0;
+    virtual bool sendSimpleKeyEvent(GdkEventKey*, WTF::String eventString = String()) = 0;
+    virtual bool sendKeyEventWithCompositionResults(GdkEventKey*, ResultsToSend = PreeditAndComposition) = 0;
+    virtual void confirmCompositionText(String composition) = 0;
+    virtual void confirmCurrentComposition() = 0;
+    virtual void cancelCurrentComposition() = 0;
+    virtual void setPreedit(String, int cursorOffset) = 0;
+
+    WTF::String m_confirmedComposition;
+    WTF::String m_preedit;
+    int m_cursorOffset;
+
+private:
+    void sendCompositionAndPreeditWithFakeKeyEvents(ResultsToSend);
+
+    GRefPtr<GtkIMContext> m_context;
+    GtkWidget* m_widget;
+    bool m_enabled;
+    bool m_composingTextCurrently;
+    bool m_filteringKeyEvent;
+    bool m_preeditChanged;
+    bool m_preventNextCommit;
+    bool m_justSentFakeKeyUp;
+    unsigned int m_lastFilteredKeyPressCodeWithNoResults;
+};
+
+} // namespace WebCore
+
index 3a2409d..81be930 100644 (file)
@@ -510,7 +510,8 @@ int PlatformKeyboardEvent::windowsKeyCodeForGdkKeyCode(unsigned keycode)
         case GDK_F23:
         case GDK_F24:
             return VK_F1 + (keycode - GDK_F1);
-
+        case GDK_KEY_VoidSymbol:
+            return VK_PROCESSKEY;
         default:
             return 0;
     }
@@ -545,12 +546,30 @@ String PlatformKeyboardEvent::singleCharacterString(unsigned val)
     }
 }
 
+static PlatformEvent::Type eventTypeForGdkKeyEvent(GdkEventKey* event)
+{
+    return event->type == GDK_KEY_RELEASE ? PlatformEvent::KeyUp : PlatformEvent::KeyDown;
+}
+
+static PlatformEvent::Modifiers modifiersForGdkKeyEvent(GdkEventKey* event)
+{
+    unsigned int modifiers = 0;
+    if (event->state & GDK_SHIFT_MASK || event->keyval == GDK_3270_BackTab)
+        modifiers |= PlatformEvent::ShiftKey;
+    if (event->state & GDK_CONTROL_MASK)
+        modifiers |= PlatformEvent::CtrlKey;
+    if (event->state & GDK_MOD1_MASK)
+        modifiers |= PlatformEvent::AltKey;
+    if (event->state & GDK_META_MASK)
+        modifiers |= PlatformEvent::MetaKey;
+    return static_cast<PlatformEvent::Modifiers>(modifiers);
+}
+
 // Keep this in sync with the other platform event constructors
-// TODO: m_gdkEventKey should be refcounted
-PlatformKeyboardEvent::PlatformKeyboardEvent(GdkEventKey* event)
-    : PlatformEvent((event->type == GDK_KEY_RELEASE) ? PlatformEvent::KeyUp : PlatformEvent::KeyDown, (event->state & GDK_SHIFT_MASK) || (event->keyval == GDK_3270_BackTab), event->state & GDK_CONTROL_MASK, event->state & GDK_MOD1_MASK, event->state & GDK_META_MASK, currentTime())
-    , m_text(singleCharacterString(event->keyval))
-    , m_unmodifiedText(singleCharacterString(event->keyval))
+PlatformKeyboardEvent::PlatformKeyboardEvent(GdkEventKey* event, const CompositionResults& compositionResults)
+    : PlatformEvent(eventTypeForGdkKeyEvent(event), modifiersForGdkKeyEvent(event), currentTime())
+    , m_text(compositionResults.simpleString.length() ? compositionResults.simpleString : singleCharacterString(event->keyval))
+    , m_unmodifiedText(m_text)
     , m_keyIdentifier(keyIdentifierForGdkKeyCode(event->keyval))
     , m_windowsVirtualKeyCode(windowsKeyCodeForGdkKeyCode(event->keyval))
     , m_nativeVirtualKeyCode(event->keyval)
@@ -559,7 +578,11 @@ PlatformKeyboardEvent::PlatformKeyboardEvent(GdkEventKey* event)
     , m_isKeypad(event->keyval >= GDK_KP_Space && event->keyval <= GDK_KP_9)
     , m_isSystemKey(false)
     , m_gdkEventKey(event)
+    , m_compositionResults(compositionResults)
 {
+    // To match the behavior of IE, we return VK_PROCESSKEY for keys that triggered composition results.
+    if (compositionResults.compositionUpdated())
+        m_windowsVirtualKeyCode = VK_PROCESSKEY;
 }
 
 void PlatformKeyboardEvent::disambiguateKeyDownEvent(Type type, bool backwardCompatibilityMode)
@@ -574,6 +597,12 @@ void PlatformKeyboardEvent::disambiguateKeyDownEvent(Type type, bool backwardCom
     if (type == PlatformEvent::RawKeyDown) {
         m_text = String();
         m_unmodifiedText = String();
+    } else if (type == PlatformEvent::Char && m_compositionResults.compositionUpdated()) {
+        // Having empty text, prevents this Char (which is a DOM keypress) event
+        // from going to the DOM. Keys that trigger composition events should not
+        // fire keypress.
+        m_text = String();
+        m_unmodifiedText = String();
     } else {
         m_keyIdentifier = String();
         m_windowsVirtualKeyCode = 0;
@@ -596,9 +625,4 @@ void PlatformKeyboardEvent::getCurrentModifierState(bool& shiftKey, bool& ctrlKe
     metaKey = state & GDK_META_MASK;
 }
 
-GdkEventKey* PlatformKeyboardEvent::gdkEventKey() const
-{
-    return m_gdkEventKey;
-}
-
 }
index 8c21a73..e2dc1b6 100644 (file)
@@ -1,3 +1,58 @@
+2012-05-03  Martin Robinson  <mrobinson@igalia.com>
+
+        [GTK] Rework IME handling to fix bugs and prepare for WebKit2
+        https://bugs.webkit.org/show_bug.cgi?id=84556
+
+        Reviewed by Gustavo Noronha Silva.
+
+        Rework input method handling logic into a class called GtkInputMethodFilter.
+        This filter now runs before WebCore event handling, allowing the code to more
+        easily fake simple compositions that should be seen as keystrokes. We can also
+        filter keypresses that should not go to web content at all, such as key up events
+        related to key down events that were filtered.
+
+        Also added is a WebViewInputMethodFilter which is a concrete implementation of
+        GtkInputMethodFilter. This class contains logic for actually sending events to
+        WebCore. In WebKit2 an implementation of GtkInputMethodFilter will send events
+        across the IPC channel.
+
+        * GNUmakefile.am: Add new files to the source list.
+        * WebCoreSupport/ContextMenuClientGtk.cpp:
+        (WebKit::inputMethodsMenuItem): Access the input method context via the filter.
+        * WebCoreSupport/EditorClientGtk.cpp: Remove the tricky logic of input method
+        events from this class, because it's now in the GtkInputMethodFilter.
+        (WebKit::EditorClient::setInputMethodState): Call into the filter.
+        (WebKit::EditorClient::shouldBeginEditing): We no longer need to update the composition here.
+        This is handled by the focus in and focus out logic in the filter.
+        (WebKit::EditorClient::shouldEndEditing): Ditto.
+        (WebKit::EditorClient::respondToChangedSelection): Call into the filter now.
+        (WebKit::EditorClient::handleInputMethodKeyboardEvent): Added this helper which executes
+        any pending composition confirmation or preedit update actions as the default action of
+        the keydown event.
+        (WebKit::EditorClient::handleKeyboardEvent): Call handleInputMethodKeyboardEvent to do
+        any pending composition action.
+        (WebKit::EditorClient::handleInputMethodKeydown): Remove all the logic from this method.
+        Keys are filtered before they are sent to WebCore now and the actual action of input method
+        events happens in the keydown default action to increase compatibility with other browsers.
+        (WebKit::EditorClient::EditorClient): Remove context signal management.
+        (WebKit::EditorClient::~EditorClient): Ditto.
+        * WebCoreSupport/EditorClientGtk.h:
+        (EditorClient): No longer has some members that tracked IME status.
+        * WebCoreSupport/WebViewInputMethodFilter.cpp: Added.
+        * WebCoreSupport/WebViewInputMethodFilter.h: Added.
+        * webkit/webkitwebview.cpp:
+        (webkit_web_view_get_property): Get the context from the filter now.
+        (webkit_web_view_key_press_event): Just send events straight to the filter.
+        The filter will decide whether or not to send them to WebCore.
+        (webkit_web_view_key_release_event): Ditto.
+        (webkit_web_view_button_press_event): Use the filter to handle button press
+        events related to IME.
+        (webkit_web_view_focus_in_event): Notify the filter now.
+        (webkit_web_view_focus_out_event): Ditto.
+        (webkit_web_view_realize): The filter takes care of listening for realize now.
+        (webkit_web_view_init): Set the WebView widget on the filter.
+        * webkit/webkitwebviewprivate.h: Change the GtkIMContext member to be a GtkInputMethodFilter member.
+
 2012-05-03  Fady Samuel  <fsamuel@chromium.org>
 
         Removing line in computeViewportAttributes that enforces a minimum scale factor to never allow zooming out more than viewport
index 5ac1a7b..2d5ad0a 100644 (file)
@@ -212,6 +212,8 @@ webkitgtk_sources += \
        Source/WebKit/gtk/WebCoreSupport/PlatformStrategiesGtk.cpp \
        Source/WebKit/gtk/WebCoreSupport/UserMediaClientGtk.cpp \
        Source/WebKit/gtk/WebCoreSupport/UserMediaClientGtk.h \
+       Source/WebKit/gtk/WebCoreSupport/WebViewInputMethodFilter.cpp \
+       Source/WebKit/gtk/WebCoreSupport/WebViewInputMethodFilter.h \
        Source/WebKit/gtk/webkit/webkitapplicationcache.cpp \
        Source/WebKit/gtk/webkit/webkitdownload.cpp \
        Source/WebKit/gtk/webkit/webkitdownloadprivate.h \
index 7dfd9ee..e0c3ed9 100644 (file)
@@ -64,7 +64,7 @@ static GtkWidget* inputMethodsMenuItem (WebKitWebView* webView)
 
     WebKitWebViewPrivate* priv = webView->priv;
     ContextMenu imContextMenu;
-    gtk_im_multicontext_append_menuitems(GTK_IM_MULTICONTEXT(priv->imContext.get()), GTK_MENU_SHELL(imContextMenu.platformDescription()));
+    gtk_im_multicontext_append_menuitems(GTK_IM_MULTICONTEXT(priv->imFilter.context()), GTK_MENU_SHELL(imContextMenu.platformDescription()));
 
     ContextMenuItem menuItem(ActionType, ContextMenuItemTagInputMethods, contextMenuItemTagInputMethods(), &imContextMenu);
     imContextMenu.releasePlatformDescription();
index a1e2eab..c9fba2d 100644 (file)
@@ -62,68 +62,13 @@ using namespace WebCore;
 
 namespace WebKit {
 
-static void imContextCommitted(GtkIMContext* context, const gchar* compositionString, EditorClient* client)
-{
-    Frame* frame = core(static_cast<WebKitWebView*>(client->webView()))->focusController()->focusedOrMainFrame();
-    if (!frame || !frame->editor()->canEdit())
-        return;
-
-    // If this signal fires during a keydown event when we are not in the middle
-    // of a composition, then treat this 'commit' as a normal key event and just
-    // change the editable area right before the keypress event.
-    if (client->treatContextCommitAsKeyEvent()) {
-        client->updatePendingComposition(compositionString);
-        return;
-    }
-
-    // If this signal fires during a mousepress event when we are in the middle
-    // of a composition, skip this 'commit' because the composition is already confirmed. 
-    if (client->preventNextCompositionCommit()) 
-        return;
-    frame->editor()->confirmComposition(String::fromUTF8(compositionString));
-    client->clearPendingComposition();
-}
-
-static void imContextPreeditChanged(GtkIMContext* context, EditorClient* client)
-{
-    Frame* frame = core(static_cast<WebKitWebView*>(client->webView()))->focusController()->focusedOrMainFrame();
-    if (!frame || !frame->editor()->canEdit())
-        return;
-
-    // We ignore the provided PangoAttrList for now.
-    GOwnPtr<gchar> newPreedit(0);
-    gtk_im_context_get_preedit_string(context, &newPreedit.outPtr(), 0, 0);
-
-    String preeditString = String::fromUTF8(newPreedit.get());
-    Vector<CompositionUnderline> underlines;
-    underlines.append(CompositionUnderline(0, preeditString.length(), Color(0, 0, 0), false));
-    frame->editor()->setComposition(preeditString, underlines, 0, 0);
-}
-
-
-void EditorClient::updatePendingComposition(const gchar* newComposition)
-{
-    // The IMContext may signal more than one completed composition in a row,
-    // in which case we want to append them, rather than overwrite the old one.
-    if (!m_pendingComposition)
-        m_pendingComposition.set(g_strdup(newComposition));
-    else
-        m_pendingComposition.set(g_strconcat(m_pendingComposition.get(), newComposition, NULL));
-}
-
 void EditorClient::willSetInputMethodState()
 {
 }
 
 void EditorClient::setInputMethodState(bool active)
 {
-    WebKitWebViewPrivate* priv = m_webView->priv;
-
-    if (active)
-        gtk_im_context_focus_in(priv->imContext.get());
-    else
-        gtk_im_context_focus_out(priv->imContext.get());
+    m_webView->priv->imFilter.setEnabled(active);
 }
 
 bool EditorClient::shouldShowUnicodeMenu()
@@ -181,8 +126,6 @@ int EditorClient::spellCheckerDocumentTag()
 
 bool EditorClient::shouldBeginEditing(WebCore::Range* range)
 {
-    clearPendingComposition();
-
     gboolean accept = TRUE;
     GRefPtr<WebKitDOMRange> kitRange(adoptGRef(kit(range)));
     g_signal_emit_by_name(m_webView, "should-begin-editing", kitRange.get(), &accept);
@@ -191,8 +134,6 @@ bool EditorClient::shouldBeginEditing(WebCore::Range* range)
 
 bool EditorClient::shouldEndEditing(WebCore::Range* range)
 {
-    clearPendingComposition();
-
     gboolean accept = TRUE;
     GRefPtr<WebKitDOMRange> kitRange(adoptGRef(kit(range)));
     g_signal_emit_by_name(m_webView, "should-end-editing", kitRange.get(), &accept);
@@ -317,25 +258,17 @@ void EditorClient::respondToChangedSelection(Frame* frame)
     if (!frame)
         return;
 
-    if (frame->editor()->ignoreCompositionSelectionChange())
-        return;
-
 #if PLATFORM(X11)
     setSelectionPrimaryClipboardIfNeeded(m_webView);
 #endif
 
-    if (!frame->editor()->hasComposition())
+    if (m_updatingComposition || !frame->editor()->hasComposition() || frame->editor()->ignoreCompositionSelectionChange())
         return;
 
     unsigned start;
     unsigned end;
-    WebKitWebViewPrivate* priv = m_webView->priv;
-
-    if (!frame->editor()->getCompositionSelection(start, end)) {
-        // gtk_im_context_reset() clears the composition for us.
-        gtk_im_context_reset(priv->imContext.get());
-        frame->editor()->cancelComposition();
-    }
+    if (!frame->editor()->getCompositionSelection(start, end))
+        m_webView->priv->imFilter.resetContext();
 }
 
 void EditorClient::didEndEditing()
@@ -482,13 +415,47 @@ bool EditorClient::executePendingEditorCommands(Frame* frame, bool allowTextInse
     }
 
     m_pendingEditorCommands.clear();
+    return success;
+}
+
+bool EditorClient::handleInputMethodKeyboardEvent(KeyboardEvent* event)
+{
+    if (event->type() != eventNames().keydownEvent)
+        return false;
 
-    // If we successfully completed all editor commands, then
-    // this signals a canceling of the composition.
-    if (success)
-        clearPendingComposition();
+    const PlatformKeyboardEvent* platformEvent = event->keyEvent();
+    if (!platformEvent)
+        return false;
 
-    return success;
+    Frame* frame = core(m_webView)->focusController()->focusedOrMainFrame();
+    if (!frame || !frame->editor()->canEdit())
+        return false;
+
+    const CompositionResults& compositionResults = platformEvent->compositionResults();
+    if (!compositionResults.compositionUpdated())
+        return false;
+
+    m_updatingComposition = true;
+
+    // This won't prevent a keypress event alone, but PlatformKeyboardEvent returns
+    // an empty string when there are composition results. That prevents the delivery
+    // of keypress, which is the behavior we want for composition events. See
+    // EventHandler::keyEvent.
+    event->preventDefault();
+    ASSERT(platformEvent->string().isNull());
+
+    if (!compositionResults.confirmedComposition.isNull())
+        frame->editor()->confirmComposition(compositionResults.confirmedComposition);
+
+    String preedit = compositionResults.preedit;
+    if (!preedit.isNull()) {
+        Vector<CompositionUnderline> underlines;
+        underlines.append(CompositionUnderline(0, preedit.length(), Color(1, 1, 1), false));
+        frame->editor()->setComposition(preedit, underlines, compositionResults.preeditCursorOffset, compositionResults.preeditCursorOffset);
+    }
+
+    m_updatingComposition = false;
+    return true;
 }
 
 void EditorClient::handleKeyboardEvent(KeyboardEvent* event)
@@ -502,6 +469,9 @@ void EditorClient::handleKeyboardEvent(KeyboardEvent* event)
     if (!platformEvent)
         return;
 
+    if (handleInputMethodKeyboardEvent(event))
+        return;
+
     KeyBindingTranslator::EventType type = event->type() == eventNames().keydownEvent ?
         KeyBindingTranslator::KeyDown : KeyBindingTranslator::KeyPress;
     m_keyBindingTranslator.getEditorCommandsForKeyEvent(platformEvent->gdkEventKey(), type, m_pendingEditorCommands);
@@ -531,103 +501,26 @@ void EditorClient::handleKeyboardEvent(KeyboardEvent* event)
     // This is just a normal text insertion, so wait to execute the insertion
     // until a keypress event happens. This will ensure that the insertion will not
     // be reflected in the contents of the field until the keyup DOM event.
-    if (event->type() == eventNames().keypressEvent) {
-
-        // If we have a pending composition at this point, it happened while
-        // filtering a keypress, so we treat it as a normal text insertion.
-        // This will also ensure that if the keypress event handler changed the
-        // currently focused node, the text is still inserted into the original
-        // node (insertText() has this logic, but confirmComposition() does not).
-        if (m_pendingComposition) {
-            frame->editor()->insertText(String::fromUTF8(m_pendingComposition.get()), event);
-            clearPendingComposition();
-            event->setDefaultHandled();
+    if (event->type() != eventNames().keypressEvent)
+        return;
 
-        } else {
-            // Don't insert null or control characters as they can result in unexpected behaviour
-            if (event->charCode() < ' ')
-                return;
+    // Don't insert null or control characters as they can result in unexpected behaviour
+    if (event->charCode() < ' ')
+        return;
 
-            // Don't insert anything if a modifier is pressed
-            if (platformEvent->ctrlKey() || platformEvent->altKey())
-                return;
+    // Don't insert anything if a modifier is pressed
+    if (platformEvent->ctrlKey() || platformEvent->altKey())
+        return;
 
-            if (frame->editor()->insertText(platformEvent->text(), event))
-                event->setDefaultHandled();
-        }
-    }
+    if (frame->editor()->insertText(platformEvent->text(), event))
+        event->setDefaultHandled();
 }
 
 void EditorClient::handleInputMethodKeydown(KeyboardEvent* event)
 {
-    Frame* targetFrame = core(m_webView)->focusController()->focusedOrMainFrame();
-    if (!targetFrame || !targetFrame->editor()->canEdit())
-        return;
-
-    WebKitWebViewPrivate* priv = m_webView->priv;
-
-    m_preventNextCompositionCommit = false;
-
-    // Some IM contexts (e.g. 'simple') will act as if they filter every
-    // keystroke and just issue a 'commit' signal during handling. In situations
-    // where the 'commit' signal happens during filtering and there is no active
-    // composition, act as if the keystroke was not filtered. The one exception to
-    // this is when the keyval parameter of the GdkKeyEvent is 0, which is often
-    // a key event sent by the IM context for committing the current composition.
-
-    // Here is a typical sequence of events for the 'simple' context:
-    // 1. GDK key press event -> webkit_web_view_key_press_event
-    // 2. Keydown event -> EditorClient::handleInputMethodKeydown
-    //     gtk_im_context_filter_keypress returns true, but there is a pending
-    //     composition so event->preventDefault is not called (below).
-    // 3. Keydown event bubbles through the DOM
-    // 4. Keydown event -> EditorClient::handleKeyboardEvent
-    //     No action taken.
-    // 4. GDK key release event -> webkit_web_view_key_release_event
-    // 5. gtk_im_context_filter_keypress is called on the release event.
-    //     Simple does not filter most key releases, so the event continues.
-    // 6. Keypress event bubbles through the DOM.
-    // 7. Keypress event -> EditorClient::handleKeyboardEvent
-    //     pending composition is inserted.
-    // 8. Keyup event bubbles through the DOM.
-    // 9. Keyup event -> EditorClient::handleKeyboardEvent
-    //     No action taken.
-
-    // There are two situations where we do filter the keystroke:
-    // 1. The IMContext instructed us to filter and we have no pending composition.
-    // 2. The IMContext did not instruct us to filter, but the keystroke caused a
-    //    composition in progress to finish. It seems that sometimes SCIM will finish
-    //    a composition and not mark the keystroke as filtered.
-    m_treatContextCommitAsKeyEvent = (!targetFrame->editor()->hasComposition())
-         && event->keyEvent()->gdkEventKey()->keyval;
-    clearPendingComposition();
-    if ((gtk_im_context_filter_keypress(priv->imContext.get(), event->keyEvent()->gdkEventKey()) && !m_pendingComposition)
-        || (!m_treatContextCommitAsKeyEvent && !targetFrame->editor()->hasComposition()))
-        event->preventDefault();
-
-    m_treatContextCommitAsKeyEvent = false;
-}
-
-void EditorClient::handleInputMethodMousePress()
-{
-    Frame* targetFrame = core(m_webView)->focusController()->focusedOrMainFrame();
-
-    if (!targetFrame || !targetFrame->editor()->canEdit())
-        return;
-
-    WebKitWebViewPrivate* priv = m_webView->priv;
-
-    // When a mouse press fires, the commit signal happens during a composition.
-    // In this case, if the focused node is changed, the commit signal happens in a diffrent node.
-    // Therefore, we need to confirm the current compositon and ignore the next commit signal. 
-    GOwnPtr<gchar> newPreedit(0);
-    gtk_im_context_get_preedit_string(priv->imContext.get(), &newPreedit.outPtr(), 0, 0);
-    
-    if (g_utf8_strlen(newPreedit.get(), -1)) {
-        targetFrame->editor()->confirmComposition();
-        m_preventNextCompositionCommit = true;
-        gtk_im_context_reset(priv->imContext.get());
-    } 
+    // Input method results are handled in handleKeyboardEvent, so that we can wait
+    // to trigger composition updates until after the keydown event handler. This better
+    // matches other browsers.
 }
 
 EditorClient::EditorClient(WebKitWebView* webView)
@@ -636,20 +529,13 @@ EditorClient::EditorClient(WebKitWebView* webView)
     , m_textCheckerClient(WEBKIT_SPELL_CHECKER(webkit_get_text_checker()))
 #endif
     , m_webView(webView)
-    , m_preventNextCompositionCommit(false)
-    , m_treatContextCommitAsKeyEvent(false)
     , m_smartInsertDeleteEnabled(false)
+    , m_updatingComposition(false)
 {
-    WebKitWebViewPrivate* priv = m_webView->priv;
-    g_signal_connect(priv->imContext.get(), "commit", G_CALLBACK(imContextCommitted), this);
-    g_signal_connect(priv->imContext.get(), "preedit-changed", G_CALLBACK(imContextPreeditChanged), this);
 }
 
 EditorClient::~EditorClient()
 {
-    WebKitWebViewPrivate* priv = m_webView->priv;
-    g_signal_handlers_disconnect_by_func(priv->imContext.get(), (gpointer)imContextCommitted, this);
-    g_signal_handlers_disconnect_by_func(priv->imContext.get(), (gpointer)imContextPreeditChanged, this);
 }
 
 void EditorClient::textFieldDidBeginEditing(Element*)
index 2f08d8d..3612d90 100644 (file)
@@ -65,12 +65,7 @@ class EditorClient : public WebCore::EditorClient {
         EditorClient(WebKitWebView*);
         ~EditorClient();
         WebKitWebView* webView() { return m_webView; }
-        bool treatContextCommitAsKeyEvent() { return m_treatContextCommitAsKeyEvent; }
-        bool preventNextCompositionCommit() { return m_preventNextCompositionCommit; }
-        void clearPendingComposition() { m_pendingComposition.set(0); }
-        bool hasPendingComposition() { return m_pendingComposition; }
         void addPendingEditorCommand(const char* command) { m_pendingEditorCommands.append(command); }
-        void updatePendingComposition(const char*);
         void generateEditorCommands(const WebCore::KeyboardEvent*);
         bool executePendingEditorCommands(WebCore::Frame*, bool);
 
@@ -119,7 +114,6 @@ class EditorClient : public WebCore::EditorClient {
 
         virtual void handleKeyboardEvent(WebCore::KeyboardEvent*);
         virtual void handleInputMethodKeydown(WebCore::KeyboardEvent*);
-        virtual void handleInputMethodMousePress();
 
         virtual void textFieldDidBeginEditing(WebCore::Element*);
         virtual void textFieldDidEndEditing(WebCore::Element*);
@@ -140,20 +134,18 @@ class EditorClient : public WebCore::EditorClient {
         virtual bool shouldShowUnicodeMenu();
 
     private:
+        bool handleInputMethodKeyboardEvent(WebCore::KeyboardEvent*);
+
 #if ENABLE(SPELLCHECK)
         TextCheckerClientGtk m_textCheckerClient;
 #else
         WebCore::EmptyTextCheckerClient m_textCheckerClient;
 #endif
         WebKitWebView* m_webView;
-        bool m_preventNextCompositionCommit;
-        bool m_treatContextCommitAsKeyEvent;
-        GOwnPtr<gchar> m_pendingComposition;
-
         WebCore::KeyBindingTranslator m_keyBindingTranslator;
         Vector<WTF::String> m_pendingEditorCommands;
-
         bool m_smartInsertDeleteEnabled;
+        bool m_updatingComposition;
     };
 }
 
diff --git a/Source/WebKit/gtk/WebCoreSupport/WebViewInputMethodFilter.cpp b/Source/WebKit/gtk/WebCoreSupport/WebViewInputMethodFilter.cpp
new file mode 100644 (file)
index 0000000..35ca23b
--- /dev/null
@@ -0,0 +1,110 @@
+/*
+ * Copyright (C) 2012 Igalia S.L.
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Library General Public
+ * License as published by the Free Software Foundation; either
+ * version 2 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Library General Public License for more details.
+ *
+ * You should have received a copy of the GNU Library General Public License
+ * along with this library; see the file COPYING.LIB.  If not, write to
+ * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
+ * Boston, MA 02110-1301, USA.
+ */
+
+#include "config.h"
+#include "WebViewInputMethodFilter.h"
+
+#include "FocusController.h"
+#include "Frame.h"
+#include "PlatformKeyboardEvent.h"
+#include "webkitwebviewprivate.h"
+#include <wtf/text/CString.h>
+
+using namespace WebCore;
+
+namespace WebKit {
+
+Frame* WebViewInputMethodFilter::focusedOrMainFrame()
+{
+    ASSERT(m_webView);
+    Page* page = core(m_webView);
+    if (!page)
+        return 0;
+
+    return page->focusController()->focusedOrMainFrame();
+}
+
+void WebViewInputMethodFilter::setWebView(WebKitWebView* webView)
+{
+    m_webView = webView;
+    GtkInputMethodFilter::setWidget(GTK_WIDGET(webView));
+}
+
+bool WebViewInputMethodFilter::canEdit()
+{
+    Frame* frame = focusedOrMainFrame();
+    return frame && frame->editor()->canEdit();
+}
+
+bool WebViewInputMethodFilter::sendSimpleKeyEvent(GdkEventKey* event, WTF::String simpleString)
+{
+    PlatformKeyboardEvent platformEvent(event, CompositionResults(simpleString));
+    return focusedOrMainFrame()->eventHandler()->keyEvent(platformEvent);
+}
+
+bool WebViewInputMethodFilter::sendKeyEventWithCompositionResults(GdkEventKey* event, ResultsToSend resultsToSend)
+{
+    PlatformKeyboardEvent platformEvent(event, CompositionResults(resultsToSend & Composition ? m_confirmedComposition : String(),
+                                                                  resultsToSend & Preedit ? m_preedit : String(),
+                                                                  m_cursorOffset));
+    return focusedOrMainFrame()->eventHandler()->keyEvent(platformEvent);
+}
+
+void WebViewInputMethodFilter::confirmCompositionText(String text)
+{
+    Frame* frame = focusedOrMainFrame();
+    if (!frame || !frame->editor()->canEdit())
+        return;
+
+    if (text.isNull()) {
+        confirmCurrentComposition();
+        return;
+    }
+    frame->editor()->confirmComposition(m_confirmedComposition);
+}
+
+void WebViewInputMethodFilter::confirmCurrentComposition()
+{
+    Frame* frame = focusedOrMainFrame();
+    if (!frame || !frame->editor()->canEdit())
+        return;
+    frame->editor()->confirmComposition();
+}
+
+void WebViewInputMethodFilter::cancelCurrentComposition()
+{
+    Frame* frame = focusedOrMainFrame();
+    if (!frame || !frame->editor()->canEdit())
+        return;
+    frame->editor()->cancelComposition();
+}
+
+void WebViewInputMethodFilter::setPreedit(String newPreedit, int cursorOffset)
+{
+    Frame* frame = focusedOrMainFrame();
+    if (!frame || !frame->editor()->canEdit())
+        return;
+
+    // TODO: We should parse the PangoAttrList that we get from the IM context here.
+    Vector<CompositionUnderline> underlines;
+    underlines.append(CompositionUnderline(0, newPreedit.length(), Color(1, 1, 1), false));
+    frame->editor()->setComposition(newPreedit, underlines, m_cursorOffset, m_cursorOffset);
+}
+
+} // namespace WebKit
diff --git a/Source/WebKit/gtk/WebCoreSupport/WebViewInputMethodFilter.h b/Source/WebKit/gtk/WebCoreSupport/WebViewInputMethodFilter.h
new file mode 100644 (file)
index 0000000..fd70edb
--- /dev/null
@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) 2012 Igalia S.L.
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Library General Public
+ * License as published by the Free Software Foundation; either
+ * version 2 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Library General Public License for more details.
+ *
+ * You should have received a copy of the GNU Library General Public License
+ * along with this library; see the file COPYING.LIB.  If not, write to
+ * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
+ * Boston, MA 02110-1301, USA.
+ */
+
+#ifndef WebViewInputMethodFilter_h
+#define WebViewInputMethodFilter_h
+
+#include "GtkInputMethodFilter.h"
+
+namespace WebCore {
+class Frame;
+}
+
+typedef struct _WebKitWebView WebKitWebView;
+
+namespace WebKit {
+
+class WebViewInputMethodFilter : public WebCore::GtkInputMethodFilter {
+public:
+    void setWebView(WebKitWebView*);
+
+protected:
+    virtual bool sendSimpleKeyEvent(GdkEventKey*, WTF::String eventString);
+    virtual bool sendKeyEventWithCompositionResults(GdkEventKey*, ResultsToSend);
+    virtual bool canEdit();
+    virtual void confirmCompositionText(String);
+    virtual void confirmCurrentComposition();
+    virtual void cancelCurrentComposition();
+    virtual void setPreedit(String, int cursorOffset);
+
+private:
+    WebCore::Frame* focusedOrMainFrame();
+
+    WebKitWebView* m_webView;
+};
+
+} // namespace WebKit
+
+#endif // WebViewInputMethodFilter_h
index 372d465..573b5eb 100644 (file)
@@ -273,8 +273,6 @@ G_DEFINE_TYPE_WITH_CODE(WebKitWebView, webkit_web_view, GTK_TYPE_CONTAINER,
 static void webkit_web_view_settings_notify(WebKitWebSettings* webSettings, GParamSpec* pspec, WebKitWebView* webView);
 static void webkit_web_view_set_window_features(WebKitWebView* webView, WebKitWebWindowFeatures* webWindowFeatures);
 
-static GtkIMContext* webkit_web_view_get_im_context(WebKitWebView*);
-
 #if ENABLE(CONTEXT_MENUS)
 static void PopupMenuPositionFunc(GtkMenu* menu, gint *x, gint *y, gboolean *pushIn, gpointer userData)
 {
@@ -581,7 +579,7 @@ static void webkit_web_view_get_property(GObject* object, guint prop_id, GValue*
         g_value_set_string(value, webkit_web_view_get_icon_uri(webView));
         break;
     case PROP_IM_CONTEXT:
-        g_value_set_object(value, webkit_web_view_get_im_context(webView));
+        g_value_set_object(value, webView->priv->imFilter.context());
         break;
     case PROP_VIEW_MODE:
         g_value_set_enum(value, webkit_web_view_get_view_mode(webView));
@@ -710,42 +708,15 @@ static gboolean webkit_web_view_draw(GtkWidget* widget, cairo_t* cr)
 
 static gboolean webkit_web_view_key_press_event(GtkWidget* widget, GdkEventKey* event)
 {
-    WebKitWebView* webView = WEBKIT_WEB_VIEW(widget);
-
-    Frame* frame = core(webView)->focusController()->focusedOrMainFrame();
-    PlatformKeyboardEvent keyboardEvent(event);
-
-    if (!frame->view())
-        return FALSE;
-
-    if (frame->eventHandler()->keyEvent(keyboardEvent))
+    if (WEBKIT_WEB_VIEW(widget)->priv->imFilter.filterKeyEvent(event))
         return TRUE;
-
-    /* Chain up to our parent class for binding activation */
     return GTK_WIDGET_CLASS(webkit_web_view_parent_class)->key_press_event(widget, event);
 }
 
 static gboolean webkit_web_view_key_release_event(GtkWidget* widget, GdkEventKey* event)
 {
-    WebKitWebView* webView = WEBKIT_WEB_VIEW(widget);
-
-    // GTK+ IM contexts often require us to filter key release events, which
-    // WebCore does not do by default, so we filter the event here. We only block
-    // the event if we don't have a pending composition, because that means we
-    // are using a context like 'simple' which marks every keystroke as filtered.
-    WebKit::EditorClient* client = static_cast<WebKit::EditorClient*>(core(webView)->editorClient());
-    if (gtk_im_context_filter_keypress(webView->priv->imContext.get(), event) && !client->hasPendingComposition())
+    if (WEBKIT_WEB_VIEW(widget)->priv->imFilter.filterKeyEvent(event))
         return TRUE;
-
-    Frame* frame = core(webView)->focusController()->focusedOrMainFrame();
-    if (!frame->view())
-        return FALSE;
-
-    PlatformKeyboardEvent keyboardEvent(event);
-    if (frame->eventHandler()->keyEvent(keyboardEvent))
-        return TRUE;
-
-    /* Chain up to our parent class for binding activation */
     return GTK_WIDGET_CLASS(webkit_web_view_parent_class)->key_release_event(widget, event);
 }
 
@@ -773,9 +744,8 @@ static gboolean webkit_web_view_button_press_event(GtkWidget* widget, GdkEventBu
     if (!frame->view())
         return FALSE;
 
+    priv->imFilter.notifyMouseButtonPress();
     gboolean result = frame->eventHandler()->handleMousePressEvent(platformEvent);
-    // Handle the IM context when a mouse press fires
-    static_cast<WebKit::EditorClient*>(core(webView)->editorClient())->handleInputMethodMousePress();
 
 #if PLATFORM(X11)
     /* Copy selection to the X11 selection clipboard */
@@ -964,20 +934,20 @@ static gboolean webkit_web_view_focus_in_event(GtkWidget* widget, GdkEventFocus*
     // TODO: Improve focus handling as suggested in
     // http://bugs.webkit.org/show_bug.cgi?id=16910
     GtkWidget* toplevel = gtk_widget_get_toplevel(widget);
-    if (widgetIsOnscreenToplevelWindow(toplevel) && gtk_window_has_toplevel_focus(GTK_WINDOW(toplevel))) {
-        WebKitWebView* webView = WEBKIT_WEB_VIEW(widget);
-        FocusController* focusController = core(webView)->focusController();
+    if (!widgetIsOnscreenToplevelWindow(toplevel) || !gtk_window_has_toplevel_focus(GTK_WINDOW(toplevel)))
+        return GTK_WIDGET_CLASS(webkit_web_view_parent_class)->focus_in_event(widget, event);
 
-        focusController->setActive(true);
+    WebKitWebView* webView = WEBKIT_WEB_VIEW(widget);
+    FocusController* focusController = core(webView)->focusController();
 
-        if (focusController->focusedFrame())
-            focusController->setFocused(true);
-        else
-            focusController->setFocusedFrame(core(webView)->mainFrame());
+    focusController->setActive(true);
+    if (focusController->focusedFrame())
+        focusController->setFocused(true);
+    else
+        focusController->setFocusedFrame(core(webView)->mainFrame());
 
-        if (focusController->focusedFrame()->editor()->canEdit())
-            gtk_im_context_focus_in(webView->priv->imContext.get());
-    }
+    if (focusController->focusedFrame()->editor()->canEdit())
+        webView->priv->imFilter.notifyFocusedIn();
     return GTK_WIDGET_CLASS(webkit_web_view_parent_class)->focus_in_event(widget, event);
 }
 
@@ -987,22 +957,17 @@ static gboolean webkit_web_view_focus_out_event(GtkWidget* widget, GdkEventFocus
 
     // We may hit this code while destroying the widget, and we might
     // no longer have a page, then.
-    Page* page = core(webView);
-    if (page) {
+    if (Page* page = core(webView)) {
         page->focusController()->setActive(false);
         page->focusController()->setFocused(false);
     }
 
-    if (webView->priv->imContext)
-        gtk_im_context_focus_out(webView->priv->imContext.get());
-
+    webView->priv->imFilter.notifyFocusedOut();
     return GTK_WIDGET_CLASS(webkit_web_view_parent_class)->focus_out_event(widget, event);
 }
 
 static void webkit_web_view_realize(GtkWidget* widget)
 {
-    WebKitWebViewPrivate* priv = WEBKIT_WEB_VIEW(widget)->priv;
-
     gtk_widget_set_realized(widget, TRUE);
 
     GtkAllocation allocation;
@@ -1058,8 +1023,6 @@ static void webkit_web_view_realize(GtkWidget* widget)
 #else
     gtk_style_context_set_background(gtk_widget_get_style_context(widget), window);
 #endif
-
-    gtk_im_context_set_client_window(priv->imContext.get(), window);
 }
 
 #ifdef GTK_API_VERSION_2
@@ -1641,12 +1604,6 @@ static gboolean webkit_web_view_show_help(GtkWidget* widget, GtkWidgetHelpType h
 }
 #endif
 
-static GtkIMContext* webkit_web_view_get_im_context(WebKitWebView* webView)
-{
-    g_return_val_if_fail(WEBKIT_IS_WEB_VIEW(webView), 0);
-    return GTK_IM_CONTEXT(webView->priv->imContext.get());
-}
-
 static void webkit_web_view_class_init(WebKitWebViewClass* webViewClass)
 {
     GtkBindingSet* binding_set;
@@ -3564,7 +3521,7 @@ static void webkit_web_view_init(WebKitWebView* webView)
     // members, which ensures they are initialized properly.
     new (priv) WebKitWebViewPrivate();
 
-    priv->imContext = adoptGRef(gtk_im_multicontext_new());
+    priv->imFilter.setWebView(webView);
 
     Page::PageClients pageClients;
     pageClients.chromeClient = new WebKit::ChromeClient(webView);
index 1fe21cd..0bc5a56 100644 (file)
@@ -29,6 +29,7 @@
 #include "GtkDragAndDropHelper.h"
 #include "Page.h"
 #include "ResourceHandle.h"
+#include "WebViewInputMethodFilter.h"
 #include "WidgetBackingStore.h"
 #include <webkit/webkitwebview.h>
 #include <wtf/gobject/GOwnPtr.h>
@@ -59,7 +60,7 @@ struct _WebKitWebViewPrivate {
     gint lastPopupYPosition;
 
     HashSet<GtkWidget*> children;
-    GRefPtr<GtkIMContext> imContext;
+    WebKit::WebViewInputMethodFilter imFilter;
 
     gboolean transparent;
     bool needsResizeOnMap;