Blob Blame History Raw
From d6dd815014564f235fb972eb72d28ccca6cf3549 Mon Sep 17 00:00:00 2001
From: Aleix Pol <aleixpol@kde.org>
Date: Wed, 10 Mar 2021 01:09:13 +0100
Subject: [PATCH 06/40] client: Allow QWaylandInputContext to accept composed
 key combinations

At the moment, we are forcing user to choose to either compose or use
the text-input channel. This patch brings some of the QComposeInputContext
functionality in order to let applications understand dead key
combinations like they are supposed to.

Having it in QWaylandInputContext rather than in QWaylandInputDevice
should solve the problems 3aedd01271dc4f4a13103d632df224971ab2b6df had
with 57c4af2b18c0fb1d266b245a107fa6cb876b9d9e, because we are doing it
in the input context rather than before. This way, if the user is
overriding the input method (e.g. by setting QT_IM_MODULE), all the key
strokes will still be properly forwarded to the module to use.

This in turn allows us to solve https://bugs.kde.org/show_bug.cgi?id=411729
and https://bugs.kde.org/show_bug.cgi?id=405388 since we don't need to
choose anymore between physical and virual keyboards anymore.

Pick-to: 5.15
Change-Id: I8601f5d7ae21edf4b3a1191fa75877286e505588
Reviewed-by: David Edmundson <davidedmundson@kde.org>
---
 src/client/qwaylanddisplay_p.h      |  3 -
 src/client/qwaylandinputcontext.cpp | 95 ++++++++++++++++++++++++++++-
 src/client/qwaylandinputcontext_p.h | 21 +++++++
 src/client/qwaylandinputdevice.cpp  |  2 +-
 src/client/qwaylandintegration.cpp  |  8 +--
 5 files changed, 119 insertions(+), 10 deletions(-)

diff --git a/src/client/qwaylanddisplay_p.h b/src/client/qwaylanddisplay_p.h
index 188e9131..3b092bc8 100644
--- a/src/client/qwaylanddisplay_p.h
+++ b/src/client/qwaylanddisplay_p.h
@@ -175,8 +175,6 @@ public:
     QWaylandHardwareIntegration *hardwareIntegration() const { return mHardwareIntegration.data(); }
     QWaylandXdgOutputManagerV1 *xdgOutputManager() const { return mXdgOutputManager.data(); }
 
-    bool usingInputContextFromCompositor() const { return mUsingInputContextFromCompositor; }
-
     struct RegistryGlobal {
         uint32_t id;
         QString interface;
@@ -282,7 +280,6 @@ private:
     QReadWriteLock m_frameQueueLock;
 
     bool mClientSideInputContextRequested = !QPlatformInputContextFactory::requested().isNull();
-    bool mUsingInputContextFromCompositor = false;
 
     void registry_global(uint32_t id, const QString &interface, uint32_t version) override;
     void registry_global_remove(uint32_t id) override;
diff --git a/src/client/qwaylandinputcontext.cpp b/src/client/qwaylandinputcontext.cpp
index e9afe05e..ef5aa375 100644
--- a/src/client/qwaylandinputcontext.cpp
+++ b/src/client/qwaylandinputcontext.cpp
@@ -406,6 +406,8 @@ bool QWaylandInputContext::isValid() const
 void QWaylandInputContext::reset()
 {
     qCDebug(qLcQpaInputMethods) << Q_FUNC_INFO;
+    if (m_composeState)
+        xkb_compose_state_reset(m_composeState);
 
     QPlatformInputContext::reset();
 
@@ -526,9 +528,14 @@ Qt::LayoutDirection QWaylandInputContext::inputDirection() const
     return textInput()->inputDirection();
 }
 
-void QWaylandInputContext::setFocusObject(QObject *)
+void QWaylandInputContext::setFocusObject(QObject *object)
 {
     qCDebug(qLcQpaInputMethods) << Q_FUNC_INFO;
+#if QT_CONFIG(xkbcommon)
+    m_focusObject = object;
+#else
+    Q_UNUSED(object);
+#endif
 
     if (!textInput())
         return;
@@ -561,6 +568,92 @@ QWaylandTextInput *QWaylandInputContext::textInput() const
     return mDisplay->defaultInputDevice()->textInput();
 }
 
+#if QT_CONFIG(xkbcommon)
+
+void QWaylandInputContext::ensureInitialized()
+{
+    if (m_initialized)
+        return;
+
+    if (!m_XkbContext) {
+        qCWarning(qLcQpaInputMethods) << "error: xkb context has not been set on" << metaObject()->className();
+        return;
+    }
+
+    m_initialized = true;
+    const char *locale = setlocale(LC_CTYPE, "");
+    if (!locale)
+        locale = setlocale(LC_CTYPE, nullptr);
+    qCDebug(qLcQpaInputMethods) << "detected locale (LC_CTYPE):" << locale;
+
+    m_composeTable = xkb_compose_table_new_from_locale(m_XkbContext, locale, XKB_COMPOSE_COMPILE_NO_FLAGS);
+    if (m_composeTable)
+        m_composeState = xkb_compose_state_new(m_composeTable, XKB_COMPOSE_STATE_NO_FLAGS);
+
+    if (!m_composeTable) {
+        qCWarning(qLcQpaInputMethods, "failed to create compose table");
+        return;
+    }
+    if (!m_composeState) {
+        qCWarning(qLcQpaInputMethods, "failed to create compose state");
+        return;
+    }
+}
+
+bool QWaylandInputContext::filterEvent(const QEvent *event)
+{
+    auto keyEvent = static_cast<const QKeyEvent *>(event);
+    if (keyEvent->type() != QEvent::KeyPress)
+        return false;
+
+    if (!inputMethodAccepted())
+        return false;
+
+    // lazy initialization - we don't want to do this on an app startup
+    ensureInitialized();
+
+    if (!m_composeTable || !m_composeState)
+        return false;
+
+    xkb_compose_state_feed(m_composeState, keyEvent->nativeVirtualKey());
+
+    switch (xkb_compose_state_get_status(m_composeState)) {
+    case XKB_COMPOSE_COMPOSING:
+        return true;
+    case XKB_COMPOSE_CANCELLED:
+        reset();
+        return false;
+    case XKB_COMPOSE_COMPOSED:
+    {
+        const int size = xkb_compose_state_get_utf8(m_composeState, nullptr, 0);
+        QVarLengthArray<char, 32> buffer(size + 1);
+        xkb_compose_state_get_utf8(m_composeState, buffer.data(), buffer.size());
+        QString composedText = QString::fromUtf8(buffer.constData());
+
+        QInputMethodEvent event;
+        event.setCommitString(composedText);
+
+        if (!m_focusObject && qApp)
+            m_focusObject = qApp->focusObject();
+
+        if (m_focusObject)
+            QCoreApplication::sendEvent(m_focusObject, &event);
+        else
+            qCWarning(qLcQpaInputMethods, "no focus object");
+
+        reset();
+        return true;
+    }
+    case XKB_COMPOSE_NOTHING:
+        return false;
+    default:
+        Q_UNREACHABLE();
+        return false;
+    }
+}
+
+#endif
+
 }
 
 QT_END_NAMESPACE
