diff --git a/abstract_client.h b/abstract_client.h --- a/abstract_client.h +++ b/abstract_client.h @@ -727,6 +727,18 @@ QRect inputGeometry() const override; + /** + * @returns the geometry of the virtual keyboard + * This geometry is in global coordinates + */ + QRect virtualKeyboardGeometry() const; + + /** + * Sets the geometry of the virtual keyboard, The window may resize itself in order to make space for the keybaord + * This geometry is in global coordinates + */ + void setVirtualKeyboardGeometry(const QRect &geo); + /** * Restores the AbstractClient after it had been hidden due to show on screen edge functionality. * The AbstractClient also gets raised (e.g. Panel mode windows can cover) and the AbstractClient @@ -957,6 +969,7 @@ int borderBottom() const; virtual void changeMaximize(bool horizontal, bool vertical, bool adjust) = 0; virtual void setGeometryRestore(const QRect &geo) = 0; + /** * Called from move after updating the geometry. Can be reimplemented to perform specific tasks. * The base implementation does nothing. @@ -1201,6 +1214,8 @@ friend class GeometryUpdatesBlocker; QRect m_visibleRectBeforeGeometryUpdate; QRect m_geometryBeforeUpdateBlocking; + QRect m_virtualKeyboardGeometry; + QRect m_keyboardGeometryRestore; struct { bool enabled = false; diff --git a/abstract_client.cpp b/abstract_client.cpp --- a/abstract_client.cpp +++ b/abstract_client.cpp @@ -72,6 +72,17 @@ connect(Decoration::DecorationBridge::self(), &QObject::destroyed, this, &AbstractClient::destroyDecoration); + // If the user manually moved the window, don't restore it after the keyboard closes + connect(this, &AbstractClient::clientFinishUserMovedResized, this, [this] () { + m_keyboardGeometryRestore = QRect(); + }); + connect(this, qOverload(&AbstractClient::clientMaximizedStateChanged), this, [this] () { + m_keyboardGeometryRestore = QRect(); + }); + connect(this, &AbstractClient::fullScreenChanged, this, [this] () { + m_keyboardGeometryRestore = QRect(); + }); + // replace on-screen-display on size changes connect(this, &AbstractClient::geometryShapeChanged, this, [this] (Toplevel *c, const QRect &old) { @@ -1871,6 +1882,38 @@ return Toplevel::inputGeometry(); } +QRect AbstractClient::virtualKeyboardGeometry() const +{ + return m_virtualKeyboardGeometry; +} + +void AbstractClient::setVirtualKeyboardGeometry(const QRect &geo) +{ + // No keyboard anymore + if (geo.isEmpty() && !m_keyboardGeometryRestore.isEmpty()) { + setGeometry(m_keyboardGeometryRestore); + m_keyboardGeometryRestore = QRect(); + } else if (geo.isEmpty()) { + return; + // The keyboard has just been opened (rather than resized) save client geometry for a restore + } else if (m_keyboardGeometryRestore.isEmpty()) { + m_keyboardGeometryRestore = geometry(); + } + + m_virtualKeyboardGeometry = geo; + + if (!geo.intersects(m_keyboardGeometryRestore)) { + return; + } + + const QRect availableArea = workspace()->clientArea(MaximizeArea, this); + QRect newWindowGeometry = m_keyboardGeometryRestore; + newWindowGeometry.moveBottom(geo.top()); + newWindowGeometry.setTop(qMax(newWindowGeometry.top(), availableArea.top())); + + setGeometry(newWindowGeometry); +} + bool AbstractClient::dockWantsInput() const { return false; diff --git a/autotests/integration/move_resize_window_test.cpp b/autotests/integration/move_resize_window_test.cpp --- a/autotests/integration/move_resize_window_test.cpp +++ b/autotests/integration/move_resize_window_test.cpp @@ -36,6 +36,7 @@ #include #include #include +#include #include #include @@ -76,6 +77,9 @@ void testAdjustClientGeometryOfAutohidingX11Panel(); void testAdjustClientGeometryOfAutohidingWaylandPanel_data(); void testAdjustClientGeometryOfAutohidingWaylandPanel(); + void testResizeForVirtualKeyboard(); + void testResizeForVirtualKeyboardWithMaximize(); + void testResizeForVirtualKeyboardWithFullScreen(); private: KWayland::Client::ConnectionThread *m_connection = nullptr; @@ -852,6 +856,136 @@ QVERIFY(windowClosedSpy.wait()); } +void MoveResizeWindowTest::testResizeForVirtualKeyboard() +{ + using namespace KWayland::Client; + + QScopedPointer surface(Test::createSurface()); + QVERIFY(!surface.isNull()); + + QScopedPointer shellSurface(Test::createXdgShellSurface( + Test::ShellSurfaceType::XdgShellStable, surface.data())); + QVERIFY(!shellSurface.isNull()); + QSignalSpy configureRequestedSpy(shellSurface.data(), &XdgShellSurface::configureRequested); + + // let's render + auto client = Test::renderAndWaitForShown(surface.data(), QSize(500, 800), Qt::blue); + client->move(100, 300); + QSignalSpy geometryChangedSpy(client, &ShellClient::geometryChanged); + + QCOMPARE(client->geometry(), QRect(100, 300, 500, 800)); + client->setVirtualKeyboardGeometry(QRect(0, 100, 1280, 500)); + configureRequestedSpy.wait(); + + shellSurface->ackConfigure(configureRequestedSpy.last()[2].toInt()); + // render at the new size + Test::render(surface.data(), configureRequestedSpy.last().first().toSize(), Qt::blue); + geometryChangedSpy.wait(); + + QCOMPARE(client->geometry(), QRect(100, 0, 500, 101)); + client->setVirtualKeyboardGeometry(QRect()); + configureRequestedSpy.wait(); + + shellSurface->ackConfigure(configureRequestedSpy.last()[2].toInt()); + // render at the new size + Test::render(surface.data(), configureRequestedSpy.last().first().toSize(), Qt::blue); + geometryChangedSpy.wait(); + QCOMPARE(client->geometry(), QRect(100, 300, 500, 800)); +} + +void MoveResizeWindowTest::testResizeForVirtualKeyboardWithMaximize() +{ + using namespace KWayland::Client; + + QScopedPointer surface(Test::createSurface()); + QVERIFY(!surface.isNull()); + + QScopedPointer shellSurface(Test::createXdgShellSurface( + Test::ShellSurfaceType::XdgShellStable, surface.data())); + QVERIFY(!shellSurface.isNull()); + QSignalSpy configureRequestedSpy(shellSurface.data(), &XdgShellSurface::configureRequested); + + // let's render + auto client = Test::renderAndWaitForShown(surface.data(), QSize(500, 800), Qt::blue); + client->move(100, 300); + QSignalSpy geometryChangedSpy(client, &ShellClient::geometryChanged); + + QCOMPARE(client->geometry(), QRect(100, 300, 500, 800)); + client->setVirtualKeyboardGeometry(QRect(0, 100, 1280, 500)); + configureRequestedSpy.wait(); + + shellSurface->ackConfigure(configureRequestedSpy.last()[2].toInt()); + // render at the new size + Test::render(surface.data(), configureRequestedSpy.last().first().toSize(), Qt::blue); + geometryChangedSpy.wait(); + + QCOMPARE(client->geometry(), QRect(100, 0, 500, 101)); + + client->setMaximize(true, true); + configureRequestedSpy.wait(); + shellSurface->ackConfigure(configureRequestedSpy.last()[2].toInt()); + Test::render(surface.data(), configureRequestedSpy.last().first().toSize(), Qt::blue); + geometryChangedSpy.wait(); + QCOMPARE(client->geometry(), QRect(0, 0, 1280, 1024)); + + + client->setVirtualKeyboardGeometry(QRect()); + QVERIFY(!configureRequestedSpy.wait(10)); + + // render at the size of the configureRequested.. it won't have changed + Test::render(surface.data(), configureRequestedSpy.last().first().toSize(), Qt::blue); + QVERIFY(!geometryChangedSpy.wait(10)); + + // Size will NOT be restored + QCOMPARE(client->geometry(), QRect(0, 0, 1280, 1024)); +} + +void MoveResizeWindowTest::testResizeForVirtualKeyboardWithFullScreen() +{ + using namespace KWayland::Client; + + QScopedPointer surface(Test::createSurface()); + QVERIFY(!surface.isNull()); + + QScopedPointer shellSurface(Test::createXdgShellSurface( + Test::ShellSurfaceType::XdgShellStable, surface.data())); + QVERIFY(!shellSurface.isNull()); + QSignalSpy configureRequestedSpy(shellSurface.data(), &XdgShellSurface::configureRequested); + + // let's render + auto client = Test::renderAndWaitForShown(surface.data(), QSize(500, 800), Qt::blue); + client->move(100, 300); + QSignalSpy geometryChangedSpy(client, &ShellClient::geometryChanged); + + QCOMPARE(client->geometry(), QRect(100, 300, 500, 800)); + client->setVirtualKeyboardGeometry(QRect(0, 100, 1280, 500)); + configureRequestedSpy.wait(); + + shellSurface->ackConfigure(configureRequestedSpy.last()[2].toInt()); + // render at the new size + Test::render(surface.data(), configureRequestedSpy.last().first().toSize(), Qt::blue); + geometryChangedSpy.wait(); + + QCOMPARE(client->geometry(), QRect(100, 0, 500, 101)); + + client->setFullScreen(true, true); + configureRequestedSpy.wait(); + shellSurface->ackConfigure(configureRequestedSpy.last()[2].toInt()); + Test::render(surface.data(), configureRequestedSpy.last().first().toSize(), Qt::blue); + geometryChangedSpy.wait(); + QCOMPARE(client->geometry(), QRect(0, 0, 1280, 1024)); + + + client->setVirtualKeyboardGeometry(QRect()); + QVERIFY(!configureRequestedSpy.wait(10)); + + // render at the size of the configureRequested.. it won't have changed + Test::render(surface.data(), configureRequestedSpy.last().first().toSize(), Qt::blue); + QVERIFY(!geometryChangedSpy.wait(10)); + // Size will NOT be restored + QCOMPARE(client->geometry(), QRect(0, 0, 1280, 1024)); +} + } WAYLANDTEST_MAIN(KWin::MoveResizeWindowTest) diff --git a/virtualkeyboard.h b/virtualkeyboard.h --- a/virtualkeyboard.h +++ b/virtualkeyboard.h @@ -25,7 +25,10 @@ #include #include +#include + class QQuickView; +class QTimer; class QWindow; class KStatusNotifierItem; @@ -53,10 +56,15 @@ void hide(); void setEnabled(bool enable); void updateSni(); + void updateInputPanelState(); bool m_enabled = false; KStatusNotifierItem *m_sni = nullptr; 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; diff --git a/virtualkeyboard.cpp b/virtualkeyboard.cpp --- a/virtualkeyboard.cpp +++ b/virtualkeyboard.cpp @@ -26,10 +26,12 @@ #include "wayland_server.h" #include "workspace.h" #include "xkb.h" +#include "shell_client.h" #include #include #include +#include #include #include @@ -44,6 +46,7 @@ #include #include #include +#include // xkbcommon #include @@ -57,6 +60,9 @@ 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 @@ -146,8 +152,20 @@ qApp->inputMethod()->update(Qt::ImQueryAll); } ); - // TODO: calculate overlap - t->setInputPanelState(m_inputWindow->isVisible(), QRect(0, 0, 0, 0)); + + 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(); @@ -176,20 +194,10 @@ m_inputWindow->setMask(m_inputWindow->rootObject()->childrenRect().toRect()); } ); - connect(qApp->inputMethod(), &QInputMethod::visibleChanged, m_inputWindow.data(), - [this] { - m_inputWindow->setVisible(qApp->inputMethod()->isVisible()); - if (qApp->inputMethod()->isVisible()) { - m_inputWindow->setMask(m_inputWindow->rootObject()->childrenRect().toRect()); - } - if (waylandServer()) { - if (auto t = waylandServer()->seat()->focusedTextInput()) { - // TODO: calculate overlap - t->setInputPanelState(m_inputWindow->isVisible(), QRect(0, 0, 0, 0)); - } - } - } - ); + + connect(qApp->inputMethod(), &QInputMethod::visibleChanged, this, &VirtualKeyboard::updateInputPanelState); + + connect(m_inputWindow->rootObject(), &QQuickItem::childrenRectChanged, this, &VirtualKeyboard::updateInputPanelState); } void VirtualKeyboard::setEnabled(bool enabled) @@ -227,6 +235,46 @@ 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) { + 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->rootObject()->childrenRect().toRect()); + } + + 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) {