Commit 97f4712f authored by Marco Martin's avatar Marco Martin
Browse files

Fix size restore upon keyboard close in XdgSurfaceClient

in XdgSurfaceClient setFrameGeometry is async,
so we can't rely on it having the final value immediately.
make setVirtualKeyboardGeometry a virtual.
in the implementation on setVirtualKeyboardGeometry
use requestedFrameGeometry() instead of frameGeometry()
parent f894976e
......@@ -827,6 +827,10 @@ install(FILES scripting/kwinscript.desktop DESTINATION ${SERVICETYPES_INSTALL_DI
add_subdirectory(qml)
if (BUILD_TESTING)
find_package(WaylandProtocols REQUIRED)
find_package(QtWaylandScanner ${QT_MIN_VERSION} REQUIRED)
find_package(Wayland REQUIRED COMPONENTS Client)
add_subdirectory(autotests)
add_subdirectory(tests)
endif()
......
......@@ -2571,6 +2571,16 @@ void AbstractClient::setVirtualKeyboardGeometry(const QRect &geo)
setFrameGeometry(newWindowGeometry);
}
QRect AbstractClient::keyboardGeometryRestore() const
{
return m_keyboardGeometryRestore;
}
void AbstractClient::setKeyboardGeometryRestore(const QRect &geom)
{
m_keyboardGeometryRestore = geom;
}
bool AbstractClient::dockWantsInput() const
{
return false;
......
......@@ -757,7 +757,7 @@ public:
* 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);
virtual void setVirtualKeyboardGeometry(const QRect &geo);
/**
* Restores the AbstractClient after it had been hidden due to show on screen edge functionality.
......@@ -1210,6 +1210,11 @@ protected:
void startShadeHoverTimer();
void startShadeUnhoverTimer();
// The geometry that the client should be restored when the virtual keyboard closes
QRect keyboardGeometryRestore() const;
void setKeyboardGeometryRestore(const QRect &geom);
QRect m_virtualKeyboardGeometry;
private Q_SLOTS:
void shadeHover();
void shadeUnhover();
......@@ -1264,7 +1269,6 @@ private:
QRect m_bufferGeometryBeforeUpdateBlocking;
QRect m_frameGeometryBeforeUpdateBlocking;
QRect m_clientGeometryBeforeUpdateBlocking;
QRect m_virtualKeyboardGeometry;
QRect m_keyboardGeometryRestore;
QRect m_maximizeGeometryRestore;
......
add_subdirectory(helper)
add_library(KWinIntegrationTestFramework STATIC kwin_wayland_test.cpp test_helpers.cpp generic_scene_opengl_test.cpp ../../cursor.cpp ${kwin_XWAYLAND_SRCS})
target_link_libraries(KWinIntegrationTestFramework kwin Qt5::Test)
set(KWinIntegrationTestFramework_SOURCES
../../cursor.cpp
generic_scene_opengl_test.cpp
kwin_wayland_test.cpp
test_helpers.cpp
${kwin_XWAYLAND_SRCS}
)
ecm_add_qtwayland_client_protocol(KWinIntegrationTestFramework_SOURCES
PROTOCOL ${WaylandProtocols_DATADIR}/unstable/input-method/input-method-unstable-v1.xml
BASENAME input-method-unstable-v1
)
add_library(KWinIntegrationTestFramework STATIC ${KWinIntegrationTestFramework_SOURCES})
target_link_libraries(KWinIntegrationTestFramework kwin Qt5::Test Wayland::Client)
function(integrationTest)
set(optionArgs WAYLAND_ONLY)
......@@ -12,7 +25,7 @@ function(integrationTest)
target_link_libraries(${ARGS_NAME} KWinIntegrationTestFramework kwin Qt5::Test ${ARGS_LIBS})
add_test(NAME kwin-${ARGS_NAME} COMMAND dbus-run-session ${CMAKE_BINARY_DIR}/bin/${ARGS_NAME})
if (${ARGS_WAYLAND_ONLY})
add_executable(${ARGS_NAME}_waylandonly ${ARGS_SRCS})
add_executable(${ARGS_NAME}_waylandonly ${ARGS_SRCS} )
set_target_properties(${ARGS_NAME}_waylandonly PROPERTIES COMPILE_DEFINITIONS "NO_XWAYLAND")
target_link_libraries(${ARGS_NAME}_waylandonly KWinIntegrationTestFramework kwin Qt5::Test ${ARGS_LIBS})
add_test(NAME kwin-${ARGS_NAME}-waylandonly COMMAND dbus-run-session ${CMAKE_BINARY_DIR}/bin/${ARGS_NAME}_waylandonly)
......@@ -60,6 +73,7 @@ integrationTest(WAYLAND_ONLY NAME testNoGlobalShortcuts SRCS no_global_shortcuts
integrationTest(WAYLAND_ONLY NAME testBufferSizeChange SRCS buffer_size_change_test.cpp )
integrationTest(WAYLAND_ONLY NAME testPlacement SRCS placement_test.cpp)
integrationTest(WAYLAND_ONLY NAME testActivation SRCS activation_test.cpp)
integrationTest(WAYLAND_ONLY NAME testVirtualKeyboard SRCS virtualkeyboard_test.cpp)
if (XCB_ICCCM_FOUND)
integrationTest(NAME testMoveResize SRCS move_resize_window_test.cpp LIBS XCB::ICCCM)
......
......@@ -14,6 +14,7 @@
#include "../../workspace.h"
#include "../../xcbutils.h"
#include "../../xwl/xwayland.h"
#include "../../virtualkeyboard.h"
#include <KPluginMetaData>
......@@ -85,6 +86,38 @@ WaylandTestApplication::~WaylandTestApplication()
void WaylandTestApplication::performStartup()
{
if (!m_inputMethodServerToStart.isEmpty()) {
VirtualKeyboard::create();
if (m_inputMethodServerToStart != QStringLiteral("internal")) {
int socket = dup(waylandServer()->createInputMethodConnection());
if (socket >= 0) {
QProcessEnvironment environment = processStartupEnvironment();
environment.insert(QStringLiteral("WAYLAND_SOCKET"), QByteArray::number(socket));
environment.insert(QStringLiteral("QT_QPA_PLATFORM"), QStringLiteral("wayland"));
environment.remove("DISPLAY");
environment.remove("WAYLAND_DISPLAY");
QProcess *p = new Process(this);
p->setProcessChannelMode(QProcess::ForwardedErrorChannel);
connect(p, qOverload<int, QProcess::ExitStatus>(&QProcess::finished), this,
[p] {
if (waylandServer()) {
waylandServer()->destroyInputMethodConnection();
}
p->deleteLater();
}
);
p->setProcessEnvironment(environment);
p->setProgram(m_inputMethodServerToStart);
// p->setArguments(arguments);
p->start();
connect(waylandServer(), &WaylandServer::terminatingInternalClientConnection, p, [p] {
p->kill();
p->waitForFinished();
});
}
}
}
// first load options - done internally by a different thread
createOptions();
waylandServer()->createInternalConnection();
......
......@@ -37,9 +37,15 @@ class SubSurface;
class Surface;
class XdgDecorationManager;
class OutputManagement;
class TextInputManager;
}
}
namespace QtWayland
{
class zwp_input_panel_surface_v1;
}
namespace KWin
{
namespace Xwl
......@@ -56,6 +62,9 @@ public:
WaylandTestApplication(OperationMode mode, int &argc, char **argv);
~WaylandTestApplication() override;
void setInputMethodServerToStart(const QString &inputMethodServer) {
m_inputMethodServerToStart = inputMethodServer;
}
protected:
void performStartup() override;
......@@ -66,11 +75,14 @@ private:
void finalizeStartup();
Xwl::Xwayland *m_xwayland = nullptr;
QString m_inputMethodServerToStart;
};
namespace Test
{
class MockInputMethod;
enum class AdditionalWaylandInterface {
Seat = 1 << 0,
Decoration = 1 << 1,
......@@ -82,6 +94,8 @@ enum class AdditionalWaylandInterface {
ShadowManager = 1 << 7,
XdgDecoration = 1 << 8,
OutputManagement = 1 << 9,
TextInputManagerV2 = 1 << 10,
InputMethodV1 = 1 << 11
};
Q_DECLARE_FLAGS(AdditionalWaylandInterfaces, AdditionalWaylandInterface)
/**
......@@ -114,6 +128,7 @@ KWayland::Client::IdleInhibitManager *waylandIdleInhibitManager();
KWayland::Client::AppMenuManager *waylandAppMenuManager();
KWayland::Client::XdgDecorationManager *xdgDecorationManager();
KWayland::Client::OutputManagement *waylandOutputManagement();
KWayland::Client::TextInputManager *waylandTextInputManager();
bool waitForWaylandPointer();
bool waitForWaylandTouch();
......@@ -133,6 +148,9 @@ enum class CreationSetup {
CreateAndConfigure, /// commit and wait for the configure event, making this surface ready to commit buffers
};
QtWayland::zwp_input_panel_surface_v1 *createInputPanelSurfaceV1(KWayland::Client::Surface *surface,
KWayland::Client::Output *output);
KWayland::Client::XdgShellSurface *createXdgShellSurface(XdgShellSurfaceType type,
KWayland::Client::Surface *surface,
QObject *parent = nullptr,
......
......@@ -11,6 +11,8 @@
#include "screenlockerwatcher.h"
#include "wayland_server.h"
#include "workspace.h"
#include "qwayland-input-method-unstable-v1.h"
#include "virtualkeyboard.h"
#include <KWayland/Client/compositor.h>
#include <KWayland/Client/connection_thread.h>
......@@ -28,6 +30,7 @@
#include <KWayland/Client/subcompositor.h>
#include <KWayland/Client/subsurface.h>
#include <KWayland/Client/surface.h>
#include <KWayland/Client/textinput.h>
#include <KWayland/Client/appmenu.h>
#include <KWayland/Client/xdgshell.h>
#include <KWayland/Client/xdgdecoration.h>
......@@ -71,8 +74,60 @@ static struct {
IdleInhibitManager *idleInhibit = nullptr;
AppMenuManager *appMenu = nullptr;
XdgDecorationManager *xdgDecoration = nullptr;
TextInputManager *textInputManager = nullptr;
QtWayland::zwp_input_panel_v1 *inputPanelV1 = nullptr;
MockInputMethod *inputMethodV1 = nullptr;
QtWayland::zwp_input_method_context_v1 *inputMethodContextV1 = nullptr;
} s_waylandConnection;
class MockInputMethod : public QtWayland::zwp_input_method_v1
{
public:
MockInputMethod(struct wl_registry *registry, int id, int version);
~MockInputMethod();
protected:
void zwp_input_method_v1_activate(struct ::zwp_input_method_context_v1 *context) override;
void zwp_input_method_v1_deactivate(struct ::zwp_input_method_context_v1 *context) override;
private:
Surface *m_inputSurface = nullptr;
QtWayland::zwp_input_panel_surface_v1 *m_inputMethodSurface = nullptr;
AbstractClient *m_client = nullptr;
};
MockInputMethod::MockInputMethod(struct wl_registry *registry, int id, int version)
: QtWayland::zwp_input_method_v1(registry, id, version)
{
}
MockInputMethod::~MockInputMethod()
{
}
void MockInputMethod::zwp_input_method_v1_activate(struct ::zwp_input_method_context_v1 *context)
{
if (!m_inputSurface) {
m_inputSurface = Test::createSurface();
m_inputMethodSurface = Test::createInputPanelSurfaceV1(m_inputSurface, s_waylandConnection.outputs.first());
}
m_client = Test::renderAndWaitForShown(m_inputSurface, QSize(1280, 400), Qt::blue);
}
void MockInputMethod::zwp_input_method_v1_deactivate(struct ::zwp_input_method_context_v1 *context)
{
zwp_input_method_context_v1_destroy(context);
if (m_inputSurface) {
m_inputSurface->release();
m_inputSurface->destroy();
delete m_inputSurface;
m_inputSurface = nullptr;
delete m_inputMethodSurface;
m_inputMethodSurface = nullptr;
}
}
bool setupWaylandConnection(AdditionalWaylandInterfaces flags)
{
if (s_waylandConnection.connection) {
......@@ -123,6 +178,16 @@ bool setupWaylandConnection(AdditionalWaylandInterfaces flags)
});
});
QObject::connect(registry, &Registry::interfaceAnnounced, [=](const QByteArray &interface, quint32 name, quint32 version) {
if (flags & AdditionalWaylandInterface::InputMethodV1) {
if (interface == QByteArrayLiteral("zwp_input_method_v1")) {
s_waylandConnection.inputMethodV1 = new MockInputMethod(*registry, name, version);
} else if (interface == QByteArrayLiteral("zwp_input_panel_v1")) {
s_waylandConnection.inputPanelV1 = new QtWayland::zwp_input_panel_v1(*registry, name, version);
}
}
});
QSignalSpy allAnnounced(registry, &Registry::interfacesAnnounced);
if (!allAnnounced.isValid()) {
return false;
......@@ -219,6 +284,12 @@ bool setupWaylandConnection(AdditionalWaylandInterfaces flags)
return false;
}
}
if (flags.testFlag(AdditionalWaylandInterface::TextInputManagerV2)) {
s_waylandConnection.textInputManager = registry->createTextInputManager(registry->interface(Registry::Interface::TextInputManagerUnstableV2).name, registry->interface(Registry::Interface::TextInputManagerUnstableV2).version);
if (!s_waylandConnection.textInputManager->isValid()) {
return false;
}
}
return true;
}
......@@ -257,6 +328,10 @@ void destroyWaylandConnection()
s_waylandConnection.appMenu = nullptr;
delete s_waylandConnection.xdgDecoration;
s_waylandConnection.xdgDecoration = nullptr;
delete s_waylandConnection.textInputManager;
s_waylandConnection.textInputManager = nullptr;
delete s_waylandConnection.inputPanelV1;
s_waylandConnection.inputPanelV1 = nullptr;
if (s_waylandConnection.thread) {
QSignalSpy spy(s_waylandConnection.connection, &QObject::destroyed);
s_waylandConnection.connection->deleteLater();
......@@ -341,6 +416,11 @@ OutputManagement *waylandOutputManagement()
return s_waylandConnection.outputManagement;
}
TextInputManager *waylandTextInputManager()
{
return s_waylandConnection.textInputManager;
}
bool waitForWaylandPointer()
{
......@@ -467,6 +547,24 @@ XdgShellSurface *createXdgShellStableSurface(Surface *surface, QObject *parent,
return s;
}
QtWayland::zwp_input_panel_surface_v1 *createInputPanelSurfaceV1(Surface *surface, Output *output)
{
if (!s_waylandConnection.inputPanelV1) {
qWarning() << "Unable to create the input panel surface. The interface input_panel global is not bound";
return nullptr;
}
QtWayland::zwp_input_panel_surface_v1 *s = new QtWayland::zwp_input_panel_surface_v1(s_waylandConnection.inputPanelV1->get_input_panel_surface(*surface));
if (!s->isInitialized()) {
delete s;
return nullptr;
}
s->set_toplevel(output->output(), QtWayland::zwp_input_panel_surface_v1::position_center_bottom);
return s;
}
XdgShellPopup *createXdgShellStablePopup(Surface *surface, XdgShellSurface *parentSurface, const XdgPositioner &positioner, QObject *parent, CreationSetup creationSetup)
{
if (!s_waylandConnection.xdgShellStable) {
......
/*
KWin - the KDE window manager
This file is part of the KDE project.
SPDX-FileCopyrightText: 2020 Marco Martin <mart@kde.org>
SPDX-License-Identifier: GPL-2.0-or-later
*/
#include "kwin_wayland_test.h"
#include "abstract_client.h"
#include "cursor.h"
#include "effects.h"
#include "deleted.h"
#include "platform.h"
#include "screens.h"
#include "wayland_server.h"
#include "workspace.h"
#include "virtualkeyboard.h"
#include "virtualkeyboard_dbus.h"
#include "qwayland-input-method-unstable-v1.h"
#include <QTest>
#include <QSignalSpy>
#include <QDBusConnection>
#include <QDBusMessage>
#include <QDBusPendingReply>
#include <KWaylandServer/clientconnection.h>
#include <KWaylandServer/display.h>
#include <KWaylandServer/surface_interface.h>
#include <KWayland/Client/compositor.h>
#include <KWayland/Client/output.h>
#include <KWayland/Client/surface.h>
#include <KWayland/Client/xdgshell.h>
#include <KWayland/Client/textinput.h>
using namespace KWin;
using namespace KWayland::Client;
using KWin::VirtualKeyboardDBus;
static const QString s_socketName = QStringLiteral("wayland_test_kwin_virtualkeyboard-0");
class VirtualKeyboardTest : public QObject
{
Q_OBJECT
private Q_SLOTS:
void initTestCase();
void init();
void cleanup();
void testOpenClose();
};
void VirtualKeyboardTest::initTestCase()
{
qRegisterMetaType<KWin::Deleted *>();
qRegisterMetaType<KWin::AbstractClient *>();
qRegisterMetaType<KWayland::Client::Output *>();
QSignalSpy applicationStartedSpy(kwinApp(), &Application::started);
QVERIFY(applicationStartedSpy.isValid());
kwinApp()->platform()->setInitialWindowSize(QSize(1280, 1024));
QVERIFY(waylandServer()->init(s_socketName.toLocal8Bit()));
QMetaObject::invokeMethod(kwinApp()->platform(), "setVirtualOutputs", Qt::DirectConnection, Q_ARG(int, 2));
static_cast<WaylandTestApplication *>(kwinApp())->setInputMethodServerToStart("internal");
kwinApp()->start();
QVERIFY(applicationStartedSpy.wait());
QCOMPARE(screens()->count(), 2);
QCOMPARE(screens()->geometry(0), QRect(0, 0, 1280, 1024));
QCOMPARE(screens()->geometry(1), QRect(1280, 0, 1280, 1024));
waylandServer()->initWorkspace();
}
void VirtualKeyboardTest::init()
{
QVERIFY(Test::setupWaylandConnection(Test::AdditionalWaylandInterface::Seat |
Test::AdditionalWaylandInterface::TextInputManagerV2 | Test::AdditionalWaylandInterface::InputMethodV1));
screens()->setCurrent(0);
KWin::Cursors::self()->mouse()->setPos(QPoint(1280, 512));
const QDBusMessage message = QDBusMessage::createMethodCall(QStringLiteral("org.kde.kwin.testvirtualkeyboard"),
QStringLiteral("/VirtualKeyboard"),
QStringLiteral("org.kde.kwin.VirtualKeyboard"),
"enable");
QDBusConnection::sessionBus().call(message);
}
void VirtualKeyboardTest::cleanup()
{
Test::destroyWaylandConnection();
}
void VirtualKeyboardTest::testOpenClose()
{
QSignalSpy clientAddedSpy(workspace(), &Workspace::clientAdded);
QSignalSpy clientRemovedSpy(workspace(), &Workspace::clientRemoved);
QVERIFY(clientAddedSpy.isValid());
// Create an xdg_toplevel surface and wait for the compositor to catch up.
QScopedPointer<Surface> surface(Test::createSurface());
QScopedPointer<XdgShellSurface> shellSurface(Test::createXdgShellStableSurface(surface.data()));
AbstractClient *client = Test::renderAndWaitForShown(surface.data(), QSize(1280, 1024), Qt::red);
QVERIFY(client);
QVERIFY(client->isActive());
QCOMPARE(client->frameGeometry().size(), QSize(1280, 1024));
QSignalSpy frameGeometryChangedSpy(client, &AbstractClient::frameGeometryChanged);
QVERIFY(frameGeometryChangedSpy.isValid());
QSignalSpy configureRequestedSpy(shellSurface.data(), &XdgShellSurface::configureRequested);
QVERIFY(configureRequestedSpy.isValid());
QScopedPointer<TextInput> textInput(Test::waylandTextInputManager()->createTextInput(Test::waylandSeat()));
QVERIFY(!textInput.isNull());
textInput->enable(surface.data());
QVERIFY(configureRequestedSpy.wait());
// Show the keyboard
textInput->showInputPanel();
QVERIFY(clientAddedSpy.wait());
AbstractClient *keyboardClient = clientAddedSpy.last().first().value<AbstractClient *>();
QVERIFY(keyboardClient);
QVERIFY(keyboardClient->isInputMethod());
// Do the actual resize
QVERIFY(configureRequestedSpy.wait());
Test::render(surface.data(), configureRequestedSpy.last().first().value<QSize>(), Qt::red);
QVERIFY(frameGeometryChangedSpy.wait());
QCOMPARE(client->frameGeometry().height(), 1024 - keyboardClient->inputGeometry().height() + 1);
// Hide the keyboard
textInput->hideInputPanel();
QVERIFY(configureRequestedSpy.wait());
Test::render(surface.data(), configureRequestedSpy.last().first().value<QSize>(), Qt::red);
QVERIFY(frameGeometryChangedSpy.wait());
QCOMPARE(client->frameGeometry().height(), 1024);
// Destroy the test client.
shellSurface.reset();
QVERIFY(Test::waitForWindowDestroyed(client));
}
WAYLANDTEST_MAIN(VirtualKeyboardTest)
#include "virtualkeyboard_test.moc"
......@@ -123,6 +123,9 @@ NET::WindowType InputPanelV1Client::windowType(bool, int) const
QRect InputPanelV1Client::inputGeometry() const
{
if (surface()->inputIsInfinite()) {
return frameGeometry();
}
return surface()->input().boundingRect().translated(pos());
}
......
......@@ -112,7 +112,9 @@ void VirtualKeyboard::init()
return;
}
m_trackedClient->setVirtualKeyboardGeometry(m_inputClient ? m_inputClient->inputGeometry() : QRect());
if (m_inputClient && !m_inputClient->inputGeometry().isEmpty()) {
m_trackedClient->setVirtualKeyboardGeometry(m_inputClient->inputGeometry());
}
};
connect(surface->surface(), &SurfaceInterface::inputChanged, this, refreshFrame);
connect(surface->surface(), &QObject::destroyed, this, [this] {
......@@ -120,8 +122,7 @@ void VirtualKeyboard::init()
m_trackedClient->setVirtualKeyboardGeometry({});
}
});
updateInputPanelState();
refreshFrame();
connect(m_inputClient, &AbstractClient::frameGeometryChanged, this, refreshFrame);
});
connect(waylandServer()->seat(), &SeatInterface::focusedTextInputChanged, this,
......@@ -158,6 +159,7 @@ void VirtualKeyboard::init()
});
m_waylandEnabledConnection = connect(t, &TextInputInterface::enabledChanged, this, [t, this] {
if (t->isEnabled()) {
//FIXME This sendDeactivate shouldn't be necessary?
waylandServer()->inputMethod()->sendDeactivate();
waylandServer()->inputMethod()->sendActivate();
adoptInputMethodContext();
......@@ -177,7 +179,6 @@ void VirtualKeyboard::init()
}
m_trackedClient = newClient;
}
updateInputPanelState();
} else {
m_waylandShowConnection = QMetaObject::Connection();
......@@ -198,6 +199,7 @@ void VirtualKeyboard::show()
{
auto t = waylandServer()->seat()->focusedTextInput();
if (t) {
//FIXME: this shouldn't be necessary and causes double emits?
Q_EMIT t->enabledChanged();
}
}
......
......@@ -518,6 +518,38 @@ void XdgSurfaceClient::destroyClient()
delete this;
}
void XdgSurfaceClient::setVirtualKeyboardGeometry(const QRect &geo)
{
// No keyboard anymore
if (geo.isEmpty() && !keyboardGeometryRestore().isEmpty()) {
setFrameGeometry(keyboardGeometryRestore());
setKeyboardGeometryRestore(QRect());
} else if (geo.isEmpty()) {
return;
// The keyboard has just been opened (rather than resized) save client geometry for a restore
} else if (keyboardGeometryRestore().isEmpty()) {
setKeyboardGeometryRestore(requestedFrameGeometry().isEmpty() ? frameGeometry() : requestedFrameGeometry());
}
m_virtualKeyboardGeometry = geo;
// Don't resize Desktop and fullscreen windows
if (isFullScreen() || isDesktop()) {
return;
}
if (!geo.intersects(keyboardGeometryRestore())) {
return;
}
const QRect availableArea = workspace()->clientArea(MaximizeArea, this);
QRect newWindowGeometry = keyboardGeometryRestore();
newWindowGeometry.moveBottom(geo.top());
newWindowGeometry.setTop(qMax(newWindowGeometry.top(), availableArea.top()));
setFrameGeometry(newWindowGeometry);
}
void XdgSurfaceClient::clean