Implement xdg-dialog-v1

This commit is contained in:
David Redondo 2023-07-11 08:49:59 +02:00
parent baaa1db5d1
commit cbab4b46c1
11 changed files with 438 additions and 1 deletions

View file

@ -25,6 +25,13 @@ qt6_generate_wayland_protocol_client_sources(KWinIntegrationTestFramework
${PLASMA_WAYLAND_PROTOCOLS_DIR}/fake-input.xml
)
# the qtwaylandscanner macro cannot handle the mismatched file name and <protocol name=""
find_package(QtWaylandScanner REQUIRED)
ecm_add_qtwayland_client_protocol(KWinIntegrationTestFramework
PROTOCOL ${WaylandProtocols_DATADIR}/staging/xdg-dialog/xdg-dialog-v1.xml
BASENAME dialog-v1
)
target_sources(KWinIntegrationTestFramework PRIVATE
generic_scene_opengl_test.cpp
kwin_wayland_test.cpp

View file

@ -21,6 +21,7 @@
#include <optional>
#include "qwayland-cursor-shape-v1.h"
#include "qwayland-dialog-v1.h"
#include "qwayland-fake-input.h"
#include "qwayland-fractional-scale-v1.h"
#include "qwayland-idle-inhibit-unstable-v1.h"
@ -547,6 +548,19 @@ public:
~SecurityContextManagerV1() override;
};
class XdgWmDialogV1 : public QtWayland::xdg_wm_dialog_v1
{
public:
~XdgWmDialogV1() override;
};
class XdgDialogV1 : public QtWayland::xdg_dialog_v1
{
public:
XdgDialogV1(XdgWmDialogV1 *wm, XdgToplevel *toplevel);
~XdgDialogV1() override;
};
enum class AdditionalWaylandInterface {
Seat = 1 << 0,
PlasmaShell = 1 << 2,
@ -568,6 +582,7 @@ enum class AdditionalWaylandInterface {
CursorShapeV1 = 1 << 18,
FakeInput = 1 << 19,
SecurityContextManagerV1 = 1 << 20,
XdgDialogV1 = 1 << 21,
};
Q_DECLARE_FLAGS(AdditionalWaylandInterfaces, AdditionalWaylandInterface)
@ -714,6 +729,7 @@ std::unique_ptr<XdgToplevelDecorationV1> createXdgToplevelDecorationV1(XdgToplev
std::unique_ptr<IdleInhibitorV1> createIdleInhibitorV1(KWayland::Client::Surface *surface);
std::unique_ptr<AutoHideScreenEdgeV1> createAutoHideScreenEdgeV1(KWayland::Client::Surface *surface, uint32_t border);
std::unique_ptr<CursorShapeDeviceV1> createCursorShapeDeviceV1(KWayland::Client::Pointer *pointer);
std::unique_ptr<XdgDialogV1> createXdgDialogV1(XdgToplevel *toplevel);
/**
* Creates a shared memory buffer of @p size in @p color and attaches it to the @p surface.

View file

@ -269,6 +269,21 @@ SecurityContextManagerV1::~SecurityContextManagerV1()
destroy();
}
XdgWmDialogV1::~XdgWmDialogV1()
{
destroy();
}
XdgDialogV1::XdgDialogV1(XdgWmDialogV1 *wm, XdgToplevel *toplevel)
: QtWayland::xdg_dialog_v1(wm->get_xdg_dialog(toplevel->object()))
{
}
XdgDialogV1::~XdgDialogV1()
{
destroy();
}
static struct
{
KWayland::Client::ConnectionThread *connection = nullptr;
@ -302,6 +317,7 @@ static struct
CursorShapeManagerV1 *cursorShapeManagerV1 = nullptr;
FakeInput *fakeInput = nullptr;
SecurityContextManagerV1 *securityContextManagerV1 = nullptr;
XdgWmDialogV1 *xdgWmDialogV1;
} s_waylandConnection;
MockInputMethod *inputMethod()
@ -517,6 +533,12 @@ bool setupWaylandConnection(AdditionalWaylandInterfaces flags)
s_waylandConnection.securityContextManagerV1->init(*registry, name, version);
}
}
if (flags & AdditionalWaylandInterface::XdgDialogV1) {
if (interface == xdg_wm_dialog_v1_interface.name) {
s_waylandConnection.xdgWmDialogV1 = new XdgWmDialogV1();
s_waylandConnection.xdgWmDialogV1->init(*registry, name, version);
}
}
});
QSignalSpy allAnnounced(registry, &KWayland::Client::Registry::interfacesAnnounced);
@ -642,6 +664,8 @@ void destroyWaylandConnection()
s_waylandConnection.fakeInput = nullptr;
delete s_waylandConnection.securityContextManagerV1;
s_waylandConnection.securityContextManagerV1 = nullptr;
delete s_waylandConnection.xdgWmDialogV1;
s_waylandConnection.xdgWmDialogV1 = nullptr;
delete s_waylandConnection.queue; // Must be destroyed last
s_waylandConnection.queue = nullptr;
@ -1098,6 +1122,16 @@ std::unique_ptr<CursorShapeDeviceV1> createCursorShapeDeviceV1(KWayland::Client:
return std::make_unique<CursorShapeDeviceV1>(manager, pointer);
}
std::unique_ptr<XdgDialogV1> createXdgDialogV1(XdgToplevel *toplevel)
{
XdgWmDialogV1 *wm = s_waylandConnection.xdgWmDialogV1;
if (!wm) {
qWarning() << "Could not create a xdg_dialog_v1 because xdg_wm_dialog_v1 global is not bound";
return nullptr;
}
return std::make_unique<XdgDialogV1>(wm, toplevel);
}
bool waitForWindowClosed(Window *window)
{
QSignalSpy closedSpy(window, &Window::closed);

View file

@ -104,6 +104,9 @@ private Q_SLOTS:
void testMaximizeAndChangeDecorationModeAfterInitialCommit();
void testFullScreenAndChangeDecorationModeAfterInitialCommit();
void testChangeDecorationModeAfterInitialCommit();
void testModal();
void testCloseModal();
void testCloseInactiveModal();
};
void TestXdgShellWindow::testXdgPopupReactive_data()
@ -206,7 +209,7 @@ void TestXdgShellWindow::initTestCase()
void TestXdgShellWindow::init()
{
QVERIFY(Test::setupWaylandConnection(Test::AdditionalWaylandInterface::Seat | Test::AdditionalWaylandInterface::XdgDecorationV1 | Test::AdditionalWaylandInterface::AppMenu));
QVERIFY(Test::setupWaylandConnection(Test::AdditionalWaylandInterface::Seat | Test::AdditionalWaylandInterface::XdgDecorationV1 | Test::AdditionalWaylandInterface::AppMenu | Test::AdditionalWaylandInterface::XdgDialogV1));
QVERIFY(Test::waitForWaylandPointer());
workspace()->setActiveOutput(QPoint(640, 512));
@ -2190,5 +2193,134 @@ void TestXdgShellWindow::testChangeDecorationModeAfterInitialCommit()
QCOMPARE(decorationConfigureRequestedSpy.last().at(0).value<Test::XdgToplevelDecorationV1::mode>(), Test::XdgToplevelDecorationV1::mode_client_side);
}
void TestXdgShellWindow::testModal()
{
auto parentSurface = Test::createSurface();
auto parentToplevel = Test::createXdgToplevelSurface(parentSurface.get());
auto parentWindow = Test::renderAndWaitForShown(parentSurface.get(), {200, 200}, Qt::cyan);
QVERIFY(parentWindow);
auto childSurface = Test::createSurface();
auto childToplevel = Test::createXdgToplevelSurface(childSurface.get(), [&parentToplevel](Test::XdgToplevel *toplevel) {
toplevel->set_parent(parentToplevel->object());
});
auto childWindow = Test::renderAndWaitForShown(childSurface.get(), {200, 200}, Qt::yellow);
QVERIFY(childWindow);
QVERIFY(!childWindow->isModal());
QCOMPARE(childWindow->transientFor(), parentWindow);
auto dialog = Test::createXdgDialogV1(childToplevel.get());
QVERIFY(Test::waylandSync());
QVERIFY(dialog);
QVERIFY(!childWindow->isModal());
QSignalSpy modalChangedSpy(childWindow, &Window::modalChanged);
dialog->set_modal();
Test::flushWaylandConnection();
QVERIFY(modalChangedSpy.wait());
QVERIFY(childWindow->isModal());
dialog->unset_modal();
Test::flushWaylandConnection();
QVERIFY(modalChangedSpy.wait());
QVERIFY(!childWindow->isModal());
dialog->set_modal();
Test::flushWaylandConnection();
QVERIFY(modalChangedSpy.wait());
Workspace::self()->activateWindow(parentWindow);
QCOMPARE(Workspace::self()->activeWindow(), childWindow);
dialog.reset();
Test::flushWaylandConnection();
QVERIFY(modalChangedSpy.wait());
QVERIFY(!childWindow->isModal());
}
void TestXdgShellWindow::testCloseModal()
{
// This test verifies that the parent window will be activated when an active modal dialog is closed.
// Create a parent and a child windows.
auto parentSurface = Test::createSurface();
auto parentToplevel = Test::createXdgToplevelSurface(parentSurface.get());
auto parent = Test::renderAndWaitForShown(parentSurface.get(), {200, 200}, Qt::cyan);
QVERIFY(parent);
auto childSurface = Test::createSurface();
auto childToplevel = Test::createXdgToplevelSurface(childSurface.get(), [&parentToplevel](Test::XdgToplevel *toplevel) {
toplevel->set_parent(parentToplevel->object());
});
auto child = Test::renderAndWaitForShown(childSurface.get(), {200, 200}, Qt::yellow);
QVERIFY(child);
QVERIFY(!child->isModal());
QCOMPARE(child->transientFor(), parent);
// Set modal state.
auto dialog = Test::createXdgDialogV1(childToplevel.get());
QSignalSpy modalChangedSpy(child, &Window::modalChanged);
dialog->set_modal();
Test::flushWaylandConnection();
QVERIFY(modalChangedSpy.wait());
QVERIFY(child->isModal());
QCOMPARE(workspace()->activeWindow(), child);
// Close the child.
QSignalSpy childClosedSpy(child, &Window::closed);
childToplevel.reset();
childSurface.reset();
dialog.reset();
Test::flushWaylandConnection();
QVERIFY(childClosedSpy.wait());
QCOMPARE(workspace()->activeWindow(), parent);
}
void TestXdgShellWindow::testCloseInactiveModal()
{
// This test verifies that the parent window will not be activated when an inactive modal dialog is closed.
// Create a parent and a child windows.
auto parentSurface = Test::createSurface();
auto parentToplevel = Test::createXdgToplevelSurface(parentSurface.get());
auto parent = Test::renderAndWaitForShown(parentSurface.get(), {200, 200}, Qt::cyan);
QVERIFY(parent);
auto childSurface = Test::createSurface();
auto childToplevel = Test::createXdgToplevelSurface(childSurface.get(), [&parentToplevel](Test::XdgToplevel *toplevel) {
toplevel->set_parent(parentToplevel->object());
});
auto child = Test::renderAndWaitForShown(childSurface.get(), {200, 200}, Qt::yellow);
QVERIFY(child);
QVERIFY(!child->isModal());
QCOMPARE(child->transientFor(), parent);
// Set modal state.
auto dialog = Test::createXdgDialogV1(childToplevel.get());
QSignalSpy modalChangedSpy(child, &Window::modalChanged);
dialog->set_modal();
Test::flushWaylandConnection();
QVERIFY(modalChangedSpy.wait());
QVERIFY(child->isModal());
QCOMPARE(workspace()->activeWindow(), child);
// Show another window.
auto otherSurface = Test::createSurface();
auto otherToplevel = Test::createXdgToplevelSurface(otherSurface.get());
auto otherWindow = Test::renderAndWaitForShown(otherSurface.get(), {200, 200}, Qt::magenta);
QVERIFY(otherWindow);
workspace()->setActiveWindow(otherWindow);
QCOMPARE(workspace()->activeWindow(), otherWindow);
// Close the child.
QSignalSpy childClosedSpy(child, &Window::closed);
childToplevel.reset();
childSurface.reset();
dialog.reset();
Test::flushWaylandConnection();
QVERIFY(childClosedSpy.wait());
QCOMPARE(workspace()->activeWindow(), otherWindow);
}
WAYLANDTEST_MAIN(TestXdgShellWindow)
#include "xdgshellwindow_test.moc"

View file

@ -231,6 +231,10 @@ ecm_add_qtwayland_server_protocol_kde(WaylandProtocols_xml
PROTOCOL ${PROJECT_SOURCE_DIR}/src/wayland/protocols/xx-color-management-v2.xml
BASENAME xx-color-management-v2
)
ecm_add_qtwayland_server_protocol_kde(WaylandProtocols_xml
PROTOCOL ${WaylandProtocols_DATADIR}/staging/xdg-dialog/xdg-dialog-v1.xml
BASENAME dialog-v1
)
target_sources(kwin PRIVATE
abstract_data_source.cpp
@ -306,6 +310,7 @@ target_sources(kwin PRIVATE
viewporter.cpp
xdgactivation_v1.cpp
xdgdecoration_v1.cpp
xdgdialog_v1.cpp
xdgforeign_v2.cpp
xdgoutput_v1.cpp
xdgshell.cpp
@ -381,6 +386,7 @@ install(FILES
viewporter.h
xdgactivation_v1.h
xdgdecoration_v1.h
xdgdialog_v1.h
xdgforeign_v2.h
xdgoutput_v1.h
xdgshell.h

View file

@ -0,0 +1,152 @@
/*
SPDX-FileCopyrightText: 2024 David Redondo <kde@david-redondo.de>
SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
*/
#include "xdgdialog_v1.h"
#include "display.h"
#include "xdgshell.h"
#include "qwayland-server-dialog-v1.h"
#include <QHash>
namespace KWin
{
constexpr int version = 1;
class XdgDialogWmV1InterfacePrivate : public QtWaylandServer::xdg_wm_dialog_v1
{
public:
XdgDialogWmV1InterfacePrivate(Display *display, XdgDialogWmV1Interface *q);
XdgDialogWmV1Interface *const q;
QHash<XdgToplevelInterface *, XdgDialogV1Interface *> m_dialogs;
protected:
void xdg_wm_dialog_v1_get_xdg_dialog(Resource *resource, uint32_t id, wl_resource *toplevel) override;
void xdg_wm_dialog_v1_destroy(Resource *resource) override;
};
class XdgDialogV1InterfacePrivate : public QtWaylandServer::xdg_dialog_v1
{
public:
XdgDialogV1InterfacePrivate(wl_resource *resource, XdgDialogV1Interface *q);
XdgDialogV1Interface *const q;
XdgToplevelInterface *m_toplevel;
bool modal = false;
protected:
void xdg_dialog_v1_destroy(Resource *resource) override;
void xdg_dialog_v1_destroy_resource(Resource *resource) override;
void xdg_dialog_v1_set_modal(Resource *resource) override;
void xdg_dialog_v1_unset_modal(Resource *resource) override;
};
XdgDialogWmV1InterfacePrivate::XdgDialogWmV1InterfacePrivate(Display *display, XdgDialogWmV1Interface *q)
: xdg_wm_dialog_v1(*display, version)
, q(q)
{
}
void XdgDialogWmV1InterfacePrivate::xdg_wm_dialog_v1_destroy(Resource *resource)
{
wl_resource_destroy(resource->handle);
}
void XdgDialogWmV1InterfacePrivate::xdg_wm_dialog_v1_get_xdg_dialog(Resource *resource, uint32_t id, wl_resource *toplevel_resource)
{
auto toplevel = XdgToplevelInterface::get(toplevel_resource);
if (!toplevel) {
wl_resource_post_error(resource->handle, 0, "Invalid surface");
return;
}
if (m_dialogs.value(toplevel)) {
wl_resource_post_error(resource->handle, error::error_already_used, "xdg_toplevel already already used to a xdg_dialog_v1");
return;
}
auto dialogResource = wl_resource_create(resource->client(), &xdg_dialog_v1_interface, resource->version(), id);
auto dialog = new XdgDialogV1Interface(dialogResource, toplevel);
m_dialogs.insert(toplevel, dialog);
auto removeDialog = [this, toplevel, dialog] {
m_dialogs.removeIf([toplevel, dialog](const std::pair<XdgToplevelInterface *, XdgDialogV1Interface *> &entry) {
return entry.first == toplevel && entry.second == dialog;
});
};
QObject::connect(dialog, &XdgDialogV1Interface::destroyed, q, removeDialog);
QObject::connect(toplevel, &XdgDialogV1Interface::destroyed, q, removeDialog);
Q_EMIT q->dialogCreated(dialog);
}
XdgDialogWmV1Interface::XdgDialogWmV1Interface(Display *display, QObject *parent)
: d(std::make_unique<XdgDialogWmV1InterfacePrivate>(display, this))
{
}
XdgDialogWmV1Interface::~XdgDialogWmV1Interface() = default;
XdgDialogV1Interface *XdgDialogWmV1Interface::dialogForToplevel(XdgToplevelInterface *toplevel) const
{
return d->m_dialogs.value(toplevel);
}
XdgDialogV1InterfacePrivate::XdgDialogV1InterfacePrivate(wl_resource *wl_resource, XdgDialogV1Interface *q)
: xdg_dialog_v1(wl_resource)
, q(q)
{
}
void XdgDialogV1InterfacePrivate::xdg_dialog_v1_destroy_resource(Resource *resource)
{
delete q;
}
void XdgDialogV1InterfacePrivate::xdg_dialog_v1_destroy(Resource *resource)
{
wl_resource_destroy(resource->handle);
}
void XdgDialogV1InterfacePrivate::xdg_dialog_v1_set_modal(Resource *resource)
{
if (modal) {
return;
}
modal = true;
Q_EMIT q->modalChanged(true);
}
void XdgDialogV1InterfacePrivate::xdg_dialog_v1_unset_modal(Resource *resource)
{
if (!modal) {
return;
}
modal = false;
Q_EMIT q->modalChanged(false);
}
XdgDialogV1Interface::XdgDialogV1Interface(wl_resource *resource, XdgToplevelInterface *toplevel)
: d(std::make_unique<XdgDialogV1InterfacePrivate>(resource, this))
{
d->m_toplevel = toplevel;
}
XdgDialogV1Interface::~XdgDialogV1Interface()
{
}
bool XdgDialogV1Interface::isModal() const
{
return d->modal;
}
XdgToplevelInterface *XdgDialogV1Interface::toplevel() const
{
return d->m_toplevel;
}
}

View file

@ -0,0 +1,58 @@
/*
SPDX-FileCopyrightText: 2024 David Redondo <kde@david-redondo.de>
SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
*/
#pragma once
#include <QObject>
#include <memory>
struct wl_resource;
namespace KWin
{
class Display;
class XdgDialogV1Interface;
class XdgDialogV1InterfacePrivate;
class XdgDialogWmV1InterfacePrivate;
class XdgToplevelInterface;
class XdgDialogWmV1Interface : public QObject
{
Q_OBJECT
public:
XdgDialogWmV1Interface(Display *display, QObject *parent = nullptr);
~XdgDialogWmV1Interface() override;
XdgDialogV1Interface *dialogForToplevel(XdgToplevelInterface *toplevel) const;
Q_SIGNALS:
void dialogCreated(XdgDialogV1Interface *dialog);
private:
std::unique_ptr<XdgDialogWmV1InterfacePrivate> d;
};
class XdgDialogV1Interface : public QObject
{
Q_OBJECT
public:
~XdgDialogV1Interface() override;
bool isModal() const;
XdgToplevelInterface *toplevel() const;
Q_SIGNALS:
void modalChanged(bool modal);
private:
XdgDialogV1Interface(wl_resource *resource, XdgToplevelInterface *toplevel);
friend class XdgDialogWmV1InterfacePrivate;
std::unique_ptr<XdgDialogV1InterfacePrivate> d;
};
}

View file

@ -67,6 +67,7 @@
#include "wayland/viewporter.h"
#include "wayland/xdgactivation_v1.h"
#include "wayland/xdgdecoration_v1.h"
#include "wayland/xdgdialog_v1.h"
#include "wayland/xdgforeign_v2.h"
#include "wayland/xdgoutput_v1.h"
#include "wayland/xdgshell.h"
@ -242,6 +243,9 @@ void WaylandServer::registerXdgToplevelWindow(XdgToplevelWindow *window)
if (auto palette = m_paletteManager->paletteForSurface(surface)) {
window->installPalette(palette);
}
if (auto dialog = m_xdgDialogWm->dialogForToplevel(window->shellSurface())) {
window->installXdgDialogV1(dialog);
}
connect(m_XdgForeign, &XdgForeignV2Interface::transientChanged, window, [this](SurfaceInterface *child) {
Q_EMIT foreignTransientChanged(child);
@ -498,6 +502,13 @@ bool WaylandServer::init(InitializationFlags flags)
if (qEnvironmentVariableIntValue("KWIN_ENABLE_XX_COLOR_MANAGEMENT")) {
m_xxColorManager = new XXColorManagerV2(m_display, m_display);
}
m_xdgDialogWm = new KWin::XdgDialogWmV1Interface(m_display, m_display);
connect(m_xdgDialogWm, &KWin::XdgDialogWmV1Interface::dialogCreated, this, [this](KWin::XdgDialogV1Interface *dialog) {
if (auto window = findXdgToplevelWindow(dialog->toplevel()->surface())) {
window->installXdgDialogV1(dialog);
}
});
return true;
}

View file

@ -50,6 +50,8 @@ class DrmLeaseManagerV1;
class TearingControlManagerV1Interface;
class XwaylandShellV1Interface;
class OutputOrderV1Interface;
class XdgDialogWmV1Interface;
class Window;
class Output;
class XdgActivationV1Integration;
@ -297,6 +299,7 @@ private:
DrmLeaseManagerV1 *m_leaseManager = nullptr;
OutputOrderV1Interface *m_outputOrder = nullptr;
XXColorManagerV2 *m_xxColorManager = nullptr;
XdgDialogWmV1Interface *m_xdgDialogWm = nullptr;
KWIN_SINGLETON(WaylandServer)
};

View file

@ -30,6 +30,7 @@
#include "wayland/surface.h"
#include "wayland/tablet_v2.h"
#include "wayland/xdgdecoration_v1.h"
#include "wayland/xdgdialog_v1.h"
#include "wayland_server.h"
#include "workspace.h"
@ -488,6 +489,9 @@ void XdgToplevelWindow::handleRoleDestroyed()
if (m_serverDecoration) {
m_serverDecoration->disconnect(this);
}
if (m_xdgDialog) {
m_xdgDialog->disconnect(this);
}
m_shellSurface->disconnect(this);
@ -1442,6 +1446,17 @@ void XdgToplevelWindow::installPalette(ServerSideDecorationPaletteInterface *pal
updateColorScheme();
}
void XdgToplevelWindow::installXdgDialogV1(XdgDialogV1Interface *dialog)
{
m_xdgDialog = dialog;
connect(dialog, &XdgDialogV1Interface::modalChanged, this, &Window::setModal);
connect(dialog, &QObject::destroyed, this, [this] {
setModal(false);
});
setModal(dialog->isModal());
}
void XdgToplevelWindow::setFullScreen(bool set)
{
if (!isFullScreenable()) {

View file

@ -27,6 +27,7 @@ class KillPrompt;
class PlasmaShellSurfaceInterface;
class ServerSideDecorationInterface;
class ServerSideDecorationPaletteInterface;
class XdgDialogV1Interface;
class XdgToplevelDecorationV1Interface;
class Output;
@ -159,6 +160,7 @@ public:
void installServerDecoration(ServerSideDecorationInterface *decoration);
void installPalette(ServerSideDecorationPaletteInterface *palette);
void installXdgDecoration(XdgToplevelDecorationV1Interface *decoration);
void installXdgDialogV1(XdgDialogV1Interface *dialog);
protected:
XdgSurfaceConfigure *sendRoleConfigure() const override;
@ -213,6 +215,7 @@ private:
QPointer<ServerSideDecorationPaletteInterface> m_paletteInterface;
QPointer<ServerSideDecorationInterface> m_serverDecoration;
QPointer<XdgToplevelDecorationV1Interface> m_xdgDecoration;
QPointer<XdgDialogV1Interface> m_xdgDialog;
XdgToplevelInterface *m_shellSurface;
XdgToplevelInterface::States m_nextStates;
XdgToplevelInterface::States m_acknowledgedStates;