blob: 2f25b9963fa03b35389956bf00d3ef4e99607411 [file] [log] [blame]
/*
* 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::deleteSurroundingCallback(InputMethodFilter* filter, int offset, unsigned characterCount)
{
filter->deleteSurrounding(offset, characterCount);
}
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);
g_signal_connect_swapped(m_context.get(), "delete-surrounding", G_CALLBACK(deleteSurroundingCallback), this);
if (isEnabled() && isViewFocused())
notifyFocusedIn();
}
void InputMethodFilter::setState(Optional<InputMethodState>&& state)
{
if (!state)
notifyFocusedOut();
m_state = WTFMove(state);
if (isEnabled() && isViewFocused())
notifyFocusedIn();
}
InputMethodFilter::FilterResult InputMethodFilter::filterKeyEvent(PlatformEventKey* keyEvent)
{
if (!isEnabled() || !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 (!isEnabled() || !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();
}
static WebKitInputPurpose toWebKitPurpose(InputMethodState::Purpose purpose)
{
switch (purpose) {
case InputMethodState::Purpose::FreeForm:
return WEBKIT_INPUT_PURPOSE_FREE_FORM;
case InputMethodState::Purpose::Digits:
return WEBKIT_INPUT_PURPOSE_DIGITS;
case InputMethodState::Purpose::Number:
return WEBKIT_INPUT_PURPOSE_NUMBER;
case InputMethodState::Purpose::Phone:
return WEBKIT_INPUT_PURPOSE_PHONE;
case InputMethodState::Purpose::Url:
return WEBKIT_INPUT_PURPOSE_URL;
case InputMethodState::Purpose::Email:
return WEBKIT_INPUT_PURPOSE_EMAIL;
case InputMethodState::Purpose::Password:
return WEBKIT_INPUT_PURPOSE_PASSWORD;
}
RELEASE_ASSERT_NOT_REACHED();
}
static WebKitInputHints toWebKitHints(const OptionSet<InputMethodState::Hint>& hints)
{
unsigned wkHints = 0;
if (hints.contains(InputMethodState::Hint::Spellcheck))
wkHints |= WEBKIT_INPUT_HINT_SPELLCHECK;
if (hints.contains(InputMethodState::Hint::Lowercase))
wkHints |= WEBKIT_INPUT_HINT_LOWERCASE;
if (hints.contains(InputMethodState::Hint::UppercaseChars))
wkHints |= WEBKIT_INPUT_HINT_UPPERCASE_CHARS;
if (hints.contains(InputMethodState::Hint::UppercaseWords))
wkHints |= WEBKIT_INPUT_HINT_UPPERCASE_WORDS;
if (hints.contains(InputMethodState::Hint::UppercaseSentences))
wkHints |= WEBKIT_INPUT_HINT_UPPERCASE_SENTENCES;
if (hints.contains(InputMethodState::Hint::InhibitOnScreenKeyboard))
wkHints |= WEBKIT_INPUT_HINT_INHIBIT_OSK;
return static_cast<WebKitInputHints>(wkHints);
}
void InputMethodFilter::notifyFocusedIn()
{
if (!isEnabled() || !m_context)
return;
g_object_freeze_notify(G_OBJECT(m_context.get()));
webkit_input_method_context_set_input_purpose(m_context.get(), toWebKitPurpose(m_state->purpose));
webkit_input_method_context_set_input_hints(m_context.get(), toWebKitHints(m_state->hints));
g_object_thaw_notify(G_OBJECT(m_context.get()));
webkit_input_method_context_notify_focus_in(m_context.get());
}
void InputMethodFilter::notifyFocusedOut()
{
if (!isEnabled() || !m_context)
return;
cancelComposition();
webkit_input_method_context_notify_focus_out(m_context.get());
}
void InputMethodFilter::notifyCursorRect(const IntRect& cursorRect)
{
if (!isEnabled() || !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::notifySurrounding(const String& text, uint64_t cursorPosition)
{
if (!isEnabled() || !m_context)
return;
if (m_surrounding.text == text && m_surrounding.cursorPosition == cursorPosition)
return;
m_surrounding.text = text;
m_surrounding.cursorPosition = cursorPosition;
auto textUTF8 = m_surrounding.text.utf8();
auto cursorPositionUTF8 = cursorPosition != text.length() ? text.substring(0, cursorPosition).utf8().length() : textUTF8.length();
webkit_input_method_context_notify_surrounding(m_context.get(), textUTF8.data(), textUTF8.length(), cursorPositionUTF8);
}
void InputMethodFilter::preeditStarted()
{
if (!isEnabled())
return;
if (m_filteringContext.isActive)
m_filteringContext.preeditChanged = true;
m_preedit = { };
}
void InputMethodFilter::preeditChanged()
{
if (!isEnabled())
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 (!isEnabled())
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 (!isEnabled())
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::deleteSurrounding(int offset, unsigned characterCount)
{
if (!isEnabled())
return;
auto* webView = webkitInputMethodContextGetWebView(m_context.get());
ASSERT(webView);
webkitWebViewDeleteSurrounding(webView, offset, characterCount);
}
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