e963ae09cc
Currently there is a 150ms delay before screen edge actions and hotcorners are triggered; the cursor has to spend that much time there before they'll activate. This is good for preventing accidental activations, but also prevents and delays deliberate activations, which are likely to be more annoying for the user. To alleviate this, the delay is reduced to 75ms, which still prevents most of the accidental activations in my testing, while making deliberate activations faster and easier.
503 lines
17 KiB
C++
503 lines
17 KiB
C++
/*
|
|
KWin - the KDE window manager
|
|
This file is part of the KDE project.
|
|
|
|
SPDX-FileCopyrightText: 2014 Martin Gräßlin <mgraesslin@kde.org>
|
|
SPDX-FileCopyrightText: 2021 Vlad Zahorodnii <vlad.zahorodnii@kde.org>
|
|
|
|
SPDX-License-Identifier: GPL-2.0-or-later
|
|
*/
|
|
|
|
#include "kwin_wayland_test.h"
|
|
|
|
#include "atoms.h"
|
|
#include "cursor.h"
|
|
#include "effect/effectloader.h"
|
|
#include "main.h"
|
|
#include "pointer_input.h"
|
|
#include "screenedge.h"
|
|
#include "wayland_server.h"
|
|
#include "window.h"
|
|
#include "workspace.h"
|
|
|
|
#include <KConfigGroup>
|
|
#include <KWayland/Client/surface.h>
|
|
|
|
#include <QAbstractEventDispatcher>
|
|
#include <QAction>
|
|
#include <QSocketNotifier>
|
|
|
|
#include <xcb/xcb_icccm.h>
|
|
|
|
Q_DECLARE_METATYPE(KWin::ElectricBorder)
|
|
|
|
namespace KWin
|
|
{
|
|
|
|
static const QString s_socketName = QStringLiteral("wayland_test_kwin_screen-edges-0");
|
|
|
|
class TestObject : public QObject
|
|
{
|
|
Q_OBJECT
|
|
|
|
public Q_SLOTS:
|
|
bool callback(ElectricBorder border)
|
|
{
|
|
Q_EMIT gotCallback(border);
|
|
return true;
|
|
}
|
|
|
|
Q_SIGNALS:
|
|
void gotCallback(KWin::ElectricBorder);
|
|
};
|
|
|
|
class ScreenEdgesTest : public QObject
|
|
{
|
|
Q_OBJECT
|
|
|
|
private Q_SLOTS:
|
|
void initTestCase();
|
|
void init();
|
|
void cleanup();
|
|
void testTouchCallback_data();
|
|
void testTouchCallback();
|
|
void testPushBack_data();
|
|
void testPushBack();
|
|
void testObjectEdge_data();
|
|
void testObjectEdge();
|
|
void testKdeNetWmScreenEdgeShow();
|
|
};
|
|
|
|
void ScreenEdgesTest::initTestCase()
|
|
{
|
|
qRegisterMetaType<KWin::Window *>();
|
|
qRegisterMetaType<KWin::ElectricBorder>("ElectricBorder");
|
|
|
|
QSignalSpy applicationStartedSpy(kwinApp(), &Application::started);
|
|
QVERIFY(waylandServer()->init(s_socketName));
|
|
Test::setOutputConfig({QRect(0, 0, 1280, 1024)});
|
|
|
|
// Disable effects, in particular present windows, which reserves a screen edge.
|
|
auto config = kwinApp()->config();
|
|
KConfigGroup plugins(config, QStringLiteral("Plugins"));
|
|
const auto builtinNames = EffectLoader().listOfKnownEffects();
|
|
for (const QString &name : builtinNames) {
|
|
plugins.writeEntry(name + QStringLiteral("Enabled"), false);
|
|
}
|
|
|
|
config->sync();
|
|
kwinApp()->setConfig(config);
|
|
|
|
kwinApp()->start();
|
|
QVERIFY(applicationStartedSpy.wait());
|
|
}
|
|
|
|
void ScreenEdgesTest::init()
|
|
{
|
|
workspace()->screenEdges()->recreateEdges();
|
|
Workspace::self()->setActiveOutput(QPoint(640, 512));
|
|
KWin::input()->pointer()->warp(QPoint(640, 512));
|
|
|
|
QVERIFY(Test::setupWaylandConnection());
|
|
}
|
|
|
|
void ScreenEdgesTest::cleanup()
|
|
{
|
|
Test::destroyWaylandConnection();
|
|
}
|
|
|
|
void ScreenEdgesTest::testTouchCallback_data()
|
|
{
|
|
QTest::addColumn<KWin::ElectricBorder>("border");
|
|
QTest::addColumn<QPointF>("startPos");
|
|
QTest::addColumn<QPointF>("delta");
|
|
|
|
QTest::newRow("left") << ElectricLeft << QPointF(0, 50) << QPointF(256, 20);
|
|
QTest::newRow("top") << ElectricTop << QPointF(50, 0) << QPointF(20, 250);
|
|
QTest::newRow("right") << ElectricRight << QPointF(1279, 50) << QPointF(-256, 0);
|
|
QTest::newRow("bottom") << ElectricBottom << QPointF(50, 1023) << QPointF(0, -205);
|
|
}
|
|
|
|
void ScreenEdgesTest::testTouchCallback()
|
|
{
|
|
// This test verifies that touch screen edges trigger associated callbacks.
|
|
|
|
auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig);
|
|
auto group = config->group(QStringLiteral("TouchEdges"));
|
|
group.writeEntry("Top", "none");
|
|
group.writeEntry("Left", "none");
|
|
group.writeEntry("Bottom", "none");
|
|
group.writeEntry("Right", "none");
|
|
config->sync();
|
|
|
|
auto s = workspace()->screenEdges();
|
|
s->setConfig(config);
|
|
s->reconfigure();
|
|
|
|
// none of our actions should be reserved
|
|
const auto &edges = s->edges();
|
|
QCOMPARE(edges.size(), 8);
|
|
for (auto &edge : edges) {
|
|
QCOMPARE(edge->isReserved(), false);
|
|
QCOMPARE(edge->activatesForPointer(), false);
|
|
QCOMPARE(edge->activatesForTouchGesture(), false);
|
|
}
|
|
|
|
// let's reserve an action
|
|
QAction action;
|
|
QSignalSpy actionTriggeredSpy(&action, &QAction::triggered);
|
|
|
|
// reserve on edge
|
|
QFETCH(KWin::ElectricBorder, border);
|
|
s->reserveTouch(border, &action);
|
|
for (auto &edge : edges) {
|
|
QCOMPARE(edge->isReserved(), edge->border() == border);
|
|
QCOMPARE(edge->activatesForPointer(), false);
|
|
QCOMPARE(edge->activatesForTouchGesture(), edge->border() == border);
|
|
}
|
|
|
|
quint32 timestamp = 0;
|
|
|
|
// press the finger
|
|
QFETCH(QPointF, startPos);
|
|
Test::touchDown(1, startPos, timestamp++);
|
|
QVERIFY(actionTriggeredSpy.isEmpty());
|
|
|
|
// move the finger
|
|
QFETCH(QPointF, delta);
|
|
Test::touchMotion(1, startPos + delta, timestamp++);
|
|
QVERIFY(actionTriggeredSpy.isEmpty());
|
|
|
|
// release the finger
|
|
Test::touchUp(1, timestamp++);
|
|
QVERIFY(actionTriggeredSpy.wait());
|
|
QCOMPARE(actionTriggeredSpy.count(), 1);
|
|
|
|
// unreserve again
|
|
s->unreserveTouch(border, &action);
|
|
for (auto &edge : edges) {
|
|
QCOMPARE(edge->isReserved(), false);
|
|
QCOMPARE(edge->activatesForPointer(), false);
|
|
QCOMPARE(edge->activatesForTouchGesture(), false);
|
|
}
|
|
|
|
// reserve another action
|
|
std::unique_ptr<QAction> action2(new QAction);
|
|
s->reserveTouch(border, action2.get());
|
|
for (auto &edge : edges) {
|
|
QCOMPARE(edge->isReserved(), edge->border() == border);
|
|
QCOMPARE(edge->activatesForPointer(), false);
|
|
QCOMPARE(edge->activatesForTouchGesture(), edge->border() == border);
|
|
}
|
|
|
|
// and unreserve by destroying
|
|
action2.reset();
|
|
for (auto &edge : edges) {
|
|
QCOMPARE(edge->isReserved(), false);
|
|
QCOMPARE(edge->activatesForPointer(), false);
|
|
QCOMPARE(edge->activatesForTouchGesture(), false);
|
|
}
|
|
}
|
|
|
|
void ScreenEdgesTest::testPushBack_data()
|
|
{
|
|
QTest::addColumn<KWin::ElectricBorder>("border");
|
|
QTest::addColumn<int>("pushback");
|
|
QTest::addColumn<QPointF>("trigger");
|
|
QTest::addColumn<QPointF>("expected");
|
|
|
|
QTest::newRow("top-left-3") << ElectricTopLeft << 3 << QPointF(0, 0) << QPointF(3, 3);
|
|
QTest::newRow("top-5") << ElectricTop << 5 << QPointF(50, 0) << QPointF(50, 5);
|
|
QTest::newRow("top-right-2") << ElectricTopRight << 2 << QPointF(1279, 0) << QPointF(1277, 2);
|
|
QTest::newRow("right-10") << ElectricRight << 10 << QPointF(1279, 50) << QPointF(1269, 50);
|
|
QTest::newRow("bottom-right-5") << ElectricBottomRight << 5 << QPointF(1279, 1023) << QPointF(1274, 1018);
|
|
QTest::newRow("bottom-10") << ElectricBottom << 10 << QPointF(50, 1023) << QPointF(50, 1013);
|
|
QTest::newRow("bottom-left-3") << ElectricBottomLeft << 3 << QPointF(0, 1023) << QPointF(3, 1020);
|
|
QTest::newRow("left-10") << ElectricLeft << 10 << QPointF(0, 50) << QPointF(10, 50);
|
|
QTest::newRow("invalid") << ElectricLeft << 10 << QPointF(50, 0) << QPointF(50, 0);
|
|
}
|
|
|
|
void ScreenEdgesTest::testPushBack()
|
|
{
|
|
// This test verifies that the pointer will be pushed back if it approached a screen edge.
|
|
|
|
QFETCH(int, pushback);
|
|
auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig);
|
|
config->group(QStringLiteral("Windows")).writeEntry("ElectricBorderPushbackPixels", pushback);
|
|
config->sync();
|
|
|
|
auto s = workspace()->screenEdges();
|
|
s->setConfig(config);
|
|
s->reconfigure();
|
|
|
|
TestObject callback;
|
|
QSignalSpy spy(&callback, &TestObject::gotCallback);
|
|
|
|
QFETCH(ElectricBorder, border);
|
|
s->reserve(border, &callback, "callback");
|
|
|
|
QFETCH(QPointF, trigger);
|
|
Test::pointerMotion(trigger, 0);
|
|
QVERIFY(spy.isEmpty());
|
|
QTEST(Cursors::self()->mouse()->pos(), "expected");
|
|
}
|
|
|
|
void ScreenEdgesTest::testObjectEdge_data()
|
|
{
|
|
QTest::addColumn<ElectricBorder>("border");
|
|
QTest::addColumn<QPointF>("triggerPoint");
|
|
QTest::addColumn<QPointF>("delta");
|
|
|
|
QTest::newRow("top") << ElectricTop << QPointF(640, 0) << QPointF(0, 50);
|
|
QTest::newRow("right") << ElectricRight << QPointF(1279, 512) << QPointF(-50, 0);
|
|
QTest::newRow("bottom") << ElectricBottom << QPointF(640, 1023) << QPointF(0, -50);
|
|
QTest::newRow("left") << ElectricLeft << QPointF(0, 512) << QPointF(50, 0);
|
|
}
|
|
|
|
void ScreenEdgesTest::testObjectEdge()
|
|
{
|
|
// This test verifies that a screen edge reserved by a script or any QObject is activated.
|
|
|
|
TestObject callback;
|
|
QSignalSpy spy(&callback, &TestObject::gotCallback);
|
|
|
|
// Reserve a screen edge border.
|
|
QFETCH(ElectricBorder, border);
|
|
workspace()->screenEdges()->reserve(border, &callback, "callback");
|
|
|
|
QFETCH(QPointF, triggerPoint);
|
|
QFETCH(QPointF, delta);
|
|
|
|
// doesn't trigger as the edge was not triggered yet
|
|
qint64 timestamp = 0;
|
|
Test::pointerMotion(triggerPoint + delta, timestamp);
|
|
QVERIFY(spy.isEmpty());
|
|
|
|
// test doesn't trigger due to too much offset
|
|
timestamp += 160;
|
|
Test::pointerMotion(triggerPoint, timestamp);
|
|
QVERIFY(spy.isEmpty());
|
|
|
|
// doesn't activate as we are waiting too short
|
|
timestamp += 50;
|
|
Test::pointerMotion(triggerPoint, timestamp);
|
|
QVERIFY(spy.isEmpty());
|
|
|
|
// and this one triggers
|
|
timestamp += 110;
|
|
Test::pointerMotion(triggerPoint, timestamp);
|
|
QVERIFY(!spy.isEmpty());
|
|
|
|
// now let's try to trigger again
|
|
timestamp += 351;
|
|
Test::pointerMotion(triggerPoint, timestamp);
|
|
QCOMPARE(spy.count(), 1);
|
|
|
|
// it's still under the reactivation
|
|
timestamp += 50;
|
|
Test::pointerMotion(triggerPoint, timestamp);
|
|
QCOMPARE(spy.count(), 1);
|
|
|
|
// now it should trigger again
|
|
timestamp += 250;
|
|
Test::pointerMotion(triggerPoint, timestamp);
|
|
QCOMPARE(spy.count(), 2);
|
|
}
|
|
|
|
static void enableAutoHide(xcb_connection_t *connection, xcb_window_t windowId, ElectricBorder border)
|
|
{
|
|
if (border == ElectricNone) {
|
|
xcb_delete_property(connection, windowId, atoms->kde_screen_edge_show);
|
|
} else {
|
|
uint32_t value = 0;
|
|
|
|
switch (border) {
|
|
case ElectricTop:
|
|
value = 0;
|
|
break;
|
|
case ElectricRight:
|
|
value = 1;
|
|
break;
|
|
case ElectricBottom:
|
|
value = 2;
|
|
break;
|
|
case ElectricLeft:
|
|
value = 3;
|
|
break;
|
|
default:
|
|
Q_UNREACHABLE();
|
|
}
|
|
|
|
xcb_change_property(connection, XCB_PROP_MODE_REPLACE, windowId, atoms->kde_screen_edge_show, XCB_ATOM_CARDINAL, 32, 1, &value);
|
|
}
|
|
}
|
|
|
|
class ScreenEdgePropertyMonitor : public QObject
|
|
{
|
|
Q_OBJECT
|
|
public:
|
|
ScreenEdgePropertyMonitor(xcb_connection_t *c, xcb_window_t window)
|
|
: QObject()
|
|
, m_connection(c)
|
|
, m_window(window)
|
|
, m_notifier(new QSocketNotifier(xcb_get_file_descriptor(m_connection), QSocketNotifier::Read, this))
|
|
{
|
|
connect(m_notifier, &QSocketNotifier::activated, this, &ScreenEdgePropertyMonitor::processXcbEvents);
|
|
connect(QCoreApplication::eventDispatcher(), &QAbstractEventDispatcher::aboutToBlock, this, &ScreenEdgePropertyMonitor::processXcbEvents);
|
|
connect(QCoreApplication::eventDispatcher(), &QAbstractEventDispatcher::awake, this, &ScreenEdgePropertyMonitor::processXcbEvents);
|
|
}
|
|
|
|
Q_SIGNALS:
|
|
void withdrawn();
|
|
|
|
private:
|
|
void processXcbEvents()
|
|
{
|
|
while (auto event = xcb_poll_for_event(m_connection)) {
|
|
const uint8_t eventType = event->response_type & ~0x80;
|
|
switch (eventType) {
|
|
case XCB_PROPERTY_NOTIFY: {
|
|
auto propertyNotifyEvent = reinterpret_cast<xcb_property_notify_event_t *>(event);
|
|
if (propertyNotifyEvent->window == m_window && propertyNotifyEvent->atom == atoms->kde_screen_edge_show && propertyNotifyEvent->state == XCB_PROPERTY_DELETE) {
|
|
Q_EMIT withdrawn();
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
free(event);
|
|
}
|
|
}
|
|
|
|
xcb_connection_t *m_connection;
|
|
xcb_window_t m_window;
|
|
QSocketNotifier *m_notifier;
|
|
};
|
|
|
|
void ScreenEdgesTest::testKdeNetWmScreenEdgeShow()
|
|
{
|
|
// This test verifies that _KDE_NET_WM_SCREEN_EDGE_SHOW is handled properly. Note that
|
|
// _KDE_NET_WM_SCREEN_EDGE_SHOW has oneshot effect. It's deleted when the window is shown.
|
|
|
|
auto config = kwinApp()->config();
|
|
config->group(QStringLiteral("Windows")).writeEntry("ElectricBorderDelay", 75);
|
|
config->sync();
|
|
workspace()->slotReconfigure();
|
|
|
|
Test::XcbConnectionPtr c = Test::createX11Connection();
|
|
QVERIFY(!xcb_connection_has_error(c.get()));
|
|
|
|
// Create a test window at the bottom of the screen.
|
|
const QRect windowGeometry(0, 1024 - 30, 1280, 30);
|
|
const uint32_t values[] = {XCB_EVENT_MASK_PROPERTY_CHANGE};
|
|
xcb_window_t windowId = xcb_generate_id(c.get());
|
|
xcb_create_window(c.get(), XCB_COPY_FROM_PARENT, windowId, rootWindow(),
|
|
windowGeometry.x(),
|
|
windowGeometry.y(),
|
|
windowGeometry.width(),
|
|
windowGeometry.height(),
|
|
0, XCB_WINDOW_CLASS_INPUT_OUTPUT,
|
|
XCB_COPY_FROM_PARENT,
|
|
XCB_CW_EVENT_MASK, values);
|
|
xcb_size_hints_t hints;
|
|
memset(&hints, 0, sizeof(hints));
|
|
xcb_icccm_size_hints_set_position(&hints, 1, windowGeometry.x(), windowGeometry.y());
|
|
xcb_icccm_size_hints_set_size(&hints, 1, windowGeometry.width(), windowGeometry.height());
|
|
xcb_icccm_set_wm_normal_hints(c.get(), windowId, &hints);
|
|
xcb_change_property(c.get(), XCB_PROP_MODE_REPLACE, windowId, atoms->wm_client_leader, XCB_ATOM_WINDOW, 32, 1, &windowId);
|
|
xcb_map_window(c.get(), windowId);
|
|
xcb_flush(c.get());
|
|
|
|
QSignalSpy windowCreatedSpy(workspace(), &Workspace::windowAdded);
|
|
QVERIFY(windowCreatedSpy.wait());
|
|
Window *window = windowCreatedSpy.first().first().value<Window *>();
|
|
QVERIFY(window);
|
|
|
|
ScreenEdgePropertyMonitor screenEdgeMonitor(c.get(), windowId);
|
|
QSignalSpy withdrawnSpy(&screenEdgeMonitor, &ScreenEdgePropertyMonitor::withdrawn);
|
|
QSignalSpy windowShownSpy(window, &Window::windowShown);
|
|
QSignalSpy windowHiddenSpy(window, &Window::windowHidden);
|
|
quint32 timestamp = 0;
|
|
|
|
// The window will be shown when the pointer approaches its reserved screen edge.
|
|
{
|
|
enableAutoHide(c.get(), windowId, ElectricBottom);
|
|
xcb_flush(c.get());
|
|
QVERIFY(windowHiddenSpy.wait());
|
|
QVERIFY(!window->isShown());
|
|
|
|
Test::pointerMotion(QPointF(640, 1023), timestamp);
|
|
timestamp += 160;
|
|
Test::pointerMotion(QPointF(640, 1023), timestamp);
|
|
QVERIFY(withdrawnSpy.wait());
|
|
QVERIFY(window->isShown());
|
|
timestamp += 160;
|
|
Test::pointerMotion(QPointF(640, 512), timestamp);
|
|
QVERIFY(window->isShown());
|
|
}
|
|
|
|
// The window will be shown when swiping on the touch screen.
|
|
{
|
|
enableAutoHide(c.get(), windowId, ElectricBottom);
|
|
xcb_flush(c.get());
|
|
QVERIFY(windowHiddenSpy.wait());
|
|
QVERIFY(!window->isShown());
|
|
|
|
Test::touchDown(0, QPointF(640, 1023), timestamp++);
|
|
Test::touchMotion(0, QPointF(640, 512), timestamp++);
|
|
Test::touchUp(0, timestamp++);
|
|
QVERIFY(withdrawnSpy.wait());
|
|
QVERIFY(window->isShown());
|
|
}
|
|
|
|
// If the screen edge is destroyed (can happen when the screen layout changes), the window will be shown.
|
|
{
|
|
enableAutoHide(c.get(), windowId, ElectricBottom);
|
|
xcb_flush(c.get());
|
|
QVERIFY(windowHiddenSpy.wait());
|
|
QVERIFY(!window->isShown());
|
|
|
|
workspace()->screenEdges()->recreateEdges();
|
|
QVERIFY(withdrawnSpy.wait());
|
|
QVERIFY(window->isShown());
|
|
}
|
|
|
|
// The window will be shown and hidden in response to changing _KDE_NET_WM_SCREEN_EDGE_SHOW.
|
|
{
|
|
enableAutoHide(c.get(), windowId, ElectricBottom);
|
|
xcb_flush(c.get());
|
|
QVERIFY(windowHiddenSpy.wait());
|
|
QVERIFY(!window->isShown());
|
|
|
|
enableAutoHide(c.get(), windowId, ElectricNone);
|
|
xcb_flush(c.get());
|
|
QVERIFY(windowShownSpy.wait());
|
|
QVERIFY(window->isShown());
|
|
}
|
|
|
|
// The approaching state will be reset if the window is shown manually.
|
|
{
|
|
QSignalSpy approachingSpy(workspace()->screenEdges(), &ScreenEdges::approaching);
|
|
enableAutoHide(c.get(), windowId, ElectricBottom);
|
|
xcb_flush(c.get());
|
|
QVERIFY(windowHiddenSpy.wait());
|
|
QVERIFY(!window->isShown());
|
|
|
|
Test::pointerMotion(QPointF(640, 1020), timestamp++);
|
|
QVERIFY(approachingSpy.last().at(1).toReal() == 0.0);
|
|
Test::pointerMotion(QPointF(640, 1021), timestamp++);
|
|
QVERIFY(approachingSpy.last().at(1).toReal() != 0.0);
|
|
|
|
enableAutoHide(c.get(), windowId, ElectricNone);
|
|
xcb_flush(c.get());
|
|
QVERIFY(windowShownSpy.wait());
|
|
QVERIFY(window->isShown());
|
|
QVERIFY(approachingSpy.last().at(1).toReal() == 0.0);
|
|
|
|
Test::pointerMotion(QPointF(640, 512), timestamp++);
|
|
}
|
|
}
|
|
|
|
} // namespace KWin
|
|
|
|
WAYLANDTEST_MAIN(KWin::ScreenEdgesTest)
|
|
#include "screenedges_test.moc"
|