From 6578795ab5de18d1a39c3a31c619a4414eca7453 Mon Sep 17 00:00:00 2001 From: Eskil Abrahamsen Blomfeldt Date: Fri, 14 Aug 2020 09:51:07 +0200 Subject: [PATCH] New input method protocol for use with Qt clients We introduce an alternative input-method protocol, which is a one-to-one mapping to Qt's input method API. Input methods such as the virtual keyboard's hunspell integration is quite sensitive to the inner workings of the input method handling, both in terms of when state is updated and which updates are delivered when. With a one-to-one mapping we are able to match these expectations and keep a well-synchronized state. Task-number: QTBUG-85135 Task-number: QTBUG-85134 Change-Id: Id69c22a7b0885ea59f39fdcc8d663749af56c7ce Reviewed-by: Paul Olav Tvete --- .../qt-text-input-method-unstable-v1.xml | 301 +++++++++++++ .../platforms/wayland/.prev_CMakeLists.txt | 3 +- src/plugins/platforms/wayland/CMakeLists.txt | 2 + src/plugins/platforms/wayland/client.pro | 3 + .../platforms/wayland/qwaylanddisplay.cpp | 13 + .../platforms/wayland/qwaylanddisplay_p.h | 3 + .../platforms/wayland/qwaylandinputdevice.cpp | 14 + .../platforms/wayland/qwaylandinputdevice_p.h | 5 + .../wayland/qwaylandinputmethodcontext.cpp | 421 ++++++++++++++++++ .../wayland/qwaylandinputmethodcontext_p.h | 152 +++++++ .../platforms/wayland/qwaylandintegration.cpp | 11 +- 11 files changed, 924 insertions(+), 4 deletions(-) create mode 100644 src/3rdparty/wayland/extensions/qt-text-input-method-unstable-v1.xml create mode 100644 src/plugins/platforms/wayland/qwaylandinputmethodcontext.cpp create mode 100644 src/plugins/platforms/wayland/qwaylandinputmethodcontext_p.h diff --git a/src/3rdparty/wayland/extensions/qt-text-input-method-unstable-v1.xml b/src/3rdparty/wayland/extensions/qt-text-input-method-unstable-v1.xml new file mode 100644 index 00000000000..2e8cd4ec3a6 --- /dev/null +++ b/src/3rdparty/wayland/extensions/qt-text-input-method-unstable-v1.xml @@ -0,0 +1,301 @@ + + + + + Copyright © 2020 The Qt Company Ltd. + + Permission to use, copy, modify, distribute, and sell this + software and its documentation for any purpose is hereby granted + without fee, provided that the above copyright notice appear in + all copies and that both that copyright notice and this permission + notice appear in supporting documentation, and that the name of + the copyright holders not be used in advertising or publicity + pertaining to distribution of the software without specific, + written prior permission. The copyright holders make no + representations about the suitability of this software for any + purpose. It is provided "as is" without express or implied + warranty. + + THE COPYRIGHT HOLDERS DISCLAIM ALL WARRANTIES WITH REGARD TO THIS + SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND + FITNESS, IN NO EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY + SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN + AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, + ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF + THIS SOFTWARE. + + + + + The qt_text_input_method interface represents input method events + associated with a seat, and is intended to exactly match the + internal events of the Qt framework. + + + + + Destroy the qt_text_input_method object. + + + + + + Enable text input in a surface (usually when a text entry inside of it + has focus). + + This can be called before or after a surface gets text (or keyboard) + focus via the enter event. Text input to a surface is only active + when it has the current text (or keyboard) focus and is enabled. + + + + + + + Disable text input in a surface (typically when there is no focus on any + text entry inside the surface). + + + + + + + Notification that this seat's text-input focus is on a certain surface. + + When the seat has the keyboard capability the text-input focus follows + the keyboard focus. + + + + + + + Notification that this seat's text-input focus is no longer on + a certain surface. + + The leave notification is sent before the enter notification + for the new focus. + + When the seat has the keyboard capability the text-input focus follows + the keyboard focus. + + + + + + + A QKeyEvent has been sent from the input method. + + + + + + + + + + + + + + + Starts an input method event. This can be followed by + any number of input_method_event_attribute events + and will always be finished by an end_input_method_event. + + + + + + + + Appends an attribute to the input method event with + the given serial. Must be preceded by a start_input_method_event + and concluded by a an end_input_method_event. See documentation + of QInputMethodEvent for details on the attributes. + + + + + + + + + + + Concludes a previously started input method event. Together with + the preceding input_method_event_attribute events with the same + serial, this should be converted into a QInputMethodEvent. + + + + + + + + + + + Event to notify client that the visibility of the input method has + been changed. + + + + + + + Event to notify client that the keyboard rectangle of the input method + has been changed. + + + + + + + + + + Event to notify client that the locale of the input method + has been changed. + + + + + + + Event to notify client that the input direction of the input method + has been changed. + + + + + + + Request for the input method to reset. Corresponds to QInputMethod::reset(). + + + + + + Request for the input method to commit its current content. Corresponds to QInputMethod::commit(). + + + + + + Passes a mouse click or context menu request from the client to the server. Corresponds to QInputMethod::invokeAction(). + + + + + + + + Notifies the server of the client's current input method hints. + + + + + + + Notifies the server of the client's current cursor rectangle. + + + + + + + + + + Notifies the server of the client's current cursor position. + + + + + + + Notifies the server of the client's current surrounding text and its offset in the + complete text. + + + + + + + + Notifies the server of the client's current anchor position. + + + + + + + Notifies the server of the client's current absolute cursor position. + + + + + + + Notifies the server of the client's current preferred language. + + + + + + + Starts an update sequence to notify the server that the client's state has + changed. This is followed by any number of update requests for specific + parts of the state and concluded by an end_update request. + + + + + + + Concludes the previously started update request. + + + + + + Requests that the input panel of the input method is visible. + + + + + + Requests that the input panel of the input method is not visible. + + + + + + Sent on receipt of an end_input_method_event to acknowledge that + the client has received and handled the event. + + + + + + + Manages qt_text_input_method objects. + + + + + Destroy the qt_text_input_method_manager object. + + + + + + Creates a new text-input-method object for a given seat. + + + + + + diff --git a/src/plugins/platforms/wayland/.prev_CMakeLists.txt b/src/plugins/platforms/wayland/.prev_CMakeLists.txt index ac323a56f3f..454b06ae38f 100644 --- a/src/plugins/platforms/wayland/.prev_CMakeLists.txt +++ b/src/plugins/platforms/wayland/.prev_CMakeLists.txt @@ -30,6 +30,7 @@ qt_add_module(WaylandClient qwaylandextendedsurface.cpp qwaylandextendedsurface_p.h qwaylandinputcontext.cpp qwaylandinputcontext_p.h qwaylandinputdevice.cpp qwaylandinputdevice_p.h + qwaylandinputmethodcontext.cpp qwaylandinputmethodcontext_p.h qwaylandintegration.cpp qwaylandintegration_p.h qwaylandnativeinterface.cpp qwaylandnativeinterface_p.h qwaylandqtkey.cpp qwaylandqtkey_p.h @@ -56,7 +57,6 @@ qt_add_module(WaylandClient LIBRARIES Qt::CorePrivate Qt::GuiPrivate - Qt::PlatformHeadersPrivate PUBLIC_LIBRARIES Qt::Core Qt::Gui @@ -74,6 +74,7 @@ qt6_generate_wayland_protocol_client_sources(WaylandClient ${CMAKE_CURRENT_SOURCE_DIR}/../3rdparty/protocol/wp-primary-selection-unstable-v1.xml ${CMAKE_CURRENT_SOURCE_DIR}/../3rdparty/protocol/xdg-output-unstable-v1.xml ${CMAKE_CURRENT_SOURCE_DIR}/../extensions/qt-key-unstable-v1.xml + ${CMAKE_CURRENT_SOURCE_DIR}/../extensions/qt-text-input-method-unstable-v1.xml ${CMAKE_CURRENT_SOURCE_DIR}/../extensions/qt-windowmanager.xml ${CMAKE_CURRENT_SOURCE_DIR}/../extensions/surface-extension.xml ${CMAKE_CURRENT_SOURCE_DIR}/../extensions/touch-extension.xml diff --git a/src/plugins/platforms/wayland/CMakeLists.txt b/src/plugins/platforms/wayland/CMakeLists.txt index 88b27b5f3a9..01957eacd7f 100644 --- a/src/plugins/platforms/wayland/CMakeLists.txt +++ b/src/plugins/platforms/wayland/CMakeLists.txt @@ -30,6 +30,7 @@ qt_add_module(WaylandClient qwaylandextendedsurface.cpp qwaylandextendedsurface_p.h qwaylandinputcontext.cpp qwaylandinputcontext_p.h qwaylandinputdevice.cpp qwaylandinputdevice_p.h + qwaylandinputmethodcontext.cpp qwaylandinputmethodcontext_p.h qwaylandintegration.cpp qwaylandintegration_p.h qwaylandnativeinterface.cpp qwaylandnativeinterface_p.h qwaylandqtkey.cpp qwaylandqtkey_p.h @@ -73,6 +74,7 @@ qt6_generate_wayland_protocol_client_sources(WaylandClient ${CMAKE_CURRENT_SOURCE_DIR}/../3rdparty/protocol/wp-primary-selection-unstable-v1.xml ${CMAKE_CURRENT_SOURCE_DIR}/../3rdparty/protocol/xdg-output-unstable-v1.xml ${CMAKE_CURRENT_SOURCE_DIR}/../extensions/qt-key-unstable-v1.xml + ${CMAKE_CURRENT_SOURCE_DIR}/../extensions/qt-text-input-method-unstable-v1.xml ${CMAKE_CURRENT_SOURCE_DIR}/../extensions/qt-windowmanager.xml ${CMAKE_CURRENT_SOURCE_DIR}/../extensions/surface-extension.xml ${CMAKE_CURRENT_SOURCE_DIR}/../extensions/touch-extension.xml diff --git a/src/plugins/platforms/wayland/client.pro b/src/plugins/platforms/wayland/client.pro index d6df946b58e..da6a926dc48 100644 --- a/src/plugins/platforms/wayland/client.pro +++ b/src/plugins/platforms/wayland/client.pro @@ -22,6 +22,7 @@ WAYLANDCLIENTSOURCES += \ ../extensions/touch-extension.xml \ ../extensions/qt-key-unstable-v1.xml \ ../extensions/qt-windowmanager.xml \ + ../extensions/qt-text-input-method-unstable-v1.xml \ ../3rdparty/protocol/wp-primary-selection-unstable-v1.xml \ ../3rdparty/protocol/tablet-unstable-v2.xml \ ../3rdparty/protocol/text-input-unstable-v2.xml \ @@ -50,6 +51,7 @@ SOURCES += qwaylandintegration.cpp \ qwaylanddecorationplugin.cpp \ qwaylandwindowmanagerintegration.cpp \ qwaylandinputcontext.cpp \ + qwaylandinputmethodcontext.cpp \ qwaylandshm.cpp \ qwaylandbuffer.cpp \ @@ -74,6 +76,7 @@ HEADERS += qwaylandintegration_p.h \ qwaylanddecorationplugin_p.h \ qwaylandwindowmanagerintegration_p.h \ qwaylandinputcontext_p.h \ + qwaylandinputmethodcontext_p.h \ qwaylandshm_p.h \ qtwaylandclientglobal.h \ qtwaylandclientglobal_p.h \ diff --git a/src/plugins/platforms/wayland/qwaylanddisplay.cpp b/src/plugins/platforms/wayland/qwaylanddisplay.cpp index 9e5359357d8..e39f912809b 100644 --- a/src/plugins/platforms/wayland/qwaylanddisplay.cpp +++ b/src/plugins/platforms/wayland/qwaylanddisplay.cpp @@ -61,6 +61,7 @@ #endif #include "qwaylandhardwareintegration_p.h" #include "qwaylandinputcontext_p.h" +#include "qwaylandinputmethodcontext_p.h" #include "qwaylandwindowmanagerintegration_p.h" #include "qwaylandshellintegration_p.h" @@ -74,6 +75,7 @@ #include #include +#include #include @@ -339,6 +341,11 @@ void QWaylandDisplay::registry_global(uint32_t id, const QString &interface, uin } else if (interface == QStringLiteral("zwp_primary_selection_device_manager_v1")) { mPrimarySelectionManager.reset(new QWaylandPrimarySelectionDeviceManagerV1(this, id, 1)); #endif + } else if (interface == QStringLiteral("qt_text_input_method_manager_v1") && !mClientSideInputContextRequested) { + mTextInputMethodManager.reset(new QtWayland::qt_text_input_method_manager_v1(registry, id, 1)); + for (QWaylandInputDevice *inputDevice : qAsConst(mInputDevices)) + inputDevice->setTextInputMethod(new QWaylandTextInputMethod(this, mTextInputMethodManager->get_text_input_method(inputDevice->wl_seat()))); + mWaylandIntegration->reconfigureInputContext(); } else if (interface == QStringLiteral("zwp_text_input_manager_v2") && !mClientSideInputContextRequested) { mTextInputManager.reset(new QtWayland::zwp_text_input_manager_v2(registry, id, 1)); for (QWaylandInputDevice *inputDevice : qAsConst(mInputDevices)) @@ -396,6 +403,12 @@ void QWaylandDisplay::registry_global_remove(uint32_t id) inputDevice->setTextInput(nullptr); mWaylandIntegration->reconfigureInputContext(); } + if (global.interface == QStringLiteral("qt_text_input_method_manager_v1")) { + mTextInputMethodManager.reset(); + for (QWaylandInputDevice *inputDevice : qAsConst(mInputDevices)) + inputDevice->setTextInputMethod(nullptr); + mWaylandIntegration->reconfigureInputContext(); + } mGlobals.removeAt(i); break; } diff --git a/src/plugins/platforms/wayland/qwaylanddisplay_p.h b/src/plugins/platforms/wayland/qwaylanddisplay_p.h index d386c66ac68..68d72812f52 100644 --- a/src/plugins/platforms/wayland/qwaylanddisplay_p.h +++ b/src/plugins/platforms/wayland/qwaylanddisplay_p.h @@ -81,6 +81,7 @@ class QPlatformPlaceholderScreen; namespace QtWayland { class qt_surface_extension; class zwp_text_input_manager_v2; + class qt_text_input_method_manager_v1; } namespace QtWaylandClient { @@ -163,6 +164,7 @@ public: QtWayland::qt_surface_extension *windowExtension() const { return mWindowExtension.data(); } QWaylandTabletManagerV2 *tabletManager() const { return mTabletManager.data(); } QWaylandTouchExtension *touchExtension() const { return mTouchExtension.data(); } + QtWayland::qt_text_input_method_manager_v1 *textInputMethodManager() const { return mTextInputMethodManager.data(); } QtWayland::zwp_text_input_manager_v2 *textInputManager() const { return mTextInputManager.data(); } QWaylandHardwareIntegration *hardwareIntegration() const { return mHardwareIntegration.data(); } QWaylandXdgOutputManagerV1 *xdgOutputManager() const { return mXdgOutputManager.data(); } @@ -268,6 +270,7 @@ private: #if QT_CONFIG(wayland_client_primary_selection) QScopedPointer mPrimarySelectionManager; #endif + QScopedPointer mTextInputMethodManager; QScopedPointer mTextInputManager; QScopedPointer mHardwareIntegration; QScopedPointer mXdgOutputManager; diff --git a/src/plugins/platforms/wayland/qwaylandinputdevice.cpp b/src/plugins/platforms/wayland/qwaylandinputdevice.cpp index e86274276f0..343ccc8d7e2 100644 --- a/src/plugins/platforms/wayland/qwaylandinputdevice.cpp +++ b/src/plugins/platforms/wayland/qwaylandinputdevice.cpp @@ -57,6 +57,7 @@ #include "qwaylanddisplay_p.h" #include "qwaylandshmbackingstore_p.h" #include "qwaylandinputcontext_p.h" +#include "qwaylandinputmethodcontext_p.h" #include #include @@ -420,6 +421,9 @@ QWaylandInputDevice::QWaylandInputDevice(QWaylandDisplay *display, int version, if (mQDisplay->textInputManager()) mTextInput.reset(new QWaylandTextInput(mQDisplay, mQDisplay->textInputManager()->get_text_input(wl_seat()))); + if (mQDisplay->textInputMethodManager()) + mTextInputMethod.reset(new QWaylandTextInputMethod(mQDisplay, mQDisplay->textInputMethodManager()->get_text_input_method(wl_seat()))); + if (auto *tm = mQDisplay->tabletManager()) mTabletSeat.reset(new QWaylandTabletSeatV2(tm, this)); } @@ -535,11 +539,21 @@ void QWaylandInputDevice::setTextInput(QWaylandTextInput *textInput) mTextInput.reset(textInput); } +void QWaylandInputDevice::setTextInputMethod(QWaylandTextInputMethod *textInputMethod) +{ + mTextInputMethod.reset(textInputMethod); +} + QWaylandTextInput *QWaylandInputDevice::textInput() const { return mTextInput.data(); } +QWaylandTextInputMethod *QWaylandInputDevice::textInputMethod() const +{ + return mTextInputMethod.data(); +} + void QWaylandInputDevice::removeMouseButtonFromState(Qt::MouseButton button) { if (mPointer) diff --git a/src/plugins/platforms/wayland/qwaylandinputdevice_p.h b/src/plugins/platforms/wayland/qwaylandinputdevice_p.h index 31bd3008849..53691e68254 100644 --- a/src/plugins/platforms/wayland/qwaylandinputdevice_p.h +++ b/src/plugins/platforms/wayland/qwaylandinputdevice_p.h @@ -91,6 +91,7 @@ class QWaylandPrimarySelectionDeviceV1; #endif class QWaylandTabletSeatV2; class QWaylandTextInput; +class QWaylandTextInputMethod; #if QT_CONFIG(cursor) class QWaylandCursorTheme; class CursorSurface; @@ -134,6 +135,9 @@ public: void setTextInput(QWaylandTextInput *textInput); QWaylandTextInput *textInput() const; + void setTextInputMethod(QWaylandTextInputMethod *textInputMethod); + QWaylandTextInputMethod *textInputMethod() const; + void removeMouseButtonFromState(Qt::MouseButton button); QWaylandWindow *pointerFocus() const; @@ -187,6 +191,7 @@ private: Touch *mTouch = nullptr; QScopedPointer mTextInput; + QScopedPointer mTextInputMethod; QScopedPointer mTabletSeat; uint32_t mTime = 0; diff --git a/src/plugins/platforms/wayland/qwaylandinputmethodcontext.cpp b/src/plugins/platforms/wayland/qwaylandinputmethodcontext.cpp new file mode 100644 index 00000000000..e6ab1da4ff1 --- /dev/null +++ b/src/plugins/platforms/wayland/qwaylandinputmethodcontext.cpp @@ -0,0 +1,421 @@ +/**************************************************************************** +** +** Copyright (C) 2020 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the plugins of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 3 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL3 included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 3 requirements +** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 2.0 or (at your option) the GNU General +** Public license version 3 or any later version approved by the KDE Free +** Qt Foundation. The licenses are as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-2.0.html and +** https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include "qwaylandinputmethodcontext_p.h" +#include "qwaylanddisplay_p.h" +#include "qwaylandinputdevice_p.h" + +#include +#include + +QT_BEGIN_NAMESPACE + +Q_DECLARE_LOGGING_CATEGORY(qLcQpaInputMethods) + +namespace QtWaylandClient { + +static constexpr int maxStringSize = 1000; // actual max is 4096/3 + +QWaylandTextInputMethod::QWaylandTextInputMethod(QWaylandDisplay *display, struct ::qt_text_input_method_v1 *textInputMethod) + : QtWayland::qt_text_input_method_v1(textInputMethod) + , m_display(display) +{ +} + +QWaylandTextInputMethod::~QWaylandTextInputMethod() +{ +} + +void QWaylandTextInputMethod::text_input_method_v1_visible_changed(int32_t visible) +{ + m_isVisible = visible; +} + +void QWaylandTextInputMethod::text_input_method_v1_locale_changed(const QString &localeName) +{ + m_locale = QLocale(localeName); +} + +void QWaylandTextInputMethod::text_input_method_v1_input_direction_changed(int32_t inputDirection) +{ + m_layoutDirection = Qt::LayoutDirection(inputDirection); +} + +void QWaylandTextInputMethod::text_input_method_v1_keyboard_rectangle_changed(wl_fixed_t x, wl_fixed_t y, wl_fixed_t width, wl_fixed_t height) +{ + m_keyboardRect = QRectF(wl_fixed_to_double(x), + wl_fixed_to_double(y), + wl_fixed_to_double(width), + wl_fixed_to_double(height)); +} + +void QWaylandTextInputMethod::text_input_method_v1_start_input_method_event(uint32_t serial, int32_t surrounding_text_offset) +{ + if (m_pendingInputMethodEvents.contains(serial)) { + qCWarning(qLcQpaInputMethods) << "Input method event with serial" << serial << "already started"; + return; + } + + m_pendingInputMethodEvents[serial] = QList{}; + m_offsetFromCompositor[serial] = surrounding_text_offset; +} + +// We need to keep surrounding text below maxStringSize characters, with cursorPos centered in that substring + +static int calculateOffset(const QString &text, int cursorPos) +{ + int size = text.size(); + int halfSize = maxStringSize/2; + if (size <= maxStringSize || cursorPos < halfSize) + return 0; + if (cursorPos > size - halfSize) + return size - maxStringSize; + return cursorPos - halfSize; +} + +static QString mapSurroundingTextToCompositor(const QString &s, int offset) +{ + return s.mid(offset, maxStringSize); +} + +static int mapPositionToCompositor(int pos, int offset) +{ + return pos - offset; +} + +static int mapPositionFromCompositor(int pos, int offset) +{ + return pos + offset; +} + +void QWaylandTextInputMethod::text_input_method_v1_input_method_event_attribute(uint32_t serial, int32_t type, int32_t start, int32_t length, const QString &value) +{ + if (!m_pendingInputMethodEvents.contains(serial)) { + qCWarning(qLcQpaInputMethods) << "Input method event with serial" << serial << "does not exist"; + return; + } + + int startMapped = mapPositionFromCompositor(start, m_offsetFromCompositor[serial]); + QList &attributes = m_pendingInputMethodEvents[serial]; + switch (type) { + case QInputMethodEvent::Selection: + attributes.append(QInputMethodEvent::Attribute(QInputMethodEvent::AttributeType(type), startMapped, length)); + break; + case QInputMethodEvent::Cursor: + attributes.append(QInputMethodEvent::Attribute(QInputMethodEvent::AttributeType(type), start, length, QColor(value))); + break; + case QInputMethodEvent::TextFormat: + { + QTextCharFormat textFormat; + textFormat.setProperty(QTextFormat::FontUnderline, true); + textFormat.setProperty(QTextFormat::TextUnderlineStyle, QTextCharFormat::SingleUnderline); + attributes.append(QInputMethodEvent::Attribute(QInputMethodEvent::AttributeType(type), start, length, textFormat)); + break; + } + case QInputMethodEvent::Language: + case QInputMethodEvent::Ruby: + attributes.append(QInputMethodEvent::Attribute(QInputMethodEvent::AttributeType(type), start, length, value)); + break; + }; +} + +void QWaylandTextInputMethod::sendInputState(QInputMethodQueryEvent *event, Qt::InputMethodQueries queries) +{ + int cursorPosition = event->value(Qt::ImCursorPosition).toInt(); + int anchorPosition = event->value(Qt::ImAnchorPosition).toInt(); + QString surroundingText = event->value(Qt::ImSurroundingText).toString(); + int offset = calculateOffset(surroundingText, cursorPosition); + + if (queries & Qt::ImCursorPosition) + update_cursor_position(mapPositionToCompositor(cursorPosition, offset)); + if (queries & Qt::ImSurroundingText) + update_surrounding_text(mapSurroundingTextToCompositor(surroundingText, offset), offset); + if (queries & Qt::ImAnchorPosition) + update_anchor_position(mapPositionToCompositor(anchorPosition, offset)); + if (queries & Qt::ImAbsolutePosition) + update_absolute_position(event->value(Qt::ImAbsolutePosition).toInt()); // do not map: this is the position in the whole document +} + + +void QWaylandTextInputMethod::text_input_method_v1_end_input_method_event(uint32_t serial, const QString &commitString, const QString &preeditString, int32_t replacementStart, int32_t replacementLength) +{ + if (!m_pendingInputMethodEvents.contains(serial)) { + qCWarning(qLcQpaInputMethods) << "Input method event with serial" << serial << "does not exist"; + return; + } + + QList attributes = m_pendingInputMethodEvents.take(serial); + m_offsetFromCompositor.remove(serial); + if (QGuiApplication::focusObject() != nullptr) { + QInputMethodEvent event(preeditString, attributes); + event.setCommitString(commitString, replacementStart, replacementLength); + QCoreApplication::sendEvent(QGuiApplication::focusObject(), &event); + } + + // Send current state to make sure it matches + if (QGuiApplication::focusObject() != nullptr) { + QInputMethodQueryEvent event(Qt::ImCursorPosition | Qt::ImSurroundingText | Qt::ImAnchorPosition | Qt::ImAbsolutePosition); + QCoreApplication::sendEvent(QGuiApplication::focusObject(), &event); + sendInputState(&event); + } + + acknowledge_input_method(); +} + +void QWaylandTextInputMethod::text_input_method_v1_key(int32_t type, + int32_t key, + int32_t modifiers, + int32_t autoRepeat, + int32_t count, + int32_t nativeScanCode, + int32_t nativeVirtualKey, + int32_t nativeModifiers, + const QString &text) +{ + if (QGuiApplication::focusObject() != nullptr) { + QKeyEvent event(QKeyEvent::Type(type), + key, + Qt::KeyboardModifiers(modifiers), + nativeScanCode, + nativeVirtualKey, + nativeModifiers, + text, + autoRepeat, + count); + QCoreApplication::sendEvent(QGuiApplication::focusObject(), &event); + } +} + +void QWaylandTextInputMethod::text_input_method_v1_enter(struct ::wl_surface *surface) +{ + m_surface = surface; +} + +void QWaylandTextInputMethod::text_input_method_v1_leave(struct ::wl_surface *surface) +{ + if (surface != m_surface) { + qCWarning(qLcQpaInputMethods) << "Got leave event for surface without corresponding enter"; + } else { + m_surface = nullptr; + } +} + +QWaylandInputMethodContext::QWaylandInputMethodContext(QWaylandDisplay *display) + : m_display(display) +{ +} + +QWaylandInputMethodContext::~QWaylandInputMethodContext() +{ +} + +bool QWaylandInputMethodContext::isValid() const +{ + return m_display->textInputMethodManager() != nullptr; +} + +void QWaylandInputMethodContext::reset() +{ + QWaylandTextInputMethod *inputMethod = textInputMethod(); + if (inputMethod != nullptr) + inputMethod->reset(); +} + +void QWaylandInputMethodContext::commit() +{ + QWaylandTextInputMethod *inputMethod = textInputMethod(); + if (inputMethod != nullptr) + inputMethod->commit(); + + m_display->forceRoundTrip(); +} + +void QWaylandInputMethodContext::update(Qt::InputMethodQueries queries) +{ + wl_surface *currentSurface = m_currentWindow != nullptr && m_currentWindow->handle() != nullptr + ? static_cast(m_currentWindow->handle())->wlSurface() + : nullptr; + if (currentSurface != nullptr && !inputMethodAccepted()) { + textInputMethod()->disable(currentSurface); + m_currentWindow.clear(); + } else if (currentSurface == nullptr && inputMethodAccepted()) { + QWindow *window = QGuiApplication::focusWindow(); + currentSurface = window != nullptr && window->handle() != nullptr + ? static_cast(window->handle())->wlSurface() + : nullptr; + if (currentSurface != nullptr) { + textInputMethod()->disable(currentSurface); + m_currentWindow = window; + } + } + + queries &= (Qt::ImEnabled + | Qt::ImHints + | Qt::ImCursorRectangle + | Qt::ImCursorPosition + | Qt::ImSurroundingText + | Qt::ImCurrentSelection + | Qt::ImAnchorPosition + | Qt::ImTextAfterCursor + | Qt::ImTextBeforeCursor + | Qt::ImPreferredLanguage); + + const Qt::InputMethodQueries queriesNeedingOffset = Qt::ImCursorPosition | Qt::ImSurroundingText | Qt::ImAnchorPosition; + if (queries & queriesNeedingOffset) + queries |= queriesNeedingOffset; + + QWaylandTextInputMethod *inputMethod = textInputMethod(); + if (inputMethod != nullptr && QGuiApplication::focusObject() != nullptr) { + QInputMethodQueryEvent event(queries); + QCoreApplication::sendEvent(QGuiApplication::focusObject(), &event); + + inputMethod->start_update(int(queries)); + + if (queries & Qt::ImHints) + inputMethod->update_hints(event.value(Qt::ImHints).toInt()); + + if (queries & Qt::ImCursorRectangle) { + QRect rect = event.value(Qt::ImCursorRectangle).toRect(); + inputMethod->update_cursor_rectangle(rect.x(), rect.y(), rect.width(), rect.height()); + } + + inputMethod->sendInputState(&event, queries); + + if (queries & Qt::ImPreferredLanguage) + inputMethod->update_preferred_language(event.value(Qt::ImPreferredLanguage).toString()); + + inputMethod->end_update(); + + // ### Should we do a display sync here and ignore all events until it is received? + } +} + +void QWaylandInputMethodContext::invokeAction(QInputMethod::Action action, int cursorPosition) +{ + QWaylandTextInputMethod *inputMethod = textInputMethod(); + if (inputMethod != nullptr) + inputMethod->invoke_action(int(action), cursorPosition); +} + +void QWaylandInputMethodContext::showInputPanel() +{ + QWaylandTextInputMethod *inputMethod = textInputMethod(); + if (inputMethod != nullptr) + inputMethod->show_input_panel(); +} + +void QWaylandInputMethodContext::hideInputPanel() +{ + QWaylandTextInputMethod *inputMethod = textInputMethod(); + if (inputMethod != nullptr) + inputMethod->hide_input_panel(); +} + +bool QWaylandInputMethodContext::isInputPanelVisible() const +{ + QWaylandTextInputMethod *inputMethod = textInputMethod(); + if (inputMethod != nullptr) + return inputMethod->isVisible(); + else + return false; +} + +QRectF QWaylandInputMethodContext::keyboardRect() const +{ + QWaylandTextInputMethod *inputMethod = textInputMethod(); + if (inputMethod != nullptr) + return inputMethod->keyboardRect(); + else + return QRectF(); +} + +QLocale QWaylandInputMethodContext::locale() const +{ + QWaylandTextInputMethod *inputMethod = textInputMethod(); + if (inputMethod != nullptr) + return inputMethod->locale(); + else + return QLocale(); +} + +Qt::LayoutDirection QWaylandInputMethodContext::inputDirection() const +{ + QWaylandTextInputMethod *inputMethod = textInputMethod(); + if (inputMethod != nullptr) + return inputMethod->inputDirection(); + else + return Qt::LeftToRight; +} + +void QWaylandInputMethodContext::setFocusObject(QObject *) +{ + QWaylandTextInputMethod *inputMethod = textInputMethod(); + if (inputMethod == nullptr) + return; + + QWindow *window = QGuiApplication::focusWindow(); + + if (m_currentWindow != nullptr && m_currentWindow->handle() != nullptr) { + if (m_currentWindow.data() != window || !inputMethodAccepted()) { + auto *surface = static_cast(m_currentWindow->handle())->wlSurface(); + if (surface) + inputMethod->disable(surface); + m_currentWindow.clear(); + } + } + + if (window != nullptr && window->handle() != nullptr && inputMethodAccepted()) { + if (m_currentWindow.data() != window) { + auto *surface = static_cast(window->handle())->wlSurface(); + if (surface != nullptr) { + inputMethod->enable(surface); + m_currentWindow = window; + } + } + } +} + +QWaylandTextInputMethod *QWaylandInputMethodContext::textInputMethod() const +{ + return m_display->defaultInputDevice()->textInputMethod(); +} + +} // QtWaylandClient + +QT_END_NAMESPACE diff --git a/src/plugins/platforms/wayland/qwaylandinputmethodcontext_p.h b/src/plugins/platforms/wayland/qwaylandinputmethodcontext_p.h new file mode 100644 index 00000000000..8be65f447b3 --- /dev/null +++ b/src/plugins/platforms/wayland/qwaylandinputmethodcontext_p.h @@ -0,0 +1,152 @@ +/**************************************************************************** +** +** Copyright (C) 2020 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the plugins of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 3 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL3 included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 3 requirements +** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 2.0 or (at your option) the GNU General +** Public license version 3 or any later version approved by the KDE Free +** Qt Foundation. The licenses are as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-2.0.html and +** https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#ifndef QWAYLANDINPUTMETHODCONTEXT_P_H +#define QWAYLANDINPUTMETHODCONTEXT_P_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include +#include +#include +#include + +#include + +QT_BEGIN_NAMESPACE + +namespace QtWaylandClient { + class QWaylandDisplay; + +class QWaylandTextInputMethod : public QtWayland::qt_text_input_method_v1 +{ +public: + QWaylandTextInputMethod(QWaylandDisplay *display, struct ::qt_text_input_method_v1 *textInputMethod); + ~QWaylandTextInputMethod() override; + + void text_input_method_v1_visible_changed(int32_t visible) override; + void text_input_method_v1_enter(struct ::wl_surface *surface) override; + void text_input_method_v1_leave(struct ::wl_surface *surface) override; + void text_input_method_v1_locale_changed(const QString &localeName) override; + void text_input_method_v1_input_direction_changed(int32_t inputDirection) override; + void text_input_method_v1_keyboard_rectangle_changed(wl_fixed_t x, wl_fixed_t y, wl_fixed_t width, wl_fixed_t height) override; + void text_input_method_v1_key(int32_t type, int32_t key, int32_t modifiers, int32_t autoRepeat, int32_t count, int32_t nativeScanCode, int32_t nativeVirtualKey, int32_t nativeModifiers, const QString &text) override; + void text_input_method_v1_start_input_method_event(uint32_t serial, int32_t surrounding_text_offset) override; + void text_input_method_v1_end_input_method_event(uint32_t serial, const QString &commitString, const QString &preeditString, int32_t replacementStart, int32_t replacementLength) override; + void text_input_method_v1_input_method_event_attribute(uint32_t serial, int32_t type, int32_t start, int32_t length, const QString &value) override; + + inline bool isVisible() const + { + return m_isVisible; + } + + inline QRectF keyboardRect() const + { + return m_keyboardRect; + } + + inline QLocale locale() const + { + return m_locale; + } + + inline Qt::LayoutDirection inputDirection() const + { + return m_layoutDirection; + } + + void sendInputState(QInputMethodQueryEvent *state, Qt::InputMethodQueries queries = Qt::ImQueryInput); + +private: + QWaylandDisplay *m_display; + + QHash > m_pendingInputMethodEvents; + QHash m_offsetFromCompositor; + + struct ::wl_surface *m_surface; + + // Cached state + bool m_isVisible = false; + QRectF m_keyboardRect; + QLocale m_locale; + Qt::LayoutDirection m_layoutDirection; +}; + +class QWaylandInputMethodContext : public QPlatformInputContext +{ +public: + QWaylandInputMethodContext(QWaylandDisplay *display); + ~QWaylandInputMethodContext() override; + + bool isValid() const override; + void reset() override; + void commit() override; + void update(Qt::InputMethodQueries) override; + void invokeAction(QInputMethod::Action, int cursorPosition) override; + void showInputPanel() override; + void hideInputPanel() override; + + bool isInputPanelVisible() const override; + QRectF keyboardRect() const override; + QLocale locale() const override; + Qt::LayoutDirection inputDirection() const override; + + void setFocusObject(QObject *object) override; + +private: + QWaylandTextInputMethod *textInputMethod() const; + + QWaylandDisplay *m_display; + QPointer m_currentWindow; +}; + +} // QtWaylandClient + +QT_END_NAMESPACE + +#endif // QWAYLANDINPUTMETHODCONTEXT_P_H diff --git a/src/plugins/platforms/wayland/qwaylandintegration.cpp b/src/plugins/platforms/wayland/qwaylandintegration.cpp index 0aa18f09825..764acc2d919 100644 --- a/src/plugins/platforms/wayland/qwaylandintegration.cpp +++ b/src/plugins/platforms/wayland/qwaylandintegration.cpp @@ -43,6 +43,7 @@ #include "qwaylandshmwindow_p.h" #include "qwaylandinputdevice_p.h" #include "qwaylandinputcontext_p.h" +#include "qwaylandinputmethodcontext_p.h" #include "qwaylandshmbackingstore_p.h" #include "qwaylandnativeinterface_p.h" #if QT_CONFIG(clipboard) @@ -465,10 +466,14 @@ void QWaylandIntegration::reconfigureInputContext() qCWarning(lcQpaWayland) << "qtvirtualkeyboard currently is not supported at client-side," " use QT_IM_MODULE=qtvirtualkeyboard at compositor-side."; - if (requested.isNull()) - mInputContext.reset(new QWaylandInputContext(mDisplay.data())); - else + if (requested.isNull()) { + if (mDisplay->textInputMethodManager() != nullptr) + mInputContext.reset(new QWaylandInputMethodContext(mDisplay.data())); + else + mInputContext.reset(new QWaylandInputContext(mDisplay.data())); + } else { mInputContext.reset(QPlatformInputContextFactory::create(requested)); + } const QString defaultInputContext(QStringLiteral("compose")); if ((!mInputContext || !mInputContext->isValid()) && requested != defaultInputContext)