From 1193b0da771a5d1042bf2aed0a2727f89ddf488e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20Gr=C3=A4=C3=9Flin?= Date: Sat, 25 Mar 2017 18:41:28 +0100 Subject: [PATCH] Initial support for popup window handling Summary: So far KWin did not properly handle popup windows. That is when a popup surface got created and a click outside the surface happened KWin did not send out the popupDone Wayland event. This change makes KWin aware of whether a surface is a popup and tracks through a new PopupInputFilter whether there are popup windows. In case there are popups the new filter waits for mouse press events and cancels the popups if the press does not happen on any surface belonging to the same client. To quote the relevant section of the Wayland documentation: The popup grab continues until the window is destroyed or a mouse button is pressed in any other client's window. A click in any of the client's surfaces is reported as normal, however, clicks in other clients' surfaces will be discarded and trigger the callback. So far the support is still incomplete. Not yet implemented are: * support xdg_shell popup windows * verifying whether the popup is allowed to be a popup * cancel the popup on more global interactions like screen lock or kwin effect BUG: 366609 FIXED-IN: 5.10 Test Plan: Auto test and manual testing with QtWayland client Reviewers: #plasma, #kwin Subscribers: plasma-devel, kwin Tags: #kwin Differential Revision: https://phabricator.kde.org/D5177 --- CMakeLists.txt | 1 + autotests/integration/pointer_input.cpp | 81 +++++++++++++++++++++++++ input.cpp | 2 + pointer_input.cpp | 1 - popup_input_filter.cpp | 80 ++++++++++++++++++++++++ popup_input_filter.h | 50 +++++++++++++++ shell_client.cpp | 16 +++++ shell_client.h | 3 + toplevel.h | 26 ++++++++ 9 files changed, 259 insertions(+), 1 deletion(-) create mode 100644 popup_input_filter.cpp create mode 100644 popup_input_filter.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 9f6205d95..2c1ed0dfb 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -437,6 +437,7 @@ set(kwin_KDEINIT_SRCS modifier_only_shortcuts.cpp xkb.cpp gestures.cpp + popup_input_filter.cpp ) if(KWIN_BUILD_TABBOX) diff --git a/autotests/integration/pointer_input.cpp b/autotests/integration/pointer_input.cpp index 6fbb5fc1c..bfc39a5aa 100644 --- a/autotests/integration/pointer_input.cpp +++ b/autotests/integration/pointer_input.cpp @@ -72,6 +72,7 @@ private Q_SLOTS: void testMouseActionActiveWindow(); void testCursorImage(); void testEffectOverrideCursorImage(); + void testPopup(); private: void render(KWayland::Client::Surface *surface, const QSize &size = QSize(100, 50)); @@ -964,6 +965,86 @@ void PointerInputTest::testEffectOverrideCursorImage() QVERIFY(p->cursorImage().isNull()); } +void PointerInputTest::testPopup() +{ + // this test validates the basic popup behavior + // a button press outside the window should dismiss the popup + + // first create a parent surface + using namespace KWayland::Client; + auto pointer = m_seat->createPointer(m_seat); + QVERIFY(pointer); + QVERIFY(pointer->isValid()); + QSignalSpy enteredSpy(pointer, &Pointer::entered); + QVERIFY(enteredSpy.isValid()); + QSignalSpy leftSpy(pointer, &Pointer::left); + QVERIFY(leftSpy.isValid()); + QSignalSpy buttonStateChangedSpy(pointer, &Pointer::buttonStateChanged); + QVERIFY(buttonStateChangedSpy.isValid()); + QSignalSpy motionSpy(pointer, &Pointer::motion); + QVERIFY(motionSpy.isValid()); + + Cursor::setPos(800, 800); + + QSignalSpy clientAddedSpy(waylandServer(), &WaylandServer::shellClientAdded); + QVERIFY(clientAddedSpy.isValid()); + Surface *surface = Test::createSurface(m_compositor); + QVERIFY(surface); + ShellSurface *shellSurface = Test::createShellSurface(surface, surface); + QVERIFY(shellSurface); + render(surface); + QVERIFY(clientAddedSpy.wait()); + AbstractClient *window = workspace()->activeClient(); + QVERIFY(window); + QCOMPARE(window->hasPopupGrab(), false); + // move pointer into window + QVERIFY(!window->geometry().contains(QPoint(800, 800))); + Cursor::setPos(window->geometry().center()); + QVERIFY(enteredSpy.wait()); + // click inside window to create serial + quint32 timestamp = 0; + kwinApp()->platform()->pointerButtonPressed(BTN_LEFT, timestamp++); + kwinApp()->platform()->pointerButtonReleased(BTN_LEFT, timestamp++); + QVERIFY(buttonStateChangedSpy.wait()); + + // now create the popup surface + Surface *popupSurface = Test::createSurface(m_compositor); + QVERIFY(popupSurface); + ShellSurface *popupShellSurface = Test::createShellSurface(popupSurface, popupSurface); + QVERIFY(popupShellSurface); + QSignalSpy popupDoneSpy(popupShellSurface, &ShellSurface::popupDone); + QVERIFY(popupDoneSpy.isValid()); + // TODO: proper serial + popupShellSurface->setTransientPopup(surface, m_seat, 0, QPoint(80, 20)); + render(popupSurface); + QVERIFY(clientAddedSpy.wait()); + auto popupClient = clientAddedSpy.last().first().value(); + QVERIFY(popupClient); + QVERIFY(popupClient != window); + QCOMPARE(window, workspace()->activeClient()); + QCOMPARE(popupClient->transientFor(), window); + QCOMPARE(popupClient->pos(), window->pos() + QPoint(80, 20)); + QCOMPARE(popupClient->hasPopupGrab(), true); + + // let's move the pointer into the center of the window + Cursor::setPos(popupClient->geometry().center()); + QVERIFY(enteredSpy.wait()); + QCOMPARE(enteredSpy.count(), 2); + QCOMPARE(leftSpy.count(), 1); + QCOMPARE(pointer->enteredSurface(), popupSurface); + + // let's move the pointer outside of the popup window + // this should not really change anything, it gets a leave event + Cursor::setPos(popupClient->geometry().bottomRight() + QPoint(2, 2)); + QVERIFY(leftSpy.wait()); + QCOMPARE(leftSpy.count(), 2); + QVERIFY(popupDoneSpy.isEmpty()); + // now click, should trigger popupDone + kwinApp()->platform()->pointerButtonPressed(BTN_LEFT, timestamp++); + QVERIFY(popupDoneSpy.wait()); + kwinApp()->platform()->pointerButtonReleased(BTN_LEFT, timestamp++); +} + } WAYLANDTEST_MAIN(KWin::PointerInputTest) diff --git a/input.cpp b/input.cpp index 8d4e10e5e..9d8c39cd0 100644 --- a/input.cpp +++ b/input.cpp @@ -41,6 +41,7 @@ along with this program. If not, see . #include "libinput/device.h" #endif #include "platform.h" +#include "popup_input_filter.h" #include "shell_client.h" #include "wayland_server.h" #include @@ -1609,6 +1610,7 @@ void InputRedirection::setupInputFilters() installInputEventFilter(new TerminateServerFilter); installInputEventFilter(new DragAndDropInputFilter); installInputEventFilter(new LockScreenFilter); + installInputEventFilter(new PopupInputFilter); m_pointerConstraintsFilter = new PointerConstraintsFilter; installInputEventFilter(m_pointerConstraintsFilter); m_windowSelector = new WindowSelectorFilter; diff --git a/pointer_input.cpp b/pointer_input.cpp index 95e128d08..65f28fecb 100644 --- a/pointer_input.cpp +++ b/pointer_input.cpp @@ -436,7 +436,6 @@ void PointerInputRedirection::update() if (input()->isSelectingWindow()) { return; } - // TODO: handle pointer grab aka popups Toplevel *t = m_input->findToplevel(m_pos.toPoint()); const auto oldDeco = m_decoration; updateInternalWindow(m_pos); diff --git a/popup_input_filter.cpp b/popup_input_filter.cpp new file mode 100644 index 000000000..9d54984da --- /dev/null +++ b/popup_input_filter.cpp @@ -0,0 +1,80 @@ +/* + * Copyright 2017 Martin Graesslin + * + * 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) version 3 or any later version + * accepted by the membership of KDE e.V. (or its successor approved + * by the membership of KDE e.V.), which shall act as a proxy + * defined in Section 14 of version 3 of the license. + * + * 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 "popup_input_filter.h" +#include "deleted.h" +#include "shell_client.h" +#include "wayland_server.h" + +#include + +namespace KWin +{ + +PopupInputFilter::PopupInputFilter() + : QObject() +{ + connect(waylandServer(), &WaylandServer::shellClientAdded, this, &PopupInputFilter::handleClientAdded); +} + +void PopupInputFilter::handleClientAdded(Toplevel *client) +{ + if (m_popupClients.contains(client)) { + return; + } + if (client->hasPopupGrab()) { + // TODO: verify that the Toplevel is allowed as a popup + connect(client, &Toplevel::windowShown, this, &PopupInputFilter::handleClientAdded, Qt::UniqueConnection); + connect(client, &Toplevel::windowClosed, this, &PopupInputFilter::handleClientRemoved, Qt::UniqueConnection); + m_popupClients << client; + } +} + +void PopupInputFilter::handleClientRemoved(Toplevel *client) +{ + m_popupClients.removeOne(client); +} +bool PopupInputFilter::pointerEvent(QMouseEvent *event, quint32 nativeButton) +{ + Q_UNUSED(nativeButton) + if (m_popupClients.isEmpty()) { + return false; + } + if (event->type() == QMouseEvent::MouseButtonPress) { + auto pointerFocus = qobject_cast(input()->findToplevel(event->globalPos())); + if (!pointerFocus || !AbstractClient::belongToSameApplication(pointerFocus, qobject_cast(m_popupClients.constLast()))) { + // a press on a window (or no window) not belonging to the popup window + cancelPopups(); + // filter out this press + return true; + } + } + return false; +} + +void PopupInputFilter::cancelPopups() +{ + while (!m_popupClients.isEmpty()) { + auto c = m_popupClients.takeLast(); + c->popupDone(); + } +} + +} diff --git a/popup_input_filter.h b/popup_input_filter.h new file mode 100644 index 000000000..b49a74663 --- /dev/null +++ b/popup_input_filter.h @@ -0,0 +1,50 @@ +/* + * Copyright 2017 Martin Graesslin + * + * 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) version 3 or any later version + * accepted by the membership of KDE e.V. (or its successor approved + * by the membership of KDE e.V.), which shall act as a proxy + * defined in Section 14 of version 3 of the license. + * + * 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_POPUP_INPUT_FILTER +#define KWIN_POPUP_INPUT_FILTER + +#include "input.h" + +#include +#include + +namespace KWin +{ +class Toplevel; +class ShellClient; + +class PopupInputFilter : public QObject, public InputEventFilter +{ + Q_OBJECT +public: + explicit PopupInputFilter(); + bool pointerEvent(QMouseEvent *event, quint32 nativeButton) override; +private: + void handleClientAdded(Toplevel *client); + void handleClientRemoved(Toplevel *client); + void disconnectClient(Toplevel *client); + void cancelPopups(); + + QVector m_popupClients; +}; +} + +#endif diff --git a/shell_client.cpp b/shell_client.cpp index 3c2daa192..8bc52bc4e 100644 --- a/shell_client.cpp +++ b/shell_client.cpp @@ -1499,4 +1499,20 @@ void ShellClient::updateApplicationMenu() } } +bool ShellClient::hasPopupGrab() const +{ + if (m_shellSurface) { + // TODO: verify grab serial + return m_shellSurface->isPopup(); + } + return false; +} + +void ShellClient::popupDone() +{ + if (m_shellSurface) { + m_shellSurface->popupDone(); + } +} + } diff --git a/shell_client.h b/shell_client.h index 54fca4d18..24a46faf4 100644 --- a/shell_client.h +++ b/shell_client.h @@ -139,6 +139,9 @@ public: void updateApplicationMenu(); + bool hasPopupGrab() const override; + void popupDone() override; + protected: void addDamage(const QRegion &damage) override; bool belongsToSameApplication(const AbstractClient *other, bool active_hack) const override; diff --git a/toplevel.h b/toplevel.h index 7f78944a4..3737c1273 100644 --- a/toplevel.h +++ b/toplevel.h @@ -393,6 +393,32 @@ public: **/ virtual QMatrix4x4 inputTransformation() const; + /** + * The window has a popup grab. This means that when it got mapped the + * parent window had an implicit (pointer) grab. + * + * Normally this is only relevant for transient windows. + * + * Once the popup grab ends (e.g. pointer press outside of any Toplevel of + * the client), the method popupDone should be invoked. + * + * The default implementation returns @c false. + * @see popupDone + * @since 5.10 + **/ + virtual bool hasPopupGrab() const { + return false; + } + /** + * This method should be invoked for Toplevels with a popup grab when + * the grab ends. + * + * The default implementation does nothing. + * @see hasPopupGrab + * @since 5.10 + **/ + virtual void popupDone() {}; + /** * @brief Finds the Toplevel matching the condition expressed in @p func in @p list. *