diff --git a/src/client/qwaylandinputcontext_p.h b/src/client/qwaylandinputcontext_p.h
index 10132dfe..50db6344 100644
--- a/src/client/qwaylandinputcontext_p.h
+++ b/src/client/qwaylandinputcontext_p.h
@@ -61,6 +61,10 @@
 
 #include <QtWaylandClient/private/qwayland-text-input-unstable-v2.h>
 #include <qwaylandinputmethodeventbuilder_p.h>
+#include <qtwaylandclientglobal_p.h>
+#if QT_CONFIG(xkbcommon)
+#include <xkbcommon/xkbcommon-compose.h>
+#endif
 
 struct wl_callback;
 struct wl_callback_listener;
@@ -155,11 +159,28 @@ public:
 
     void setFocusObject(QObject *object) override;
 
+#if QT_CONFIG(xkbcommon)
+    bool filterEvent(const QEvent *event) override;
+
+    // This invokable is called from QXkbCommon::setXkbContext().
+    Q_INVOKABLE void setXkbContext(struct xkb_context *context) { m_XkbContext = context; }
+#endif
+
 private:
     QWaylandTextInput *textInput() const;
 
     QWaylandDisplay *mDisplay = nullptr;
     QPointer<QWindow> mCurrentWindow;
+
+#if QT_CONFIG(xkbcommon)
+    void ensureInitialized();
+
+    bool m_initialized = false;
+    QObject *m_focusObject = nullptr;
+    xkb_compose_table *m_composeTable = nullptr;
+    xkb_compose_state *m_composeState = nullptr;
+    struct xkb_context *m_XkbContext = nullptr;
+#endif
 };
 
 }
diff --git a/src/client/qwaylandinputdevice.cpp b/src/client/qwaylandinputdevice.cpp
index ed4a0eb4..ae045f4f 100644
--- a/src/client/qwaylandinputdevice.cpp
+++ b/src/client/qwaylandinputdevice.cpp
@@ -1201,7 +1201,7 @@ void QWaylandInputDevice::Keyboard::handleKey(ulong timestamp, QEvent::Type type
     QPlatformInputContext *inputContext = QGuiApplicationPrivate::platformIntegration()->inputContext();
     bool filtered = false;
 
-    if (inputContext && !mParent->mQDisplay->usingInputContextFromCompositor()) {
+    if (inputContext) {
         QKeyEvent event(type, key, modifiers, nativeScanCode, nativeVirtualKey,
                         nativeModifiers, text, autorepeat, count);
         event.setTimestamp(timestamp);
diff --git a/src/client/qwaylandintegration.cpp b/src/client/qwaylandintegration.cpp
index 7ad8e05e..c53ccb78 100644
--- a/src/client/qwaylandintegration.cpp
+++ b/src/client/qwaylandintegration.cpp
@@ -474,13 +474,11 @@ void QWaylandIntegration::reconfigureInputContext()
 
 #if QT_CONFIG(xkbcommon)
     QXkbCommon::setXkbContext(mInputContext.data(), mDisplay->xkbContext());
+    if (QWaylandInputContext* waylandInput = qobject_cast<QWaylandInputContext*>(mInputContext.get())) {
+        waylandInput->setXkbContext(mDisplay->xkbContext());
+    }
 #endif
 
-    // Even if compositor-side input context handling has been requested, we fallback to
-    // client-side handling if compositor does not provide the text-input extension. This
-    // is why we need to check here which input context actually is being used.
-    mDisplay->mUsingInputContextFromCompositor = qobject_cast<QWaylandInputContext *>(mInputContext.data());
-
     qCDebug(lcQpaWayland) << "using input method:" << inputContext()->metaObject()->className();
 }
 
-- 
2.35.1