diff --git a/CMakeLists.txt b/CMakeLists.txt --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -436,6 +436,7 @@ appmenu.cpp modifier_only_shortcuts.cpp xkb.cpp + popup_input_filter.cpp ) if(KWIN_BUILD_TABBOX) diff --git a/autotests/integration/pointer_input.cpp b/autotests/integration/pointer_input.cpp --- a/autotests/integration/pointer_input.cpp +++ b/autotests/integration/pointer_input.cpp @@ -70,6 +70,7 @@ void testMouseActionActiveWindow(); void testCursorImage(); void testEffectOverrideCursorImage(); + void testPopup(); private: void render(KWayland::Client::Surface *surface, const QSize &size = QSize(100, 50)); @@ -918,6 +919,86 @@ 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 --- a/input.cpp +++ b/input.cpp @@ -40,6 +40,7 @@ #include "libinput/device.h" #endif #include "platform.h" +#include "popup_input_filter.h" #include "shell_client.h" #include "wayland_server.h" #include @@ -1504,6 +1505,7 @@ 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 --- a/pointer_input.cpp +++ b/pointer_input.cpp @@ -387,7 +387,6 @@ 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.h b/popup_input_filter.h new file mode 100644 --- /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/popup_input_filter.cpp b/popup_input_filter.cpp new file mode 100644 --- /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/shell_client.h b/shell_client.h --- a/shell_client.h +++ b/shell_client.h @@ -139,6 +139,9 @@ 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/shell_client.cpp b/shell_client.cpp --- a/shell_client.cpp +++ b/shell_client.cpp @@ -1496,4 +1496,20 @@ } } +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/toplevel.h b/toplevel.h --- a/toplevel.h +++ b/toplevel.h @@ -394,6 +394,32 @@ 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. * * The method is templated to operate on either a list of Toplevels or on a list of