diff --git a/autotests/client/test_wayland_shell.cpp b/autotests/client/test_wayland_shell.cpp --- a/autotests/client/test_wayland_shell.cpp +++ b/autotests/client/test_wayland_shell.cpp @@ -55,6 +55,7 @@ void testToplevel(); void testTransient_data(); void testTransient(); + void testTransientPopup(); void testPing(); void testTitle(); void testWindowClass(); @@ -475,6 +476,72 @@ QVERIFY(serverSurface2->acceptsKeyboardFocus()); } +void TestWaylandShell::testTransientPopup() +{ + using namespace KWayland::Server; + using namespace KWayland::Client; + QScopedPointer s(m_compositor->createSurface()); + QVERIFY(!s.isNull()); + QVERIFY(s->isValid()); + ShellSurface *surface = m_shell->createSurface(s.data(), m_shell); + + QSignalSpy serverSurfaceSpy(m_shellInterface, &ShellInterface::surfaceCreated); + QVERIFY(serverSurfaceSpy.isValid()); + QVERIFY(serverSurfaceSpy.wait()); + ShellSurfaceInterface *serverSurface = serverSurfaceSpy.first().first().value(); + QVERIFY(serverSurface); + QCOMPARE(serverSurface->isToplevel(), true); + QCOMPARE(serverSurface->isPopup(), false); + QCOMPARE(serverSurface->isTransient(), false); + QCOMPARE(serverSurface->transientFor(), QPointer()); + QCOMPARE(serverSurface->transientOffset(), QPoint()); + QVERIFY(serverSurface->acceptsKeyboardFocus()); + + QSignalSpy transientSpy(serverSurface, &ShellSurfaceInterface::transientChanged); + QVERIFY(transientSpy.isValid()); + QSignalSpy transientOffsetSpy(serverSurface, &ShellSurfaceInterface::transientOffsetChanged); + QVERIFY(transientOffsetSpy.isValid()); + QSignalSpy transientForChangedSpy(serverSurface, &ShellSurfaceInterface::transientForChanged); + QVERIFY(transientForChangedSpy.isValid()); + + QScopedPointer s2(m_compositor->createSurface()); + m_shell->createSurface(s2.data(), m_shell); + serverSurfaceSpy.clear(); + QVERIFY(serverSurfaceSpy.wait()); + ShellSurfaceInterface *serverSurface2 = serverSurfaceSpy.first().first().value(); + QVERIFY(serverSurface2 != serverSurface); + QVERIFY(serverSurface2); + QVERIFY(serverSurface2->acceptsKeyboardFocus()); + + // TODO: proper serial checking + surface->setTransientPopup(s2.data(), m_seat, 1, QPoint(10, 20)); + QVERIFY(transientSpy.wait()); + QCOMPARE(transientSpy.count(), 1); + QCOMPARE(transientSpy.first().first().toBool(), true); + QCOMPARE(transientOffsetSpy.count(), 1); + QCOMPARE(transientOffsetSpy.first().first().toPoint(), QPoint(10, 20)); + QCOMPARE(transientForChangedSpy.count(), 1); + QCOMPARE(serverSurface->isToplevel(), false); + QCOMPARE(serverSurface->isPopup(), true); + QCOMPARE(serverSurface->isTransient(), true); + QCOMPARE(serverSurface->transientFor(), QPointer(serverSurface2->surface())); + QCOMPARE(serverSurface->transientOffset(), QPoint(10, 20)); + // TODO: honor the flag + QCOMPARE(serverSurface->acceptsKeyboardFocus(), false); + + QCOMPARE(serverSurface2->isToplevel(), true); + QCOMPARE(serverSurface2->isPopup(), false); + QCOMPARE(serverSurface2->isTransient(), false); + QCOMPARE(serverSurface2->transientFor(), QPointer()); + QCOMPARE(serverSurface2->transientOffset(), QPoint()); + + // send popup done + QSignalSpy popupDoneSpy(surface, &ShellSurface::popupDone); + QVERIFY(popupDoneSpy.isValid()); + serverSurface->popupDone(); + QVERIFY(popupDoneSpy.wait()); +} + void TestWaylandShell::testPing() { using namespace KWayland::Server; diff --git a/src/client/shell.h b/src/client/shell.h --- a/src/client/shell.h +++ b/src/client/shell.h @@ -247,6 +247,26 @@ **/ void setTransient(Surface *parent, const QPoint &offset = QPoint(), TransientFlags flags = TransientFlag::Default); + /** + * Sets this Surface as a popup transient for @p parent. + * + * A popup is a transient with an added pointer grab on the @p grabbedSeat. + * + * The popup grab can be created if the client has an implicit grab (e.g. button press) + * on the @p grabbedSeat. It needs to pass the @p grabSerial indicating the implicit grab + * to the request for setting the surface. The implicit grab is turned into a popup grab + * which will persist after the implicit grab ends. The popup grab ends when the ShellSurface + * gets destroyed or when the compositor breaks the grab through the @link{popupDone} signal. + * + * @param parent The parent Surface of this ShellSurface + * @param grabbedSeat The Seat on which an implicit grab exists + * @param grabSerial The serial of the implicit grab + * @param offset The offset of this Surface in the parent coordinate system + * @param flags The flags for the transient + * @since 5.33 + **/ + void setTransientPopup(Surface *parent, Seat *grabbedSeat, quint32 grabSerial, const QPoint &offset = QPoint(), TransientFlags flags = TransientFlag::Default); + bool isValid() const; /** @@ -308,6 +328,15 @@ void pinged(); void sizeChanged(const QSize &); + /** + * The popupDone signal is sent out when a popup grab is broken, that is, + * when the user clicks a surface that doesn't belong to the client owning + * the popup surface. + * @see setTransientPopup + * @since 5.33 + **/ + void popupDone(); + private: class Private; QScopedPointer d; diff --git a/src/client/shell.cpp b/src/client/shell.cpp --- a/src/client/shell.cpp +++ b/src/client/shell.cpp @@ -262,9 +262,9 @@ void ShellSurface::Private::popupDoneCallback(void *data, wl_shell_surface *shellSurface) { - // not needed, we don't have popups - Q_UNUSED(data) - Q_UNUSED(shellSurface) + auto s = reinterpret_cast(data); + Q_ASSERT(s->surface == shellSurface); + emit s->q->popupDone(); } void ShellSurface::setup(wl_shell_surface *surface) @@ -315,6 +315,18 @@ wl_shell_surface_set_transient(d->surface, *parent, offset.x(), offset.y(), wlFlags); } +void ShellSurface::setTransientPopup(Surface *parent, Seat *grabbedSeat, quint32 grabSerial, const QPoint &offset, TransientFlags flags) +{ + Q_ASSERT(isValid()); + Q_ASSERT(parent); + Q_ASSERT(grabbedSeat); + uint32_t wlFlags = 0; + if (flags.testFlag(TransientFlag::NoFocus)) { + wlFlags |= WL_SHELL_SURFACE_TRANSIENT_INACTIVE; + } + wl_shell_surface_set_popup(d->surface, *grabbedSeat, grabSerial, *parent, offset.x(), offset.y(), wlFlags); +} + void ShellSurface::requestMove(Seat *seat, quint32 serial) { Q_ASSERT(isValid()); diff --git a/src/server/shell_interface.h b/src/server/shell_interface.h --- a/src/server/shell_interface.h +++ b/src/server/shell_interface.h @@ -231,6 +231,18 @@ **/ bool acceptsKeyboardFocus() const; + /** + * Sends a popup done event to the shell surface. + * This is only relevant for popup windows. It indicates that the popup grab + * got canceled. This happens when e.g. the user clicks outside of any surface + * of the same client as this ShellSurfaceInterface. It is the task of the + * compositor to send the popupDone event appropriately. + * + * @see isPopup + * @since 5.33 + **/ + void popupDone(); + Q_SIGNALS: /** * Emitted whenever the title changes. diff --git a/src/server/shell_interface.cpp b/src/server/shell_interface.cpp --- a/src/server/shell_interface.cpp +++ b/src/server/shell_interface.cpp @@ -379,6 +379,9 @@ emit s->q_func()->transientChanged(!s->transientFor.isNull()); emit s->q_func()->transientOffsetChanged(s->transientOffset); emit s->q_func()->transientForChanged(); + // we ignore the flags as Qt requests keyboard focus for popups + // if we would honor the flag this could break compositors + // compare QtWayland (5.6), file qwaylandwlshellsurface.cpp:208 s->setAcceptsFocus(WL_SHELL_SURFACE_TRANSIENT_INACTIVE); } @@ -449,6 +452,14 @@ return d->acceptsKeyboardFocus; } +void ShellSurfaceInterface::popupDone() +{ + Q_D(); + if (isPopup() && d->resource) { + wl_shell_surface_send_popup_done(d->resource); + } +} + QPointer< SurfaceInterface > ShellSurfaceInterface::transientFor() const { Q_D();