wayland: Introduce internal popup event filter

The event filter allows dismissing the user actions menu by clicking
anywhere outside of it.

BUG: 428408
This commit is contained in:
Vlad Zahorodnii 2021-01-30 22:29:55 +02:00
parent 30464e5c8b
commit f5925e2f17
6 changed files with 104 additions and 51 deletions

View file

@ -9,6 +9,7 @@
#include "kwin_wayland_test.h"
#include "platform.h"
#include "cursor.h"
#include "deleted.h"
#include "effects.h"
#include "internal_client.h"
#include "screens.h"
@ -65,6 +66,7 @@ private Q_SLOTS:
void testChangeWindowType();
void testEffectWindow();
void testReentrantSetFrameGeometry();
void testDismissPopup();
};
class HelperWindow : public QRasterWindow
@ -173,6 +175,7 @@ void HelperWindow::keyReleaseEvent(QKeyEvent *event)
void InternalWindowTest::initTestCase()
{
qRegisterMetaType<KWin::AbstractClient *>();
qRegisterMetaType<KWin::Deleted *>();
qRegisterMetaType<KWin::InternalClient *>();
QSignalSpy applicationStartedSpy(kwinApp(), &Application::started);
QVERIFY(applicationStartedSpy.isValid());
@ -809,6 +812,41 @@ void InternalWindowTest::testReentrantSetFrameGeometry()
QCOMPARE(client->pos(), QPoint(100, 100));
}
void InternalWindowTest::testDismissPopup()
{
// This test verifies that a popup window created by the compositor will be dismissed
// when user clicks another window.
// Create a toplevel window.
QSignalSpy clientAddedSpy(workspace(), &Workspace::internalClientAdded);
QVERIFY(clientAddedSpy.isValid());
HelperWindow clientToplevel;
clientToplevel.setGeometry(0, 0, 100, 100);
clientToplevel.show();
QTRY_COMPARE(clientAddedSpy.count(), 1);
auto serverToplevel = clientAddedSpy.last().first().value<InternalClient *>();
QVERIFY(serverToplevel);
// Create a popup window.
QRasterWindow clientPopup;
clientPopup.setFlag(Qt::Popup);
clientPopup.setTransientParent(&clientToplevel);
clientPopup.setGeometry(0, 0, 50, 50);
clientPopup.show();
QTRY_COMPARE(clientAddedSpy.count(), 2);
auto serverPopup = clientAddedSpy.last().first().value<InternalClient *>();
QVERIFY(serverPopup);
// Click somewhere outside the popup window.
QSignalSpy popupClosedSpy(serverPopup, &InternalClient::windowClosed);
quint32 timestamp = 0;
kwinApp()->platform()->pointerMotion(QPointF(serverPopup->x() + serverPopup->width() + 1,
serverPopup->y() + serverPopup->height() + 1),
timestamp++);
kwinApp()->platform()->pointerButtonPressed(BTN_LEFT, timestamp++);
QTRY_COMPARE(popupClosedSpy.count(), 1);
}
}
WAYLANDTEST_MAIN(KWin::InternalWindowTest)

View file

