From d023da86399b99d365f1e745fb0d9424afc8194b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Morten=20Johan=20S=C3=B8rvig?= Date: Tue, 4 Feb 2020 17:47:23 +0100 Subject: Add high-dpi auto test MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Use the new screen config feature of the offscreen platform plugin to run tests on virtual screen setup. This has the benefit that we can auto-test the QHighDpiScaling implementation (as well as its usage in QtGui) on any platform with a fixed mock screen setup which does not rely on physical screen configuration. Test the following configurations: - three screens: 96 DPI. (reference) - three screens: 192 DPI - three screens: mixed (high) DPI Change-Id: I2fac889d896cf30ab2a79c306cee22177ad8f4ac Reviewed-by: Morten Johan Sørvig --- tests/auto/gui/kernel/CMakeLists.txt | 1 + tests/auto/gui/kernel/kernel.pro | 1 + tests/auto/gui/kernel/qhighdpi/CMakeLists.txt | 14 + tests/auto/gui/kernel/qhighdpi/qhighdpi.pro | 6 + tests/auto/gui/kernel/qhighdpi/tst_qhighdpi.cpp | 433 ++++++++++++++++++++++++ 5 files changed, 455 insertions(+) create mode 100644 tests/auto/gui/kernel/qhighdpi/CMakeLists.txt create mode 100644 tests/auto/gui/kernel/qhighdpi/qhighdpi.pro create mode 100644 tests/auto/gui/kernel/qhighdpi/tst_qhighdpi.cpp diff --git a/tests/auto/gui/kernel/CMakeLists.txt b/tests/auto/gui/kernel/CMakeLists.txt index 690a679833..2753007a95 100644 --- a/tests/auto/gui/kernel/CMakeLists.txt +++ b/tests/auto/gui/kernel/CMakeLists.txt @@ -36,6 +36,7 @@ if(QT_FEATURE_shortcut) add_subdirectory(qshortcut) endif() if(QT_FEATURE_highdpiscaling) + add_subdirectory(qhighdpi) add_subdirectory(qhighdpiscaling) endif() if(TARGET Qt::Widgets) diff --git a/tests/auto/gui/kernel/kernel.pro b/tests/auto/gui/kernel/kernel.pro index c11c0ad26b..346c7fb3c4 100644 --- a/tests/auto/gui/kernel/kernel.pro +++ b/tests/auto/gui/kernel/kernel.pro @@ -13,6 +13,7 @@ SUBDIRS=\ qguimetatype \ qguitimer \ qguivariant \ + qhighdpi\ qhighdpiscaling \ qinputdevice \ qinputmethod \ diff --git a/tests/auto/gui/kernel/qhighdpi/CMakeLists.txt b/tests/auto/gui/kernel/qhighdpi/CMakeLists.txt new file mode 100644 index 0000000000..bdd9a5e17f --- /dev/null +++ b/tests/auto/gui/kernel/qhighdpi/CMakeLists.txt @@ -0,0 +1,14 @@ +# Generated from qhighdpi.pro. + +##################################################################### +## tst_qhighdpi Test: +##################################################################### + +qt_internal_add_test(tst_qhighdpi + SOURCES + tst_qhighdpi.cpp + PUBLIC_LIBRARIES + Qt::CorePrivate + Qt::Gui + Qt::GuiPrivate +) diff --git a/tests/auto/gui/kernel/qhighdpi/qhighdpi.pro b/tests/auto/gui/kernel/qhighdpi/qhighdpi.pro new file mode 100644 index 0000000000..41e73848f3 --- /dev/null +++ b/tests/auto/gui/kernel/qhighdpi/qhighdpi.pro @@ -0,0 +1,6 @@ +CONFIG += testcase +TARGET = tst_qhighdpi + +QT += core-private gui-private testlib + +SOURCES += tst_qhighdpi.cpp diff --git a/tests/auto/gui/kernel/qhighdpi/tst_qhighdpi.cpp b/tests/auto/gui/kernel/qhighdpi/tst_qhighdpi.cpp new file mode 100644 index 0000000000..a7b18167ba --- /dev/null +++ b/tests/auto/gui/kernel/qhighdpi/tst_qhighdpi.cpp @@ -0,0 +1,433 @@ +/**************************************************************************** +** +** Copyright (C) 2020 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 +#include + +#include + +class tst_QHighDpi: public QObject +{ + Q_OBJECT +private: // helpers + QJsonArray createStandardScreens(const QList &dpiValues); + QGuiApplication *createOffscreenApplication(const QByteArray &jsonConfig); + QGuiApplication *createStandardOffscreenApp(const QList &dpiValues); + static void standardScreenDpiTestData(); +private slots: + void initTestCase(); + void qhighdpiscaling_data(); + void qhighdpiscaling(); + void screenDpiAndDpr_data(); + void screenDpiAndDpr(); + void screenAt_data(); + void screenAt(); + void screenGeometry_data(); + void screenGeometry(); + void windowGeometry_data(); + void windowGeometry(); + void spanningWindows_data(); + void spanningWindows(); + void mouseEvents_data(); + void mouseEvents(); +}; + +/// Offscreen platform plugin test setup +const int standardScreenWidth = 640; +const int standardScreenHeight = 480; +const int standardBaseDpi = 96; +const int standardScreenCount = 3; + +QJsonArray tst_QHighDpi::createStandardScreens(const QList &dpiValues) +{ + Q_ASSERT(dpiValues.count() == standardScreenCount); + + // Create row of three screens: screen#0 screen#1 screen#2 + return QJsonArray { + QJsonObject { + {"name", "screen#0"}, + {"x", -standardScreenWidth}, + {"y", -10}, + {"width", standardScreenWidth}, + {"height", standardScreenHeight}, + {"logicalDpi", dpiValues[0]}, + {"logicalBaseDpi", standardBaseDpi}, + {"dpr", 1} + }, + QJsonObject { + {"name", "screen#1"}, + {"x", 0}, + {"y", 0}, + {"width", standardScreenWidth}, + {"height", standardScreenHeight}, + {"logicalDpi", dpiValues[1]}, + {"logicalBaseDpi", standardBaseDpi}, + {"dpr", 1} + }, + QJsonObject { + {"name", "screen#2"}, + {"x", standardScreenWidth}, + {"y", 10}, + {"width", standardScreenWidth}, + {"height", standardScreenHeight}, + {"logicalDpi", dpiValues[2]}, + {"logicalBaseDpi", standardBaseDpi}, + {"dpr", 1} + } + }; +} + +QGuiApplication *tst_QHighDpi::createOffscreenApplication(const QByteArray &jsonConfig) +{ + // Write offscreen platform config file + QFile configFile(QLatin1String("qt-offscreen-test-config.json")); + if (!configFile.open(QIODevice::WriteOnly | QIODevice::Truncate)) + qFatal("Failed to open test config file: %s", qPrintable(configFile.errorString())); + configFile.resize(0); // truncate + if (configFile.write(jsonConfig) == -1) + qFatal("Could not write config file: %s", qPrintable(configFile.errorString())); + configFile.close(); + + // Create QGuiApplication which loads the offscreen platform plugin + // Note that argc and argv need to stay valid for the duration of the app lifetime, + // and may be used at any point. The config file used at app startup only. + static int argc; + argc = 3; + static char *argv[3]; + static QByteArray binaryNameArg = QByteArray("tst_qguiapplication"); + argv[0] = binaryNameArg.data(); + static QByteArray platformArg = QByteArray("-platform"); + argv[1] = platformArg.data(); + static QByteArray offscreenAndFileArg; + offscreenAndFileArg = QByteArray("offscreen:configfile=") + configFile.fileName().toUtf8(); + argv[2] = offscreenAndFileArg.data(); + + QGuiApplication *app = new QGuiApplication(argc, argv); + configFile.remove(); // config file is needed during QGuiApplication construction only. + return app; +} + +QGuiApplication *tst_QHighDpi::createStandardOffscreenApp(const QList &dpiValues) +{ + QJsonArray screens = createStandardScreens(dpiValues); + QJsonObject config { + {"synchronousWindowSystemEvents", true}, + {"windowFrameMargins", false}, + {"screens" , screens}, + }; + return createOffscreenApplication(QJsonDocument(config).toJson()); +} + +/// Auto test begins + +void tst_QHighDpi::standardScreenDpiTestData() +{ + // We run each test under three screen configurations (each with three screens): + QTest::addColumn>("dpiValues"); + QTest::newRow("96") << QList { 96, 96, 96 }; // standard-dpi sanity check + QTest::newRow("192") << QList { 192, 192, 192 }; // 2x high dpi + QTest::newRow("144-168-192") << QList { 144, 168, 192 }; // mixed dpi (1.5x, 1.75x, 2x) +} + +void tst_QHighDpi::initTestCase() +{ +#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) + // Run tests on the (proposed) Qt 6 default configuration + QGuiApplication::setAttribute(Qt::AA_EnableHighDpiScaling); + QGuiApplication::setHighDpiScaleFactorRoundingPolicy(Qt::HighDpiScaleFactorRoundingPolicy::PassThrough); +#endif +} + +void tst_QHighDpi::screenDpiAndDpr_data() +{ + standardScreenDpiTestData(); +} + +void tst_QHighDpi::screenDpiAndDpr() +{ + QFETCH(QList, dpiValues); + + std::unique_ptr app(createStandardOffscreenApp(dpiValues)); + int i = 0; + for (QScreen *screen : app->screens()) { + qreal dpi = dpiValues[i++]; + + // verify that the devicePixelRatio equation holds: DPR = DPI / BaseDPI + QCOMPARE(screen->devicePixelRatio(), dpi / standardBaseDpi); + QCOMPARE(screen->logicalDotsPerInch(), dpi / screen->devicePixelRatio()); + + QWindow window(screen); + QCOMPARE(window.devicePixelRatio(), screen->devicePixelRatio()); + } +} + +void tst_QHighDpi::screenAt_data() +{ + standardScreenDpiTestData(); +} + +void tst_QHighDpi::screenAt() +{ + QFETCH(QList, dpiValues); + std::unique_ptr app(createStandardOffscreenApp(dpiValues)); + + QCOMPARE(app->screens().count(), standardScreenCount); // standard setup + + // Verify that screenAt() returns the correct or no screen for various points, + // for all screens. + int i = 0; + for (QScreen *screen : app->screens()) { + qreal dpi = dpiValues[i++]; + + // veryfy virtualSiblings and that AA_EnableHighDpiScaling is active + QCOMPARE(screen->virtualSiblings().count(), standardScreenCount); + QCOMPARE(screen->geometry().size(), QSize(standardScreenWidth, standardScreenHeight) * (96.0 / dpi)); + + // test points on screen + QCOMPARE(app->screenAt(screen->geometry().center()), screen); + QCOMPARE(app->screenAt(screen->geometry().topLeft()), screen); + QCOMPARE(app->screenAt(screen->geometry().bottomRight()), screen); + + // test points off screen + QCOMPARE(app->screenAt(screen->geometry().center() + QPoint(0, -1000)), nullptr); + QCOMPARE(app->screenAt(screen->geometry().topLeft() + QPoint(0, -1)), nullptr); + QCOMPARE(app->screenAt(screen->geometry().bottomRight() + QPoint(0, +1)), nullptr); + + // check the "gaps" created by Qt::AA_EnableHighDpiScaling: no screen there + if (dpi > 96) { + QCOMPARE(app->screenAt(screen->geometry().topLeft() + QPoint(-1, 0)), nullptr); + QCOMPARE(app->screenAt(screen->geometry().bottomRight() + QPoint(1, 0)), nullptr); + } + } +} + +void tst_QHighDpi::screenGeometry_data() +{ + standardScreenDpiTestData(); +} + +void tst_QHighDpi::screenGeometry() +{ + QFETCH(QList, dpiValues); + std::unique_ptr app(createStandardOffscreenApp(dpiValues)); + + for (QScreen *screen : app->screens()) { + QRect geometry = screen->geometry(); + QPoint onScreen = geometry.topLeft() + QPoint(10, 10); + QPoint onScreenNative = QHighDpi::toNativePixels(onScreen, screen); + QPoint onScreenBack = QHighDpi::fromNativePixels(onScreenNative, screen); + + QCOMPARE(onScreen, onScreenBack); + + QPoint offScreen = geometry.topLeft() - QPoint(10, 10); + QPoint offScreenNative = QHighDpi::toNativePixels(offScreen, screen); + QPoint offScreenBack = QHighDpi::fromNativePixels(offScreenNative, screen); + QCOMPARE(offScreenBack, offScreenBack); + } +} + +void tst_QHighDpi::windowGeometry_data() +{ + standardScreenDpiTestData(); +} + +void tst_QHighDpi::windowGeometry() +{ + QFETCH(QList, dpiValues); + std::unique_ptr app(createStandardOffscreenApp(dpiValues)); + + auto testWindow = [&app](QWindow *window, QScreen *expectedScreen, QPoint expectedPosition, QSize expectedSize) { + + // Is the window correctly sized and on the correct screen? + QCOMPARE(window->size(), expectedSize); + QCOMPARE(window->position(), expectedPosition); + QCOMPARE(window->screen(), expectedScreen); + QCOMPARE(app->screenAt(window->mapToGlobal(QPoint(0, 0))), expectedScreen); + + // Round-trip coordinates local->global->local, which should return the starting + // coordinates, also for coordinates outside the window (and screen) + auto globalRoundtrip = [](QWindow *window, QPoint pos) { + QCOMPARE(window->mapFromGlobal(window->mapToGlobal(pos)), pos); + }; + globalRoundtrip(window, QPoint(10, 10)); // window-interior + globalRoundtrip(window, QPoint(-5, -5)); // outside window, on same screen + globalRoundtrip(window, QPoint(standardScreenWidth *2, standardScreenHeight * 2)); // Outside window, outside all screens + globalRoundtrip(window, QPoint(0, -standardScreenWidth)); // Outside window, on neighbor screen + + // Round-trip float coordinates + auto globalRoundtripF = [](QWindow *window, QPointF pos) { + QCOMPARE(window->mapFromGlobal(window->mapToGlobal(pos)), pos); + }; + + globalRoundtripF(window, QPointF(10, 10)); // window-interior + globalRoundtripF(window, QPointF(10.1, 10.1)); + globalRoundtripF(window, QPointF(10.5, 10.5)); + globalRoundtripF(window, QPointF(10.9, 10.9)); + globalRoundtripF(window, QPointF(-5.5, -5.5)); // outside window, on same screen + globalRoundtripF(window, QPointF(standardScreenWidth * 2.1, standardScreenHeight * 2.1)); // Outside window, outside all screens + globalRoundtripF(window, QPointF(0.5, -standardScreenWidth)); // Outside window, on neighbor screen + }; + + // verify window geometry for top-level and child windows on all screens + for (QScreen *screen : app->screens()) { + QWindow topLevelWindow; + QSize topLevelSize(40, 40); + QPoint topLevelPosition(screen->geometry().center()); + topLevelWindow.resize(topLevelSize); + topLevelWindow.setPosition(topLevelPosition); + topLevelWindow.show(); + testWindow(&topLevelWindow, screen, topLevelPosition, topLevelSize); + + QWindow childWindow(&topLevelWindow); + QSize childSize(20, 20); + QPoint childPosition(10, 10); + childWindow.resize(childSize); + childWindow.setPosition(childPosition); + childWindow.show(); + testWindow(&childWindow, screen, childPosition, childSize); + } +} + +void tst_QHighDpi::spanningWindows_data() +{ + standardScreenDpiTestData(); +} + +void tst_QHighDpi::spanningWindows() +{ + QFETCH(QList, dpiValues); + std::unique_ptr app(createStandardOffscreenApp(dpiValues)); + + QPoint screen0Center = app->screens()[0]->geometry().center(); + int screenWidth = app->screens()[0]->geometry().width(); + + // Create window spanning screen 0 and screen 1 + QWindow window; + QRect windowGeometry = QRect(screen0Center, QSize(screenWidth - 10, 20)); + windowGeometry.adjust(0, 0, -10, 0); // Make sure the center point is on screen 0 + window.setGeometry(windowGeometry); + window.show(); + QCOMPARE(window.geometry(), windowGeometry); + + // Device independent screen space may be non-contiguous, in which case global + // window geometry behaves non-intuitivly when a window spans multiple screens: + // - The main screen for the window is defined by the windowing system + // (usually the screen with most window coverage), and is reflected + // by QWindow::screen() + // - screen coordinate linear math does not work for points on the window + // extending beyond the main screen - these may be on a different screen + // with a non-linear coordinate offset. + // + // Local window geometry works (mostly) as before: + // - QWindow::mapToGlobal() can map any window-local coordinate to the correct + // global coordinate and screen, as long as the coordinate is on the window. + // - QWindow::mapFromGlobal() can map any global coordinate to the correct + // local coordinate, as long as the coordinate is on screen and on the window. + // + // Open issue: + // - Mapping coordinates which are outside of the window is iffy; we might + // fall back to using/assuming the coordinate system for the main screen + // in this case. + QPoint globalTopLeft = window.mapToGlobal(QPoint(0, 0)); + QSize foo = window.geometry().size() - QSize(1, 1); + QPoint globalBottomRight = window.mapToGlobal(QPoint(foo.width(), foo.height())); + + QCOMPARE(app->screenAt(globalTopLeft), app->screens()[0]); + QCOMPARE(app->screenAt(globalBottomRight), app->screens()[1]); +} + +void tst_QHighDpi::mouseEvents_data() +{ + standardScreenDpiTestData(); +} + +void tst_QHighDpi::mouseEvents() +{ + QFETCH(QList, dpiValues); + std::unique_ptr app(createStandardOffscreenApp(dpiValues)); + + class MousePressTestWindow : public QWindow { + public: + QPoint m_mouseTestPoint; + + MousePressTestWindow(QWindow *parent = nullptr) + :QWindow(parent) + { + + } + + void mousePressEvent(QMouseEvent *ev) override + { + QCOMPARE(ev->position(), m_mouseTestPoint); + if (devicePixelRatio() == 1 || devicePixelRatio() == 2) // ### off-by-one error on non-integer dpr + QCOMPARE(mapFromGlobal(ev->globalPosition()), m_mouseTestPoint); + } + + }; + + // Verify mouse event coordinates for top-level and chlid windows on each screen + for (QScreen *screen : app->screens()) { + QPoint mouseTestPoint(10, 10); + MousePressTestWindow topLevelWindow; + topLevelWindow.m_mouseTestPoint = mouseTestPoint; + topLevelWindow.resize(QSize(40, 40)); + topLevelWindow.setPosition(screen->geometry().center()); + topLevelWindow.show(); + + QTest::mouseClick(&topLevelWindow, Qt::LeftButton, Qt::KeyboardModifiers(), mouseTestPoint); + MousePressTestWindow childWindow(&topLevelWindow); + childWindow.m_mouseTestPoint = mouseTestPoint; + childWindow.resize(QSize(20, 20)); + childWindow.setPosition(QPoint(15, 15)); + childWindow.show(); + QTest::mouseClick(&childWindow, Qt::LeftButton, Qt::KeyboardModifiers(), mouseTestPoint); + } + + // Verify mouse event coordinates for a window spanning screen 0 and screen 1 + QPoint screen0Center = app->screens()[0]->geometry().center(); + int screenWidth = app->screens()[0]->geometry().width(); + QSize windowSize = QSize(screenWidth - 10, 20); + QRect windowGeometry = QRect(screen0Center, windowSize); + windowGeometry.adjust(0, 0, -10, 0); // Make sure the center point is on screen 0 + MousePressTestWindow window; + window.setGeometry(windowGeometry); + window.show(); + + QPoint screen0Point(QPoint(10,10)); + QPoint screen1Point(QPoint(windowSize.width() - 20,10)); + QCOMPARE(app->screenAt(window.mapToGlobal(screen0Point)), app->screens()[0]); + QCOMPARE(app->screenAt(window.mapToGlobal(screen1Point)), app->screens()[1]); + + window.m_mouseTestPoint = screen0Point; + QTest::mouseClick(&window, Qt::LeftButton, Qt::KeyboardModifiers(), screen0Point); + window.m_mouseTestPoint = screen1Point; + QTest::mouseClick(&window, Qt::LeftButton, Qt::KeyboardModifiers(), screen1Point); +} + +#include "tst_qhighdpi.moc" +QTEST_APPLESS_MAIN(tst_QHighDpi); -- cgit v1.2.3