diff --git a/qml/virtualkeyboard/main.qml b/qml/virtualkeyboard/main.qml index 5887e876d..ef1a967f3 100644 --- a/qml/virtualkeyboard/main.qml +++ b/qml/virtualkeyboard/main.qml @@ -1,84 +1,65 @@ /******************************************************************** KWin - the KDE window manager This file is part of the KDE project. Copyright (C) 2016 Martin Gräßlin This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program 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 General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . *********************************************************************/ import QtQuick 2.0 -import QtQuick.Window 2.0 import QtQuick.Controls 2.3 import QtQuick.VirtualKeyboard 2.1 import org.kde.kirigami 2.5 as Kirigami -Window { +Item { id: window property real adjustment: 0 property real adjustmentFactor: 0.0 - visible: false - - Loader { - anchors.fill: parent - - sourceComponent: window.visible ? theKeyboard : null - - Component { - id: theKeyboard - Item { - InputPanel { - id: inputPanel - - readonly property real sideMargin: window.width * window.adjustmentFactor/2 - anchors { - left: parent.left - right: parent.right - bottom: parent.bottom - leftMargin: sideMargin - rightMargin: sideMargin - } - } - //NOTE: ToolButton for some reasons breaks the virtual keyboard loading on Plasma Mobile - Button { - id: resizeButton - visible: !Kirigami.Settings.isMobile //don't show on handheld devices - flat: true - display: AbstractButton.IconOnly - icon.name: "transform-scale" - icon.color: "white" - down: mouseArea.pressed + InputPanel { + id: inputPanel + objectName: "inputPanel" + width: parent.width - parent.width * parent.adjustmentFactor + anchors.horizontalCenter: parent.horizontalCenter + anchors.bottom: parent.bottom + } + //NOTE: ToolButton for some reasons breaks the virtual keyboard loading on Plasma Mobile + Button { + id: resizeButton + visible: !Kirigami.Settings.isMobile //don't show on handheld devices + flat: true + display: AbstractButton.IconOnly + icon.name: "transform-scale" + icon.color: "white" + down: mouseArea.pressed - anchors { - right: inputPanel.right - top: inputPanel.top - } + anchors { + right: inputPanel.right + top: inputPanel.top + } - MouseArea { - id: mouseArea - property real startPoint: 0 - anchors.fill: parent - onPressed: { - startPoint = mouse.x; - } - onPositionChanged: { - window.adjustment -= (mouse.x - startPoint); - window.adjustmentFactor = Math.min(Math.max(window.adjustment / window.width, 0.0), 0.66); - startPoint = mouse.x; - } - } - } + MouseArea { + id: mouseArea + property real startPoint: 0 + anchors.fill: parent + onPressed: { + startPoint = mouse.x; + } + onPositionChanged: { + window.adjustment -= (mouse.x - startPoint); + window.adjustmentFactor = Math.min(Math.max(window.adjustment / window.width, 0.0), 0.66); + startPoint = mouse.x; } } } } diff --git a/virtualkeyboard.cpp b/virtualkeyboard.cpp index b799c5d94..7ee7e5e2c 100644 --- a/virtualkeyboard.cpp +++ b/virtualkeyboard.cpp @@ -1,510 +1,498 @@ /******************************************************************** KWin - the KDE window manager This file is part of the KDE project. Copyright (C) 2016 Martin Gräßlin This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program 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 General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . *********************************************************************/ #include "virtualkeyboard.h" #include "virtualkeyboard_dbus.h" #include "input.h" #include "keyboard_input.h" #include "utils.h" #include "screens.h" #include "wayland_server.h" #include "workspace.h" #include "xkb.h" #include "shell_client.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include +#include #include -#include #include // xkbcommon #include using namespace KWayland::Server; namespace KWin { KWIN_SINGLETON_FACTORY(VirtualKeyboard) VirtualKeyboard::VirtualKeyboard(QObject *parent) : QObject(parent) { m_floodTimer = new QTimer(this); m_floodTimer->setSingleShot(true); m_floodTimer->setInterval(250); // this is actually too late. Other processes are started before init, // so might miss the availability of text input // but without Workspace we don't have the window listed at all connect(kwinApp(), &Application::workspaceCreated, this, &VirtualKeyboard::init); } VirtualKeyboard::~VirtualKeyboard() = default; void VirtualKeyboard::init() { // TODO: need a shared Qml engine - const QUrl source = QUrl::fromLocalFile(QStandardPaths::locate(QStandardPaths::GenericDataLocation, QStringLiteral(KWIN_NAME "/virtualkeyboard/main.qml"))); - qCDebug(KWIN_VIRTUALKEYBOARD) << "Initializing window" << source; - QQmlApplicationEngine* engine = new QQmlApplicationEngine(this); - connect(engine, &QQmlEngine::warnings, this, [] (const QList &warnings) { qCWarning(KWIN_VIRTUALKEYBOARD ) << warnings; }); - engine->load(source); - - if (engine->rootObjects().isEmpty()) { - qCWarning(KWIN_VIRTUALKEYBOARD) << "could not load the virtual keyboard"; + qCDebug(KWIN_VIRTUALKEYBOARD) << "Initializing window"; + m_inputWindow.reset(new QQuickView(nullptr)); + m_inputWindow->setFlags(Qt::FramelessWindowHint); + m_inputWindow->setGeometry(screens()->geometry(screens()->current())); + m_inputWindow->setResizeMode(QQuickView::SizeRootObjectToView); + m_inputWindow->setSource(QUrl::fromLocalFile(QStandardPaths::locate(QStandardPaths::GenericDataLocation, QStringLiteral(KWIN_NAME "/virtualkeyboard/main.qml")))); + if (m_inputWindow->status() != QQuickView::Status::Ready) { + qCWarning(KWIN_VIRTUALKEYBOARD) << "window not ready yet"; m_inputWindow.reset(); return; } - - m_inputWindow.reset(qobject_cast(engine->rootObjects().constFirst())); - m_inputWindow->setFlags(Qt::FramelessWindowHint); m_inputWindow->setProperty("__kwin_input_method", true); if (waylandServer()) { m_enabled = !input()->hasAlphaNumericKeyboard(); qCDebug(KWIN_VIRTUALKEYBOARD) << "enabled by default: " << m_enabled; connect(input(), &InputRedirection::hasAlphaNumericKeyboardChanged, this, [this] (bool set) { qCDebug(KWIN_VIRTUALKEYBOARD) << "AlphaNumeric Keyboard changed:" << set << "toggling VirtualKeyboard."; setEnabled(!set); } ); } qCDebug(KWIN_VIRTUALKEYBOARD) << "Registering the SNI"; m_sni = new KStatusNotifierItem(QStringLiteral("kwin-virtual-keyboard"), this); m_sni->setStandardActionsEnabled(false); m_sni->setCategory(KStatusNotifierItem::Hardware); m_sni->setStatus(KStatusNotifierItem::Passive); m_sni->setTitle(i18n("Virtual Keyboard")); updateSni(); connect(m_sni, &KStatusNotifierItem::activateRequested, this, [this] { setEnabled(!m_enabled); } ); connect(this, &VirtualKeyboard::enabledChanged, this, &VirtualKeyboard::updateSni); auto dbus = new VirtualKeyboardDBus(this); qCDebug(KWIN_VIRTUALKEYBOARD) << "Registering the DBus interface"; dbus->setEnabled(m_enabled); connect(dbus, &VirtualKeyboardDBus::activateRequested, this, &VirtualKeyboard::setEnabled); connect(this, &VirtualKeyboard::enabledChanged, dbus, &VirtualKeyboardDBus::setEnabled); if (waylandServer()) { // we can announce support for the text input interface auto t = waylandServer()->display()->createTextInputManager(TextInputInterfaceVersion::UnstableV0, waylandServer()->display()); t->create(); auto t2 = waylandServer()->display()->createTextInputManager(TextInputInterfaceVersion::UnstableV2, waylandServer()->display()); t2->create(); connect(waylandServer()->seat(), &SeatInterface::focusedTextInputChanged, this, [this] { disconnect(m_waylandShowConnection); disconnect(m_waylandHideConnection); disconnect(m_waylandHintsConnection); disconnect(m_waylandSurroundingTextConnection); disconnect(m_waylandResetConnection); disconnect(m_waylandEnabledConnection); qApp->inputMethod()->reset(); if (auto t = waylandServer()->seat()->focusedTextInput()) { m_waylandShowConnection = connect(t, &TextInputInterface::requestShowInputPanel, this, &VirtualKeyboard::show); m_waylandHideConnection = connect(t, &TextInputInterface::requestHideInputPanel, this, &VirtualKeyboard::hide); m_waylandSurroundingTextConnection = connect(t, &TextInputInterface::surroundingTextChanged, this, - [this] { - if (m_enabled) { - m_inputWindow->show(); - } + [] { qApp->inputMethod()->update(Qt::ImSurroundingText | Qt::ImCursorPosition | Qt::ImAnchorPosition); } ); m_waylandHintsConnection = connect(t, &TextInputInterface::contentTypeChanged, this, - [this] { - if (m_enabled) { - m_inputWindow->show(); - } + [] { qApp->inputMethod()->update(Qt::ImHints); } ); m_waylandResetConnection = connect(t, &TextInputInterface::requestReset, qApp->inputMethod(), &QInputMethod::reset); m_waylandEnabledConnection = connect(t, &TextInputInterface::enabledChanged, this, - [this] { - if (m_enabled) { - m_inputWindow->show(); - } + [] { qApp->inputMethod()->update(Qt::ImQueryAll); } ); auto newClient = waylandServer()->findAbstractClient(waylandServer()->seat()->focusedTextInputSurface()); // Reset the old client virtual keybaord geom if necessary // Old and new clients could be the same if focus moves between subsurfaces if (newClient != m_trackedClient) { if (m_trackedClient) { m_trackedClient->setVirtualKeyboardGeometry(QRect()); } m_trackedClient = newClient; } m_trackedClient = waylandServer()->findAbstractClient(waylandServer()->seat()->focusedTextInputSurface()); updateInputPanelState(); } else { m_waylandShowConnection = QMetaObject::Connection(); m_waylandHideConnection = QMetaObject::Connection(); m_waylandHintsConnection = QMetaObject::Connection(); m_waylandSurroundingTextConnection = QMetaObject::Connection(); m_waylandResetConnection = QMetaObject::Connection(); m_waylandEnabledConnection = QMetaObject::Connection(); } qApp->inputMethod()->update(Qt::ImQueryAll); } ); } m_inputWindow->installEventFilter(this); connect(Workspace::self(), &Workspace::destroyed, this, [this] { m_inputWindow.reset(); } ); m_inputWindow->setColor(Qt::transparent); - m_inputWindow->setMask(m_inputWindow->contentItem()->childrenRect().toRect()); - connect(m_inputWindow->contentItem(), &QQuickItem::childrenRectChanged, m_inputWindow.data(), + m_inputWindow->setMask(m_inputWindow->rootObject()->childrenRect().toRect()); + connect(m_inputWindow->rootObject(), &QQuickItem::childrenRectChanged, m_inputWindow.data(), [this] { if (!m_inputWindow) { return; } - m_inputWindow->setMask(m_inputWindow->contentItem()->childrenRect().toRect()); + m_inputWindow->setMask(m_inputWindow->rootObject()->childrenRect().toRect()); } ); connect(qApp->inputMethod(), &QInputMethod::visibleChanged, this, &VirtualKeyboard::updateInputPanelState); - connect(m_inputWindow->contentItem(), &QQuickItem::childrenRectChanged, this, &VirtualKeyboard::updateInputPanelState); + connect(m_inputWindow->rootObject(), &QQuickItem::childrenRectChanged, this, &VirtualKeyboard::updateInputPanelState); } void VirtualKeyboard::setEnabled(bool enabled) { if (m_enabled == enabled) { return; } m_enabled = enabled; qApp->inputMethod()->update(Qt::ImQueryAll); emit enabledChanged(m_enabled); // send OSD message QDBusMessage msg = QDBusMessage::createMethodCall( QStringLiteral("org.kde.plasmashell"), QStringLiteral("/org/kde/osdService"), QStringLiteral("org.kde.osdService"), QStringLiteral("virtualKeyboardEnabledChanged") ); msg.setArguments({enabled}); QDBusConnection::sessionBus().asyncCall(msg); } void VirtualKeyboard::updateSni() { if (!m_sni) { return; } if (m_enabled) { m_sni->setIconByName(QStringLiteral("input-keyboard-virtual-on")); m_sni->setTitle(i18n("Virtual Keyboard: enabled")); } else { m_sni->setIconByName(QStringLiteral("input-keyboard-virtual-off")); m_sni->setTitle(i18n("Virtual Keyboard: disabled")); } m_sni->setToolTipTitle(i18n("Whether to show the virtual keyboard on demand.")); } void VirtualKeyboard::updateInputPanelState() { if (!waylandServer()) { return; } auto t = waylandServer()->seat()->focusedTextInput(); if (!t || !m_inputWindow) { return; } const bool inputPanelHasBeenClosed = m_inputWindow->isVisible() && !qApp->inputMethod()->isVisible(); if (inputPanelHasBeenClosed && m_floodTimer->isActive()) { return; } m_floodTimer->start(); m_inputWindow->setVisible(qApp->inputMethod()->isVisible()); if (qApp->inputMethod()->isVisible()) { - m_inputWindow->setMask(m_inputWindow->contentItem()->childrenRect().toRect()); + m_inputWindow->setMask(m_inputWindow->rootObject()->childrenRect().toRect()); } - if (m_inputWindow->isVisible() && m_trackedClient && m_inputWindow) { - const QRect inputPanelGeom = m_inputWindow->contentItem()->childrenRect().toRect().translated(m_inputWindow->geometry().topLeft()); + if (m_inputWindow->isVisible() && m_trackedClient && m_inputWindow->rootObject()) { + const QRect inputPanelGeom = m_inputWindow->rootObject()->childrenRect().toRect().translated(m_inputWindow->geometry().topLeft()); m_trackedClient->setVirtualKeyboardGeometry(inputPanelGeom); t->setInputPanelState(true, QRect(0, 0, 0, 0)); } else { if (inputPanelHasBeenClosed && m_trackedClient) { m_trackedClient->setVirtualKeyboardGeometry(QRect()); } t->setInputPanelState(false, QRect(0, 0, 0, 0)); } } void VirtualKeyboard::show() { if (m_inputWindow.isNull() || !m_enabled) { return; } m_inputWindow->setGeometry(screens()->geometry(screens()->current())); qApp->inputMethod()->show(); } void VirtualKeyboard::hide() { - if (!m_inputWindow) { + if (m_inputWindow.isNull()) { return; } qApp->inputMethod()->hide(); } bool VirtualKeyboard::event(QEvent *e) { if (e->type() == QEvent::InputMethod) { QInputMethodEvent *event = static_cast(e); if (m_enabled && waylandServer()) { bool isPreedit = false; for (auto attribute : event->attributes()) { switch (attribute.type) { case QInputMethodEvent::TextFormat: case QInputMethodEvent::Cursor: case QInputMethodEvent::Language: case QInputMethodEvent::Ruby: isPreedit = true; break; default: break; } } TextInputInterface *ti = waylandServer()->seat()->focusedTextInput(); if (ti && ti->isEnabled()) { if (!isPreedit && event->preeditString().isEmpty() && !event->commitString().isEmpty()) { ti->commit(event->commitString().toUtf8()); } else { ti->preEdit(event->preeditString().toUtf8(), event->commitString().toUtf8()); } } } } if (e->type() == QEvent::InputMethodQuery) { auto event = static_cast(e); TextInputInterface *ti = nullptr; if (waylandServer() && m_enabled) { ti = waylandServer()->seat()->focusedTextInput(); } if (event->queries().testFlag(Qt::ImEnabled)) { event->setValue(Qt::ImEnabled, QVariant(ti != nullptr && ti->isEnabled())); } if (event->queries().testFlag(Qt::ImCursorRectangle)) { // not used by virtual keyboard } if (event->queries().testFlag(Qt::ImFont)) { // not used by virtual keyboard } if (event->queries().testFlag(Qt::ImCursorPosition)) { // the virtual keyboard doesn't send us the cursor position in the preedit // this would break text input, thus we ignore it // see https://bugreports.qt.io/browse/QTBUG-53517 #if 0 event->setValue(Qt::ImCursorPosition, QString::fromUtf8(ti->surroundingText().left(ti->surroundingTextCursorPosition())).size()); #else event->setValue(Qt::ImCursorPosition, 0); #endif } if (event->queries().testFlag(Qt::ImSurroundingText)) { // the virtual keyboard doesn't send us the cursor position in the preedit // this would break text input, thus we ignore it // see https://bugreports.qt.io/browse/QTBUG-53517 #if 0 event->setValue(Qt::ImSurroundingText, QString::fromUtf8(ti->surroundingText())); #else event->setValue(Qt::ImSurroundingText, QString()); #endif } if (event->queries().testFlag(Qt::ImCurrentSelection)) { // TODO: should be text between cursor and anchor, but might be dangerous } if (event->queries().testFlag(Qt::ImMaximumTextLength)) { // not used by virtual keyboard } if (event->queries().testFlag(Qt::ImAnchorPosition)) { // not used by virtual keyboard } if (event->queries().testFlag(Qt::ImHints)) { if (ti && ti->isEnabled()) { Qt::InputMethodHints hints; const auto contentHints = ti->contentHints(); if (!contentHints.testFlag(TextInputInterface::ContentHint::AutoCompletion)) { hints |= Qt::ImhNoPredictiveText; } if (contentHints.testFlag(TextInputInterface::ContentHint::AutoCorrection)) { // no mapping in Qt } if (!contentHints.testFlag(TextInputInterface::ContentHint::AutoCapitalization)) { hints |= Qt::ImhNoAutoUppercase; } if (contentHints.testFlag(TextInputInterface::ContentHint::LowerCase)) { hints |= Qt::ImhPreferLowercase; } if (contentHints.testFlag(TextInputInterface::ContentHint::UpperCase)) { hints |= Qt::ImhPreferUppercase; } if (contentHints.testFlag(TextInputInterface::ContentHint::TitleCase)) { // no mapping in Qt } if (contentHints.testFlag(TextInputInterface::ContentHint::HiddenText)) { hints |= Qt::ImhHiddenText; } if (contentHints.testFlag(TextInputInterface::ContentHint::SensitiveData)) { hints |= Qt::ImhSensitiveData; } if (contentHints.testFlag(TextInputInterface::ContentHint::Latin)) { hints |= Qt::ImhPreferLatin; } if (contentHints.testFlag(TextInputInterface::ContentHint::MultiLine)) { hints |= Qt::ImhMultiLine; } switch (ti->contentPurpose()) { case TextInputInterface::ContentPurpose::Digits: hints |= Qt::ImhDigitsOnly; break; case TextInputInterface::ContentPurpose::Number: hints |= Qt::ImhFormattedNumbersOnly; break; case TextInputInterface::ContentPurpose::Phone: hints |= Qt::ImhDialableCharactersOnly; break; case TextInputInterface::ContentPurpose::Url: hints |= Qt::ImhUrlCharactersOnly; break; case TextInputInterface::ContentPurpose::Email: hints |= Qt::ImhEmailCharactersOnly; break; case TextInputInterface::ContentPurpose::Date: hints |= Qt::ImhDate; break; case TextInputInterface::ContentPurpose::Time: hints |= Qt::ImhTime; break; case TextInputInterface::ContentPurpose::DateTime: hints |= Qt::ImhDate; hints |= Qt::ImhTime; break; case TextInputInterface::ContentPurpose::Name: // no mapping in Qt case TextInputInterface::ContentPurpose::Password: // no mapping in Qt case TextInputInterface::ContentPurpose::Terminal: // no mapping in Qt case TextInputInterface::ContentPurpose::Normal: // that's the default case TextInputInterface::ContentPurpose::Alpha: // no mapping in Qt break; } event->setValue(Qt::ImHints, QVariant(int(hints))); } else { event->setValue(Qt::ImHints, Qt::ImhNone); } } if (event->queries().testFlag(Qt::ImPreferredLanguage)) { // not used by virtual keyboard } if (event->queries().testFlag(Qt::ImPlatformData)) { // not used by virtual keyboard } if (event->queries().testFlag(Qt::ImAbsolutePosition)) { // not used by virtual keyboard } if (event->queries().testFlag(Qt::ImTextBeforeCursor)) { // not used by virtual keyboard } if (event->queries().testFlag(Qt::ImTextAfterCursor)) { // not used by virtual keyboard } event->accept(); return true; } return QObject::event(e); } bool VirtualKeyboard::eventFilter(QObject *o, QEvent *e) { if (o != m_inputWindow.data() || !m_inputWindow->isVisible()) { return false; } if (e->type() == QEvent::KeyPress || e->type() == QEvent::KeyRelease) { QKeyEvent *event = static_cast(e); if (event->nativeScanCode() == 0) { // this is a key composed by the virtual keyboard - we need to send it to the client const auto sym = input()->keyboard()->xkb()->fromKeyEvent(event); if (sym != 0) { if (waylandServer()) { auto t = waylandServer()->seat()->focusedTextInput(); if (t && t->isEnabled()) { if (e->type() == QEvent::KeyPress) { t->keysymPressed(sym); } else if (e->type() == QEvent::KeyRelease) { t->keysymReleased(sym); } } } } return true; } } return false; } QWindow *VirtualKeyboard::inputPanel() const { return m_inputWindow.data(); } } diff --git a/virtualkeyboard.h b/virtualkeyboard.h index 641bdfd1c..21d2bad33 100644 --- a/virtualkeyboard.h +++ b/virtualkeyboard.h @@ -1,79 +1,79 @@ /******************************************************************** KWin - the KDE window manager This file is part of the KDE project. Copyright (C) 2016 Martin Gräßlin This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program 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 General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . *********************************************************************/ #ifndef KWIN_VIRTUAL_KEYBOARD_H #define KWIN_VIRTUAL_KEYBOARD_H #include #include #include #include -class QQuickWindow; +class QQuickView; class QTimer; class QWindow; class KStatusNotifierItem; namespace KWin { class KWIN_EXPORT VirtualKeyboard : public QObject { Q_OBJECT public: ~VirtualKeyboard() override; void init(); bool event(QEvent *e) override; bool eventFilter(QObject *o, QEvent *event) override; QWindow *inputPanel() const; Q_SIGNALS: void enabledChanged(bool enabled); private: void show(); void hide(); void setEnabled(bool enable); void updateSni(); void updateInputPanelState(); bool m_enabled = false; KStatusNotifierItem *m_sni = nullptr; - QScopedPointer m_inputWindow; + QScopedPointer m_inputWindow; QPointer m_trackedClient; // If a surface loses focus immediately after being resized by the keyboard, don't react to it to avoid resize loops QTimer *m_floodTimer; QMetaObject::Connection m_waylandShowConnection; QMetaObject::Connection m_waylandHideConnection; QMetaObject::Connection m_waylandHintsConnection; QMetaObject::Connection m_waylandSurroundingTextConnection; QMetaObject::Connection m_waylandResetConnection; QMetaObject::Connection m_waylandEnabledConnection; KWIN_SINGLETON(VirtualKeyboard) }; } #endif