InputMethodFilter.cpp   [plain text]


/*
 * Copyright (C) 2012, 2014, 2019 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 "InputMethodFilter.h"

#include "WebKitInputMethodContextPrivate.h"
#include "WebKitWebViewPrivate.h"
#include "WebPageProxy.h"
#include <WebCore/PlatformDisplay.h>
#include <wtf/SetForScope.h>

namespace WebKit {
using namespace WebCore;

InputMethodFilter::~InputMethodFilter()
{
    setContext(nullptr);
}

void InputMethodFilter::preeditStartedCallback(InputMethodFilter* filter)
{
    filter->preeditStarted();
}

void InputMethodFilter::preeditChangedCallback(InputMethodFilter* filter)
{
    filter->preeditChanged();
}

void InputMethodFilter::preeditFinishedCallback(InputMethodFilter* filter)
{
    filter->preeditFinished();
}

void InputMethodFilter::committedCallback(InputMethodFilter* filter, const char* compositionString)
{
    filter->committed(compositionString);
}

void InputMethodFilter::setContext(WebKitInputMethodContext* context)
{
    if (m_context) {
        webkitInputMethodContextSetWebView(m_context.get(), nullptr);
        g_signal_handlers_disconnect_matched(m_context.get(), G_SIGNAL_MATCH_DATA, 0, 0, nullptr, nullptr, this);
    }

    m_context = context;
    if (!m_context)
        return;

    ASSERT(webkitInputMethodContextGetWebView(m_context.get()));
    g_signal_connect_swapped(m_context.get(), "preedit-started", G_CALLBACK(preeditStartedCallback), this);
    g_signal_connect_swapped(m_context.get(), "preedit-changed", G_CALLBACK(preeditChangedCallback), this);
    g_signal_connect_swapped(m_context.get(), "preedit-finished", G_CALLBACK(preeditFinishedCallback), this);
    g_signal_connect_swapped(m_context.get(), "committed", G_CALLBACK(committedCallback), this);

    if (m_enabled && isViewFocused())
        notifyFocusedIn();
}

void InputMethodFilter::setEnabled(bool enabled)
{
    if (!enabled)
        notifyFocusedOut();

    m_enabled = enabled;
    if (m_enabled && isViewFocused())
        notifyFocusedIn();
}

InputMethodFilter::FilterResult InputMethodFilter::filterKeyEvent(PlatformEventKey* keyEvent)
{
    if (!m_enabled || !m_context)
        return { };

    SetForScope<bool> filteringContextIsAcive(m_filteringContext.isActive, true);
    m_filteringContext.preeditChanged = false;
    m_compositionResult = { };

    bool handled = webkit_input_method_context_filter_key_event(m_context.get(), keyEvent);
    if (!handled)
        return { };

    // Simple input methods work such that even normal keystrokes fire the commit signal without any preedit change.
    if (!m_filteringContext.preeditChanged && m_compositionResult.length() == 1)
        return { false, WTFMove(m_compositionResult) };

    if (!platformEventKeyIsKeyPress(keyEvent))
        return { };

    return { true, { } };
}

bool InputMethodFilter::isViewFocused() const
{
    if (!m_enabled || !m_context)
        return false;

#if ENABLE(DEVELOPER_MODE) && PLATFORM(X11)
    // Xvfb doesn't support toplevel focus, so the WebView is never focused. We simply assume the WebView is focused
    // since it's the only application running.
    if (PlatformDisplay::sharedDisplay().type() == PlatformDisplay::Type::X11) {
        if (!g_strcmp0(g_getenv("UNDER_XVFB"), "yes"))
            return true;
    }
#endif

    auto* webView = webkitInputMethodContextGetWebView(m_context.get());
    ASSERT(webView);
    return webkitWebViewGetPage(webView).isViewFocused();
}

void InputMethodFilter::notifyFocusedIn()
{
    if (!m_enabled || !m_context)
        return;

    webkit_input_method_context_notify_focus_in(m_context.get());
}

void InputMethodFilter::notifyFocusedOut()
{
    if (!m_enabled || !m_context)
        return;

    cancelComposition();
    webkit_input_method_context_notify_focus_out(m_context.get());
}

void InputMethodFilter::notifyCursorRect(const IntRect& cursorRect)
{
    if (!m_enabled || !m_context)
        return;

    // Don't move the window unless the cursor actually moves more than 10
    // pixels. This prevents us from making the window flash during minor
    // cursor adjustments.
    static const int windowMovementThreshold = 10 * 10;
    if (cursorRect.location().distanceSquaredToPoint(m_cursorLocation) < windowMovementThreshold)
        return;

    m_cursorLocation = cursorRect.location();
    auto translatedRect = platformTransformCursorRectToViewCoordinates(cursorRect);
    webkit_input_method_context_notify_cursor_area(m_context.get(), translatedRect.x(), translatedRect.y(), translatedRect.width(), translatedRect.height());
}

void InputMethodFilter::preeditStarted()
{
    if (!m_enabled)
        return;

    if (m_filteringContext.isActive)
        m_filteringContext.preeditChanged = true;

    m_preedit = { };
}

void InputMethodFilter::preeditChanged()
{
    if (!m_enabled)
        return;

    if (m_filteringContext.isActive)
        m_filteringContext.preeditChanged = true;

    GUniqueOutPtr<gchar> newPreedit;
    GList* underlines = nullptr;
    unsigned cursorOffset;
    webkit_input_method_context_get_preedit(m_context.get(), &newPreedit.outPtr(), &underlines, &cursorOffset);

    if (m_preedit.text.utf8() == newPreedit.get()) {
        g_list_free_full(underlines, reinterpret_cast<GDestroyNotify>(webkit_input_method_underline_free));
        return;
    }

    m_preedit.text = String::fromUTF8(newPreedit.get());
    m_preedit.cursorOffset = std::min(cursorOffset, m_preedit.text.length());
    if (underlines) {
        for (auto* it = underlines; it; it = g_list_next(it)) {
            auto* underline = static_cast<WebKitInputMethodUnderline*>(it->data);
            m_preedit.underlines.append(webkitInputMethodUnderlineGetCompositionUnderline(underline));
        }
        g_list_free_full(underlines, reinterpret_cast<GDestroyNotify>(webkit_input_method_underline_free));
    } else
        m_preedit.underlines.append(CompositionUnderline(0, m_preedit.text.length(), CompositionUnderlineColor::TextColor, Color(Color::black), false));

    auto* webView = webkitInputMethodContextGetWebView(m_context.get());
    ASSERT(webView);
    webkitWebViewSetComposition(webView, m_preedit.text, m_preedit.underlines, EditingRange(m_preedit.cursorOffset, 1));
}

void InputMethodFilter::preeditFinished()
{
    if (!m_enabled)
        return;

    if (m_filteringContext.isActive)
        m_filteringContext.preeditChanged = true;

    bool wasEmpty = m_preedit.text.isEmpty();
    m_preedit = { };

    if (wasEmpty)
        return;

    auto* webView = webkitInputMethodContextGetWebView(m_context.get());
    ASSERT(webView);
    webkitWebViewSetComposition(webView, { }, { }, EditingRange(0, 1));
}

void InputMethodFilter::committed(const char* compositionString)
{
    if (!m_enabled)
        return;

    m_compositionResult = String::fromUTF8(compositionString);
    bool preeditWasEmpty = m_preedit.text.isEmpty();
    m_preedit = { };

    auto* webView = webkitInputMethodContextGetWebView(m_context.get());
    ASSERT(webView);
    if (m_filteringContext.isActive) {
        if (!m_filteringContext.preeditChanged && preeditWasEmpty && m_compositionResult.length() == 1)
            return;
    }
    webkitWebViewConfirmComposition(webView, m_compositionResult);
    m_compositionResult = { };
}

void InputMethodFilter::cancelComposition()
{
    if (m_preedit.text.isNull())
        return;

    auto* webView = webkitInputMethodContextGetWebView(m_context.get());
    ASSERT(webView);
    webkitWebViewCancelComposition(webView, m_preedit.text);
    m_preedit = { };
    m_compositionResult = { };
    webkit_input_method_context_reset(m_context.get());
}

} // namespace WebKit