@ -2560,10 +2560,44 @@ Toplevel *InputRedirection::findToplevel(const QPoint &pos)
return u;
}
}
if (Toplevel *window = findInternal(pos)) {
return window;
}
}
return findManagedToplevel(pos);
}
Toplevel *InputRedirection::findInternal(const QPoint &pos) const
{
const QList<InternalClient *> &internalClients = workspace()->internalClients();
if (internalClients.isEmpty()) {
return nullptr;
}
auto it = internalClients.end();
do {
--it;
QWindow *w = (*it)->internalWindow();
if (!w || !w->isVisible()) {
continue;
}
if (!(*it)->frameGeometry().contains(pos)) {
continue;
}
// check input mask
const QRegion mask = w->mask().translated(w->geometry().topLeft());
if (!mask.isEmpty() && !mask.contains(pos)) {
continue;
}
if (w->property("outputOnly").toBool()) {
continue;
}
return *it;
} while (it != internalClients.begin());
return nullptr;
}
Toplevel *InputRedirection::findManagedToplevel(const QPoint &pos)
{
if (!Workspace::self()) {
@ -2785,16 +2819,8 @@ void InputDeviceHandler::update()
}
Toplevel *toplevel = nullptr;
QWindow *internalWindow = nullptr;
if (positionValid()) {
const auto pos = position().toPoint();
internalWindow = findInternalWindow(pos);
if (internalWindow) {
toplevel = workspace()->findInternal(internalWindow);
} else {
toplevel = input()->findToplevel(pos);
}
toplevel = input()->findToplevel(position().toPoint());
}
// Always set the toplevel at the position of the input device.
setAt(toplevel);
@ -2803,11 +2829,12 @@ void InputDeviceHandler::update()
return;
}
if (internalWindow) {
if (m_focus.internalWindow != internalWindow) {
if (auto client = qobject_cast<InternalClient *>(toplevel)) {
QWindow *handle = client->internalWindow();
if (m_focus.internalWindow != handle) {
// changed internal window
updateDecoration();
updateInternalWindow(internalWindow);
updateInternalWindow(handle);
updateFocus();
} else if (updateDecoration()) {
// went onto or off from decoration, update focus
@ -2850,39 +2877,4 @@ QWindow *InputDeviceHandler::internalWindow() const
return m_focus.internalWindow;
}
QWindow* InputDeviceHandler::findInternalWindow(const QPoint &pos) const
{
if (waylandServer()->isScreenLocked()) {
return nullptr;
}
const QList<InternalClient *> &internalClients = workspace()->internalClients();
if (internalClients.isEmpty()) {
return nullptr;
}
auto it = internalClients.end();
do {
--it;
QWindow *w = (*it)->internalWindow();
if (!w || !w->isVisible()) {
continue;
}
if (!(*it)->frameGeometry().contains(pos)) {
continue;
}
// check input mask
const QRegion mask = w->mask().translated(w->geometry().topLeft());
if (!mask.isEmpty() && !mask.contains(pos)) {
continue;
}
if (w->property("outputOnly").toBool()) {
continue;
}
return w;
} while (it != internalClients.begin());
return nullptr;
}
} // namespace

View file

@ -316,6 +316,7 @@ private:
void reconfigure();
void setupInputFilters();
void installInputEventFilter(InputEventFilter *filter);
Toplevel *findInternal(const QPoint &pos) const;
KeyboardInputRedirection *m_keyboard;
PointerInputRedirection *m_pointer;
TabletInputRedirection *m_tablet;
@ -497,8 +498,6 @@ private:
bool updateDecoration();
void updateInternalWindow(QWindow *window);
QWindow* findInternalWindow(const QPoint &pos) const;
struct {
QPointer<Toplevel> at;
QMetaObject::Connection surfaceCreatedConnection;

View file

@ -14,6 +14,7 @@
#include <KDecoration2/Decoration>
#include <QMouseEvent>
#include <QOpenGLFramebufferObject>
#include <QWindow>
@ -388,6 +389,18 @@ void InternalClient::destroyClient()
delete this;
}
bool InternalClient::hasPopupGrab() const
{
return !m_internalWindow->flags().testFlag(Qt::WindowTransparentForInput) &&
m_internalWindow->flags().testFlag(Qt::Popup) &&
!m_internalWindow->flags().testFlag(Qt::ToolTip);
}
void InternalClient::popupDone()
{
m_internalWindow->hide();
}
void InternalClient::present(const QSharedPointer<QOpenGLFramebufferObject> fbo)
{
Q_ASSERT(m_internalImage.isNull());
@ -438,8 +451,15 @@ bool InternalClient::acceptsFocus() const
bool InternalClient::belongsToSameApplication(const AbstractClient *other, SameApplicationChecks checks) const
{
Q_UNUSED(checks)
return qobject_cast<const InternalClient *>(other) != nullptr;
const InternalClient *otherInternal = qobject_cast<const InternalClient *>(other);
if (!otherInternal) {
return false;
}
if (otherInternal == this) {
return true;
}
return otherInternal->internalWindow()->isAncestorOf(internalWindow()) ||
internalWindow()->isAncestorOf(otherInternal->internalWindow());
}
void InternalClient::doMove(int x, int y)

View file

@ -63,6 +63,8 @@ public:
void setNoBorder(bool set) override;
void updateDecoration(bool check_workspace_pos, bool force = false) override;
void destroyClient() override;
bool hasPopupGrab() const override;
void popupDone() override;
void present(const QSharedPointer<QOpenGLFramebufferObject> fbo);
void present(const QImage &image, const QRegion &damage);

View file

@ -7,6 +7,7 @@
#include "popup_input_filter.h"
#include "abstract_client.h"
#include "deleted.h"
#include "internal_client.h"
#include "workspace.h"
#include <QMouseEvent>
@ -18,6 +19,7 @@ PopupInputFilter::PopupInputFilter()
: QObject()
{
connect(workspace(), &Workspace::clientAdded, this, &PopupInputFilter::handleClientAdded);
connect(workspace(), &Workspace::internalClientAdded, this, &PopupInputFilter::handleClientAdded);
}
void PopupInputFilter::handleClientAdded(Toplevel *client)