blob: 2e17bef8727dbe36a06d15466669ab7e0fcbf70b [file] [log] [blame]
/****************************************************************************
**
** Copyright (C) 2018 The Qt Company Ltd.
** Contact: https://www.qt.io/licensing/
**
** This file is part of the test suite of the Qt Toolkit.
**
** $QT_BEGIN_LICENSE:GPL-EXCEPT$
** Commercial License Usage
** Licensees holding valid commercial Qt licenses may use this file in
** accordance with the commercial license agreement provided with the
** Software or, alternatively, in accordance with the terms contained in
** a written agreement between you and The Qt Company. For licensing terms
** and conditions see https://www.qt.io/terms-conditions. For further
** information use the contact form at https://www.qt.io/contact-us.
**
** GNU General Public License Usage
** Alternatively, this file may be used under the terms of the GNU
** General Public License version 3 as published by the Free Software
** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
** included in the packaging of this file. Please review the following
** information to ensure the GNU General Public License requirements will
** be met: https://www.gnu.org/licenses/gpl-3.0.html.
**
** $QT_END_LICENSE$
**
****************************************************************************/
#include "mockcompositor.h"
#include <QtGui/QRasterWindow>
#include <QtGui/QOpenGLWindow>
#if QT_CONFIG(cursor)
#include <wayland-cursor.h>
#include <QtGui/private/qguiapplication_p.h>
#include <QtWaylandClient/private/qwaylanddisplay_p.h>
#include <QtWaylandClient/private/qwaylandintegration_p.h>
#endif
using namespace MockCompositor;
// wl_seat version 5 was introduced in wayland 1.10, and although that's pretty old,
// there are still compositors that have yet to update their implementation to support
// the new version (most importantly our own QtWaylandCompositor).
// As long as that's the case, this test makes sure input events still works on version 4.
class SeatV4Compositor : public DefaultCompositor {
public:
explicit SeatV4Compositor()
{
exec([this] {
m_config.autoConfigure = true;
removeAll<Seat>();
uint capabilities = Seat::capability_pointer | Seat::capability_keyboard;
int version = 4;
add<Seat>(capabilities, version);
});
}
};
class tst_seatv4 : public QObject, private SeatV4Compositor
{
Q_OBJECT
private slots:
void cleanup();
void bindsToSeat();
void keyboardKeyPress();
#if QT_CONFIG(cursor)
void createsPointer();
void setsCursorOnEnter();
void usesEnterSerial();
void focusDestruction();
void mousePress();
void mousePressFloat();
void simpleAxis_data();
void simpleAxis();
void invalidPointerEvents();
void scaledCursor();
void unscaledFallbackCursor();
void bitmapCursor();
void hidpiBitmapCursor();
void hidpiBitmapCursorNonInt();
void animatedCursor();
#endif
};
void tst_seatv4::cleanup()
{
QTRY_VERIFY2(isClean(), qPrintable(dirtyMessage()));
QCOMPOSITOR_COMPARE(getAll<Output>().size(), 1); // No extra outputs left
}
void tst_seatv4::bindsToSeat()
{
QCOMPOSITOR_COMPARE(get<Seat>()->resourceMap().size(), 1);
QCOMPOSITOR_COMPARE(get<Seat>()->resourceMap().first()->version(), 4);
}
void tst_seatv4::keyboardKeyPress()
{
class Window : public QRasterWindow {
public:
void keyPressEvent(QKeyEvent *) override { m_pressed = true; }
bool m_pressed = false;
};
Window window;
window.resize(64, 64);
window.show();
QCOMPOSITOR_TRY_VERIFY(xdgSurface() && xdgSurface()->m_committedConfigureSerial);
uint keyCode = 80; // arbitrarily chosen
exec([&] {
auto *surface = xdgSurface()->m_surface;
keyboard()->sendEnter(surface);
keyboard()->sendKey(client(), keyCode, Keyboard::key_state_pressed);
keyboard()->sendKey(client(), keyCode, Keyboard::key_state_released);
});
QTRY_VERIFY(window.m_pressed);
}
#if QT_CONFIG(cursor)
void tst_seatv4::createsPointer()
{
QCOMPOSITOR_TRY_COMPARE(pointer()->resourceMap().size(), 1);
QCOMPOSITOR_TRY_COMPARE(pointer()->resourceMap().first()->version(), 4);
}
void tst_seatv4::setsCursorOnEnter()
{
QRasterWindow window;
window.resize(64, 64);
window.show();
QCOMPOSITOR_TRY_VERIFY(xdgSurface() && xdgSurface()->m_committedConfigureSerial);
exec([=] { pointer()->sendEnter(xdgSurface()->m_surface, {32, 32}); });
QCOMPOSITOR_TRY_VERIFY(cursorSurface());
}
void tst_seatv4::usesEnterSerial()
{
QSignalSpy setCursorSpy(exec([=] { return pointer(); }), &Pointer::setCursor);
QRasterWindow window;
window.resize(64, 64);
window.show();
QCOMPOSITOR_TRY_VERIFY(xdgSurface() && xdgSurface()->m_committedConfigureSerial);
uint enterSerial = exec([=] {
return pointer()->sendEnter(xdgSurface()->m_surface, {32, 32});
});
QCOMPOSITOR_TRY_VERIFY(cursorSurface());
QTRY_COMPARE(setCursorSpy.count(), 1);
QCOMPARE(setCursorSpy.takeFirst().at(0).toUInt(), enterSerial);
}
void tst_seatv4::focusDestruction()
{
QSignalSpy setCursorSpy(exec([=] { return pointer(); }), &Pointer::setCursor);
QRasterWindow window;
window.resize(64, 64);
window.show();
QCOMPOSITOR_TRY_VERIFY(xdgSurface() && xdgSurface()->m_committedConfigureSerial);
// Setting a cursor now is not allowed since there has been no enter event
QCOMPARE(setCursorSpy.count(), 0);
uint enterSerial = exec([=] {
return pointer()->sendEnter(xdgSurface()->m_surface, {32, 32});
});
QCOMPOSITOR_TRY_VERIFY(cursorSurface());
QTRY_COMPARE(setCursorSpy.count(), 1);
QCOMPARE(setCursorSpy.takeFirst().at(0).toUInt(), enterSerial);
// Destroy the focus
window.close();
QRasterWindow window2;
window2.resize(64, 64);
window2.show();
window2.setCursor(Qt::WaitCursor);
QCOMPOSITOR_TRY_VERIFY(xdgSurface() && xdgSurface()->m_committedConfigureSerial);
// Setting a cursor now is not allowed since there has been no enter event
xdgPingAndWaitForPong();
QCOMPARE(setCursorSpy.count(), 0);
}
void tst_seatv4::mousePress()
{
class Window : public QRasterWindow {
public:
void mousePressEvent(QMouseEvent *) override { m_pressed = true; }
bool m_pressed = false;
};
Window window;
window.resize(64, 64);
window.show();
QCOMPOSITOR_TRY_VERIFY(xdgSurface() && xdgSurface()->m_committedConfigureSerial);
exec([&] {
auto *surface = xdgSurface()->m_surface;
pointer()->sendEnter(surface, {32, 32});
pointer()->sendButton(client(), BTN_LEFT, 1);
pointer()->sendButton(client(), BTN_LEFT, 0);
});
QTRY_VERIFY(window.m_pressed);
}
void tst_seatv4::mousePressFloat()
{
class Window : public QRasterWindow {
public:
void mousePressEvent(QMouseEvent *e) override { m_position = e->localPos(); }
QPointF m_position;
};
Window window;
window.resize(64, 64);
window.show();
QCOMPOSITOR_TRY_VERIFY(xdgSurface() && xdgSurface()->m_committedConfigureSerial);
exec([&] {
auto *surface = xdgSurface()->m_surface;
pointer()->sendEnter(surface, {32.75, 32.25});
pointer()->sendButton(client(), BTN_LEFT, 1);
pointer()->sendButton(client(), BTN_LEFT, 0);
});
QMargins m = window.frameMargins();
QPointF pressedPosition(32.75 -m.left(), 32.25 - m.top());
QTRY_COMPARE(window.m_position, pressedPosition);
}
void tst_seatv4::simpleAxis_data()
{
QTest::addColumn<uint>("axis");
QTest::addColumn<qreal>("value");
QTest::addColumn<QPoint>("angleDelta");
// Directions in regular windows/linux terms (no "natural" scrolling)
QTest::newRow("down") << uint(Pointer::axis_vertical_scroll) << 1.0 << QPoint{0, -12};
QTest::newRow("up") << uint(Pointer::axis_vertical_scroll) << -1.0 << QPoint{0, 12};
QTest::newRow("left") << uint(Pointer::axis_horizontal_scroll) << 1.0 << QPoint{-12, 0};
QTest::newRow("right") << uint(Pointer::axis_horizontal_scroll) << -1.0 << QPoint{12, 0};
QTest::newRow("up big") << uint(Pointer::axis_vertical_scroll) << -10.0 << QPoint{0, 120};
}
void tst_seatv4::simpleAxis()
{
QFETCH(uint, axis);
QFETCH(qreal, value);
QFETCH(QPoint, angleDelta);
class WheelWindow : QRasterWindow {
public:
explicit WheelWindow()
{
resize(64, 64);
show();
}
void wheelEvent(QWheelEvent *event) override
{
QRasterWindow::wheelEvent(event);
// Angle delta should always be provided (says docs)
QVERIFY(!event->angleDelta().isNull());
// There are now scroll phases on Wayland prior to v5
QCOMPARE(event->phase(), Qt::NoScrollPhase);
// Pixel delta should only be set if we know it's a high-res input device (which we don't)
QCOMPARE(event->pixelDelta(), QPoint(0, 0));
// The axis vector of the event is already in surface space, so there is now way to tell
// whether it is inverted or not.
QCOMPARE(event->inverted(), false);
// We didn't press any buttons
QCOMPARE(event->buttons(), Qt::NoButton);
// There has been no information about what created the event.
// Documentation says not synthesized is appropriate in such cases
QCOMPARE(event->source(), Qt::MouseEventNotSynthesized);
m_events.append(Event{event->pixelDelta(), event->angleDelta()});
}
struct Event // Because I didn't find a convenient way to copy it entirely
{
Event() = default;
const QPoint pixelDelta;
const QPoint angleDelta; // eights of a degree, positive is upwards, left
};
QVector<Event> m_events;
};
WheelWindow window;
QCOMPOSITOR_TRY_VERIFY(xdgSurface() && xdgSurface()->m_committedConfigureSerial);
exec([=] {
Surface *surface = xdgSurface()->m_surface;
pointer()->sendEnter(surface, {32, 32});
wl_client *client = surface->resource()->client();
// Length of vector in surface-local space. i.e. positive is downwards
pointer()->sendAxis(
client,
Pointer::axis(axis),
value // Length of vector in surface-local space. i.e. positive is downwards
);
});
QTRY_COMPARE(window.m_events.size(), 1);
auto event = window.m_events.takeFirst();
QCOMPARE(event.angleDelta, angleDelta);
}
void tst_seatv4::invalidPointerEvents()
{
QRasterWindow window;
window.resize(64, 64);
window.show();
QCOMPOSITOR_TRY_VERIFY(xdgSurface() && xdgSurface()->m_committedConfigureSerial);
exec([=] {
auto *p = pointer();
auto *c = client();
// Purposefully send events without a wl_pointer.enter
p->sendMotion(c, {32, 32});
p->sendButton(c, BTN_LEFT, Pointer::button_state_pressed);
p->sendAxis(c, Pointer::axis_vertical_scroll, 1.0);
});
// Make sure we get here without crashing
xdgPingAndWaitForPong();
}
static bool supportsCursorSize(uint size, wl_shm *shm)
{
auto *theme = wl_cursor_theme_load(qgetenv("XCURSOR_THEME"), size, shm);
if (!theme)
return false;
constexpr std::array<const char *, 4> names{"left_ptr", "default", "left_arrow", "top_left_arrow"};
for (const char *name : names) {
if (auto *cursor = wl_cursor_theme_get_cursor(theme, name)) {
auto *image = cursor->images[0];
return image->width == image->height && image->width == size;
}
}
return false;
}
static bool supportsCursorSizes(const QVector<uint> &sizes)
{
auto *waylandIntegration = static_cast<QtWaylandClient::QWaylandIntegration *>(QGuiApplicationPrivate::platformIntegration());
wl_shm *shm = waylandIntegration->display()->shm()->object();
return std::all_of(sizes.begin(), sizes.end(), [=](uint size) {
return supportsCursorSize(size, shm);
});
}
static uint defaultCursorSize() {
const int xCursorSize = qEnvironmentVariableIntValue("XCURSOR_SIZE");
return xCursorSize > 0 ? uint(xCursorSize) : 32;
}
void tst_seatv4::scaledCursor()
{
const uint defaultSize = defaultCursorSize();
if (!supportsCursorSizes({defaultSize, defaultSize * 2}))
QSKIP("Cursor themes with default size and 2x default size not found.");
// Add a highdpi output
exec([&] {
OutputData d;
d.scale = 2;
d.position = {1920, 0};
add<Output>(d);
});
QRasterWindow window;
window.resize(64, 64);
window.show();
QCOMPOSITOR_TRY_VERIFY(xdgSurface() && xdgSurface()->m_committedConfigureSerial);
exec([=] { pointer()->sendEnter(xdgSurface()->m_surface, {32, 32}); });
QCOMPOSITOR_TRY_VERIFY(cursorSurface());
QCOMPOSITOR_TRY_VERIFY(cursorSurface()->m_committed.buffer);
QCOMPOSITOR_TRY_COMPARE(cursorSurface()->m_committed.bufferScale, 1);
QSize unscaledPixelSize = exec([=] {
return cursorSurface()->m_committed.buffer->size();
});
exec([=] {
auto *surface = cursorSurface();
surface->sendEnter(getAll<Output>()[1]);
surface->sendLeave(getAll<Output>()[0]);
});
QCOMPOSITOR_TRY_COMPARE(cursorSurface()->m_committed.buffer->size(), unscaledPixelSize * 2);
// Remove the extra output to clean up for the next test
exec([&] { remove(output(1)); });
}
void tst_seatv4::unscaledFallbackCursor()
{
const uint defaultSize = defaultCursorSize();
if (!supportsCursorSizes({defaultSize}))
QSKIP("Default cursor size not supported");
const int screens = 4; // with scales 1, 2, 4, 8
exec([=] {
for (int i = 1; i < screens; ++i) {
OutputData d;
d.scale = int(qPow(2, i));
d.position = {1920 * i, 0};
add<Output>(d);
}
});
QRasterWindow window;
window.resize(64, 64);
window.show();
QCOMPOSITOR_TRY_VERIFY(xdgSurface() && xdgSurface()->m_committedConfigureSerial);
exec([=] { pointer()->sendEnter(xdgSurface()->m_surface, {32, 32}); });
QCOMPOSITOR_TRY_VERIFY(cursorSurface());
QCOMPOSITOR_TRY_VERIFY(cursorSurface()->m_committed.buffer);
QCOMPOSITOR_TRY_COMPARE(cursorSurface()->m_committed.bufferScale, 1);
QSize unscaledPixelSize = exec([=] {
return cursorSurface()->m_committed.buffer->size();
});
QCOMPARE(unscaledPixelSize.width(), int(defaultSize));
QCOMPARE(unscaledPixelSize.height(), int(defaultSize));
for (int i = 1; i < screens; ++i) {
exec([=] {
auto *surface = cursorSurface();
surface->sendEnter(getAll<Output>()[i]);
surface->sendLeave(getAll<Output>()[i-1]);
});
xdgPingAndWaitForPong(); // Give the client a chance to mess up
// Surface size (buffer size / scale) should stay constant
QCOMPOSITOR_TRY_COMPARE(cursorSurface()->m_committed.buffer->size() / cursorSurface()->m_committed.bufferScale, unscaledPixelSize);
}
// Remove the extra outputs to clean up for the next test
exec([&] { while (auto *o = output(1)) remove(o); });
}
void tst_seatv4::bitmapCursor()
{
// Add a highdpi output
exec([&] {
OutputData d;
d.scale = 2;
d.position = {1920, 0};
add<Output>(d);
});
QRasterWindow window;
window.resize(64, 64);
QPixmap pixmap(24, 24);
pixmap.setDevicePixelRatio(1);
QPoint hotspot(12, 12); // In device pixel coordinates
QCursor cursor(pixmap, hotspot.x(), hotspot.y());
window.setCursor(cursor);
window.show();
QCOMPOSITOR_TRY_VERIFY(xdgSurface() && xdgSurface()->m_committedConfigureSerial);
exec([=] { pointer()->sendEnter(xdgSurface()->m_surface, {32, 32}); });
QCOMPOSITOR_TRY_VERIFY(cursorSurface());
QCOMPOSITOR_TRY_VERIFY(cursorSurface()->m_committed.buffer);
QCOMPOSITOR_COMPARE(cursorSurface()->m_committed.buffer->size(), QSize(24, 24));
QCOMPOSITOR_COMPARE(cursorSurface()->m_committed.bufferScale, 1);
QCOMPOSITOR_COMPARE(pointer()->m_hotspot, QPoint(12, 12));
exec([=] {
auto *surface = cursorSurface();
surface->sendEnter(getAll<Output>()[1]);
surface->sendLeave(getAll<Output>()[0]);
});
xdgPingAndWaitForPong();
// Everything should remain the same, the cursor still has dpr 1
QCOMPOSITOR_COMPARE(cursorSurface()->m_committed.bufferScale, 1);
QCOMPOSITOR_COMPARE(cursorSurface()->m_committed.buffer->size(), QSize(24, 24));
QCOMPOSITOR_COMPARE(pointer()->m_hotspot, QPoint(12, 12));
// Remove the extra output to clean up for the next test
exec([&] { remove(getAll<Output>()[1]); });
}
void tst_seatv4::hidpiBitmapCursor()
{
// Add a highdpi output
exec([&] {
OutputData d;
d.scale = 2;
d.position = {1920, 0};
add<Output>(d);
});
QRasterWindow window;
window.resize(64, 64);
QPixmap pixmap(48, 48);
pixmap.setDevicePixelRatio(2);
QPoint hotspot(12, 12); // In device pixel coordinates
QCursor cursor(pixmap, hotspot.x(), hotspot.y());
window.setCursor(cursor);
window.show();
QCOMPOSITOR_TRY_VERIFY(xdgSurface() && xdgSurface()->m_committedConfigureSerial);
exec([=] { pointer()->sendEnter(xdgSurface()->m_surface, {32, 32}); });
QCOMPOSITOR_TRY_VERIFY(cursorSurface());
QCOMPOSITOR_TRY_VERIFY(cursorSurface()->m_committed.buffer);
QCOMPOSITOR_COMPARE(cursorSurface()->m_committed.buffer->size(), QSize(48, 48));
QCOMPOSITOR_COMPARE(cursorSurface()->m_committed.bufferScale, 2);
QCOMPOSITOR_COMPARE(pointer()->m_hotspot, QPoint(12, 12));
exec([=] {
auto *surface = cursorSurface();
surface->sendEnter(getAll<Output>()[1]);
surface->sendLeave(getAll<Output>()[0]);
});
xdgPingAndWaitForPong();
QCOMPOSITOR_COMPARE(cursorSurface()->m_committed.bufferScale, 2);
QCOMPOSITOR_COMPARE(cursorSurface()->m_committed.buffer->size(), QSize(48, 48));
QCOMPOSITOR_COMPARE(pointer()->m_hotspot, QPoint(12, 12));
// Remove the extra output to clean up for the next test
exec([&] { remove(getAll<Output>()[1]); });
}
void tst_seatv4::hidpiBitmapCursorNonInt()
{
QRasterWindow window;
window.resize(64, 64);
QPixmap pixmap(100, 100);
pixmap.setDevicePixelRatio(2.5); // dpr width is now 100 / 2.5 = 40
QPoint hotspot(20, 20); // In device pixel coordinates (middle of buffer)
QCursor cursor(pixmap, hotspot.x(), hotspot.y());
window.setCursor(cursor);
window.show();
QCOMPOSITOR_TRY_VERIFY(xdgSurface() && xdgSurface()->m_committedConfigureSerial);
exec([=] { pointer()->sendEnter(xdgSurface()->m_surface, {32, 32}); });
QCOMPOSITOR_TRY_VERIFY(cursorSurface());
QCOMPOSITOR_TRY_VERIFY(cursorSurface()->m_committed.buffer);
QCOMPOSITOR_COMPARE(cursorSurface()->m_committed.buffer->size(), QSize(100, 100));
QCOMPOSITOR_COMPARE(cursorSurface()->m_committed.bufferScale, 2);
// Verify that the hotspot was scaled correctly
// Surface size is now 100 / 2 = 50, so the middle should be at 25 in surface coordinates
QCOMPOSITOR_COMPARE(pointer()->m_hotspot, QPoint(25, 25));
}
void tst_seatv4::animatedCursor()
{
QRasterWindow window;
window.resize(64, 64);
window.setCursor(Qt::WaitCursor); // TODO: verify that the theme has an animated wait cursor or skip test
window.show();
QCOMPOSITOR_TRY_VERIFY(xdgSurface() && xdgSurface()->m_committedConfigureSerial);
exec([=] { pointer()->sendEnter(xdgSurface()->m_surface, {32, 32}); });
QCOMPOSITOR_TRY_VERIFY(cursorSurface());
// We should get the first buffer without waiting for a frame callback
QCOMPOSITOR_TRY_VERIFY(cursorSurface()->m_committed.buffer);
QSignalSpy bufferSpy(exec([=] { return cursorSurface(); }), &Surface::bufferCommitted);
exec([&] {
// Make sure no extra buffers have arrived
QVERIFY(bufferSpy.empty());
// The client should send a frame request in order to time animations correctly
QVERIFY(!cursorSurface()->m_waitingFrameCallbacks.empty());
// Tell the client it's time to animate
cursorSurface()->sendFrameCallbacks();
});
// Verify that we get a new cursor buffer
QTRY_COMPARE(bufferSpy.count(), 1);
}
#endif // QT_CONFIG(cursor)
QCOMPOSITOR_TEST_MAIN(tst_seatv4)
#include "tst_seatv4.moc"