diff options
Diffstat (limited to 'tests/auto/gui/kernel/qhighdpi/tst_qhighdpi.cpp')
-rw-r--r-- | tests/auto/gui/kernel/qhighdpi/tst_qhighdpi.cpp | 887 |
1 files changed, 887 insertions, 0 deletions
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..6fe4faec03 --- /dev/null +++ b/tests/auto/gui/kernel/qhighdpi/tst_qhighdpi.cpp @@ -0,0 +1,887 @@ +// Copyright (C) 2020 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only + +#include <private/qhighdpiscaling_p.h> +#include <qpa/qplatformscreen.h> +#include <qpa/qplatformnativeinterface.h> + +#include <QTest> +#include <QJsonArray> +#include <QJsonObject> +#include <QJsonDocument> +#include <QStringView> +#include <QSignalSpy> + +Q_LOGGING_CATEGORY(lcTests, "qt.gui.tests") + +class tst_QHighDpi: public QObject +{ + Q_OBJECT +private: // helpers + QJsonArray createStandardScreens(const QList<qreal> &dpiValues); + QGuiApplication *createOffscreenApplication(const QByteArray &jsonConfig); + QGuiApplication *createStandardOffscreenApp(const QList<qreal> &dpiValues); + QGuiApplication *createStandardOffscreenApp(const QJsonArray &screens); + static void standardScreenDpiTestData(); + + static void setOffscreenConfiguration(const QJsonObject &configuration); + static QJsonObject offscreenConfiguration(); + +private slots: + void cleanup(); + void qhighdpiscaling_data(); + void qhighdpiscaling(); + void minimumDpr(); + void noscreens(); + void screenDpiAndDpr_data(); + void screenDpiAndDpr(); + void screenDpiChange(); + void screenDpiChangeWithWindow(); + void environment_QT_SCALE_FACTOR(); + void environment_QT_SCREEN_SCALE_FACTORS_data(); + void environment_QT_SCREEN_SCALE_FACTORS(); + void environment_QT_USE_PHYSICAL_DPI(); + void environment_QT_SCALE_FACTOR_ROUNDING_POLICY(); + void application_setScaleFactorRoundingPolicy(); + 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(); + void mouseVelocity(); + void mouseVelocity_data(); + void setCursor(); + void setCursor_data(); + void setGlobalFactorEmits(); + void setScreenFactorEmits(); +}; + +/// 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<qreal> &dpiValues) +{ + Q_ASSERT(dpiValues.size() == 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<qreal> &dpiValues) +{ + QJsonArray screens = createStandardScreens(dpiValues); + return createStandardOffscreenApp(screens); +} + +QGuiApplication *tst_QHighDpi::createStandardOffscreenApp(const QJsonArray &screens) +{ + 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 different DPI configurations, each with three screens: + QTest::addColumn<QList<qreal>>("dpiValues"); + // Standard-DPI sanity check + QTest::newRow("96") << QList<qreal> { 96, 96, 96 }; + // 2x high DPI + QTest::newRow("192") << QList<qreal> { 192, 192, 192 }; + // Mixed desktop DPI (1.5x, 1.75x, 2x) + QTest::newRow("144-168-192") << QList<qreal> { 144, 168, 192 }; + // Densities from Android's DisplayMetrics docs, normalized to base 96 DPI + QTest::newRow("240-252-360") << QList<qreal> { 400./160 * 96, 420./160 * 96, 600./160 * 96 }; +} + +void tst_QHighDpi::setOffscreenConfiguration(const QJsonObject &configuration) +{ + Q_ASSERT(qApp->platformName() == QLatin1String("offscreen")); + QPlatformNativeInterface *platformNativeInterface = qApp->platformNativeInterface(); + auto setConfiguration = reinterpret_cast<void (*)(QJsonObject, QPlatformNativeInterface *)>( + platformNativeInterface->nativeResourceForIntegration("setConfiguration")); + setConfiguration(configuration, platformNativeInterface); +} + +QJsonObject tst_QHighDpi::offscreenConfiguration() +{ + Q_ASSERT(qApp->platformName() == QLatin1String("offscreen")); + QPlatformNativeInterface *platformNativeInterface = qApp->platformNativeInterface(); + auto getConfiguration = reinterpret_cast<QJsonObject (*)(QPlatformNativeInterface *)>( + platformNativeInterface->nativeResourceForIntegration("configuration")); + return getConfiguration(platformNativeInterface); +} + +void tst_QHighDpi::cleanup() +{ + // Some test functions set environment variables. Unset them here, + // in order to avoid getting confusing follow-on errors on test failures. + qunsetenv("QT_SCALE_FACTOR"); + qunsetenv("QT_SCREEN_SCALE_FACTORS"); + qunsetenv("QT_USE_PHYSICAL_DPI"); + qunsetenv("QT_SCALE_FACTOR_ROUNDING_POLICY"); +} + +void tst_QHighDpi::qhighdpiscaling_data() +{ + standardScreenDpiTestData(); +} + +// Tests the QHighDpiScaling API directly +void tst_QHighDpi::qhighdpiscaling() +{ + QFETCH(QList<qreal>, dpiValues); + std::unique_ptr<QGuiApplication> app(createStandardOffscreenApp(dpiValues)); + + QHighDpiScaling::setGlobalFactor(2); + + // Verfy that QHighDpiScaling::factor() does not crash on nullptr contexts. + QScreen *screenContext = nullptr; + QVERIFY(QHighDpiScaling::factor(screenContext) >= 0); + QPlatformScreen *platformScreenContext = nullptr; + QVERIFY(QHighDpiScaling::factor(platformScreenContext) >= 0); + QWindow *windowContext = nullptr; + QVERIFY(QHighDpiScaling::factor(windowContext) >= 0); + QHighDpiScaling::setGlobalFactor(1); +} + +void tst_QHighDpi::screenDpiAndDpr_data() +{ + standardScreenDpiTestData(); +} + +void tst_QHighDpi::screenDpiAndDpr() +{ + QFETCH(QList<qreal>, dpiValues); + + std::unique_ptr<QGuiApplication> 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()); + window.setGeometry(QRect(screen->geometry().center(), QSize(10, 10))); + window.create(); + QCOMPARE(window.devicePixelRatio(), screen->devicePixelRatio()); + } +} + +void tst_QHighDpi::screenDpiChange() +{ + QList<qreal> dpiValues = { 96, 96, 96}; + std::unique_ptr<QGuiApplication> app(createStandardOffscreenApp(dpiValues)); + + QCOMPARE(app->devicePixelRatio(), 1); + + // Set new DPI + int newDpi = 192; + QJsonValue config = offscreenConfiguration(); + // API defect until Qt 7, so go indirectly via CBOR + QCborMap map = QCborMap::fromJsonObject(config.toObject()); + map[QLatin1String("screens")][0][QLatin1String("logicalDpi")] = newDpi; + map[QLatin1String("screens")][1][QLatin1String("logicalDpi")] = newDpi; + map[QLatin1String("screens")][2][QLatin1String("logicalDpi")] = newDpi; + setOffscreenConfiguration(map.toJsonObject()); + + // TODO check events + + // Verify that the new DPI is in use + for (QScreen *screen : app->screens()) { + QCOMPARE(screen->devicePixelRatio(), newDpi / standardBaseDpi); + QCOMPARE(screen->logicalDotsPerInch(), newDpi / screen->devicePixelRatio()); + + QWindow window(screen); + QCOMPARE(window.devicePixelRatio(), screen->devicePixelRatio()); + window.create(); + QCOMPARE(window.devicePixelRatio(), screen->devicePixelRatio()); + } + QCOMPARE(app->devicePixelRatio(), newDpi / standardBaseDpi); +} + +void tst_QHighDpi::screenDpiChangeWithWindow() +{ + QList<qreal> dpiValues = { 96, 192, 288 }; + std::unique_ptr<QGuiApplication> app(createStandardOffscreenApp(dpiValues)); + + // Create windows for screens + QList<QScreen *> screens = app->screens(); + QList<QWindow *> windows; + for (int i = 0; i < screens.count(); ++i) { + QScreen *screen = screens[i]; + QWindow *window = new QWindow(); + windows.append(window); + window->setGeometry(QRect(screen->geometry().center(), QSize(10, 10))); + window->create(); + QCOMPARE(window->devicePixelRatio(), dpiValues[i] / standardBaseDpi); + } + + // Change screen DPI + QList<qreal> newDpiValues = { 288, 192, 96 }; + QJsonValue config = offscreenConfiguration(); + QCborMap map = QCborMap::fromJsonObject(config.toObject()); + for (int i = 0; i < screens.count(); ++i) { + map[QLatin1String("screens")][i][QLatin1String("logicalDpi")] = newDpiValues[i]; + } + setOffscreenConfiguration(map.toJsonObject()); + + // Verify that window DPR changes on Screen DPI change. + for (int i = 0; i < screens.count(); ++i) { + QWindow *window = windows[i]; + QCOMPARE(window->devicePixelRatio(), newDpiValues[i] / standardBaseDpi); + } +} + +void tst_QHighDpi::environment_QT_SCALE_FACTOR() +{ + qreal factor = 3.1415; + qputenv("QT_SCALE_FACTOR", std::to_string(factor)); + + QList<qreal> dpiValues { 96, 144, 192 }; + std::unique_ptr<QGuiApplication> app(createStandardOffscreenApp(dpiValues)); + int i = 0; + for (QScreen *screen : app->screens()) { + // Verify that QT_SCALE_FACTOR applies as a multiplicative factor. + qreal expextedDpr = (dpiValues[i] / standardBaseDpi) * factor; + ++i; + QCOMPARE(screen->devicePixelRatio(), expextedDpr); + QCOMPARE(screen->logicalDotsPerInch(), 96); + QWindow window(screen); + QCOMPARE(window.devicePixelRatio(), expextedDpr); + } +} + +void tst_QHighDpi::environment_QT_SCREEN_SCALE_FACTORS_data() +{ + QTest::addColumn<QList<qreal>>("platformScreenDpi"); // The to-be-overridden values + QTest::addColumn<QByteArray>("environment"); + QTest::addColumn<QList<qreal>>("expectedDprValues"); + + QList<qreal> platformScreenDpi { 192, 216, 240 }; + QList<qreal> fromPlatformScreenDpr { 2, 2.25, 2.5 }; + QList<qreal> fromEnvironmentDpr { 1, 1.5, 2 }; + + // Correct env. variable values. + QTest::newRow("list") << platformScreenDpi << QByteArray("1;1.5;2") << fromEnvironmentDpr; + QTest::newRow("names") << platformScreenDpi << QByteArray("screen#1=1.5;screen#0=1;screen#2=2") << fromEnvironmentDpr; + + // Various broken env. variable values. Should not crash, + // and should not change the DPR. + QTest::newRow("empty") << platformScreenDpi << QByteArray("") << fromPlatformScreenDpr; + QTest::newRow("bogus-1") << platformScreenDpi << QByteArray("foo=bar") << fromPlatformScreenDpr; + QTest::newRow("bogus-2") << platformScreenDpi << QByteArray("fo0==2;;=;==;=3") << fromPlatformScreenDpr; +} + +void tst_QHighDpi::environment_QT_SCREEN_SCALE_FACTORS() +{ + QFETCH(QList<qreal>, platformScreenDpi); + QFETCH(QByteArray, environment); + QFETCH(QList<qreal>, expectedDprValues); + + qputenv("QT_SCREEN_SCALE_FACTORS", environment); + + // Verify that setting QT_SCREEN_SCALE_FACTORS overrides the from-platform-screen-DPI DPR. + { + std::unique_ptr<QGuiApplication> app(createStandardOffscreenApp(platformScreenDpi)); + int i = 0; + for (QScreen *screen : app->screens()) { + qreal expextedDpr = expectedDprValues[i]; + ++i; + QCOMPARE(screen->devicePixelRatio(), expextedDpr); + QCOMPARE(screen->logicalDotsPerInch(), 96); + QWindow window(screen); + QCOMPARE(window.devicePixelRatio(), expextedDpr); + } + } + + // Verify that setHighDpiScaleFactorRoundingPolicy applies to QT_SCREEN_SCALE_FACTORS as well + QGuiApplication::setHighDpiScaleFactorRoundingPolicy(Qt::HighDpiScaleFactorRoundingPolicy::Round); + { + std::unique_ptr<QGuiApplication> app(createStandardOffscreenApp(platformScreenDpi)); + int i = 0; + for (QScreen *screen : app->screens()) { + qreal expectedRounderDpr = qRound(expectedDprValues[i++]); + qreal windowDpr = QWindow(screen).devicePixelRatio(); + QCOMPARE(windowDpr, expectedRounderDpr); + } + } +} + +void tst_QHighDpi::environment_QT_USE_PHYSICAL_DPI() +{ + qputenv("QT_USE_PHYSICAL_DPI", "1"); + + QList<qreal> dpiValues { 96, 144, 192 }; + std::unique_ptr<QGuiApplication> app(createStandardOffscreenApp(dpiValues)); + + // Verify that the device pixel ratio is computed as physicalDpi / baseDpi. + // (which in practice uses physicalSize since this is what QPlatformScreen provides) + + // The default QPlatformScreen::physicalSize() implementation (which QOffscreenScreen + // currerently uses) assumes a default DPI of 100 and calculates a fake physical size + // based on that value. Use DPI 100 here as well: if you have changed the default value + // in QPlatformScreen and get a test failure then update the value below. + const qreal platformScreenDefualtDpi = 100; + qreal expextedDpr = (platformScreenDefualtDpi / qreal(standardBaseDpi)); + + for (QScreen *screen : app->screens()) { + QCOMPARE(screen->devicePixelRatio(), expextedDpr); + QCOMPARE(screen->logicalDotsPerInch(), 96); + QWindow window(screen); + QCOMPARE(window.devicePixelRatio(), expextedDpr); + } +} + +void tst_QHighDpi::environment_QT_SCALE_FACTOR_ROUNDING_POLICY() +{ + QList<qreal> dpiValues { 96, 144, 192 }; + + qputenv("QT_SCALE_FACTOR_ROUNDING_POLICY", "PassThrough"); + { + std::unique_ptr<QGuiApplication> app(createStandardOffscreenApp(dpiValues)); + for (int i = 0; i < dpiValues.size(); ++i) + QCOMPARE(app->screens()[i]->devicePixelRatio(), dpiValues[i] / qreal(96)); + } + + qputenv("QT_SCALE_FACTOR_ROUNDING_POLICY", "Round"); + { + std::unique_ptr<QGuiApplication> app(createStandardOffscreenApp(dpiValues)); + for (int i = 0; i < dpiValues.size(); ++i) + QCOMPARE(app->screens()[i]->devicePixelRatio(), qRound(dpiValues[i] / qreal(96))); + } + + qunsetenv("QT_SCALE_FACTOR_ROUNDING_POLICY"); + { + std::unique_ptr<QGuiApplication> app(createStandardOffscreenApp(dpiValues)); + for (int i = 0; i < dpiValues.size(); ++i) + QCOMPARE(app->screens()[i]->devicePixelRatio(), dpiValues[i] / qreal(96)); + } +} + +void tst_QHighDpi::application_setScaleFactorRoundingPolicy() +{ + QList<qreal> dpiValues { 96, 144, 192 }; + QGuiApplication::setHighDpiScaleFactorRoundingPolicy(Qt::HighDpiScaleFactorRoundingPolicy::Round); + { + std::unique_ptr<QGuiApplication> app(createStandardOffscreenApp(dpiValues)); + for (int i = 0; i < dpiValues.size(); ++i) + QCOMPARE(app->screens()[i]->devicePixelRatio(), qRound(dpiValues[i] / qreal(96))); + } + + QGuiApplication::setHighDpiScaleFactorRoundingPolicy(Qt::HighDpiScaleFactorRoundingPolicy::PassThrough); + { + std::unique_ptr<QGuiApplication> app(createStandardOffscreenApp(dpiValues)); + for (int i = 0; i < dpiValues.size(); ++i) + QCOMPARE(app->screens()[i]->devicePixelRatio(), dpiValues[i] / qreal(96)); + } + + // Verify that environment overrides app setting + QGuiApplication::setHighDpiScaleFactorRoundingPolicy(Qt::HighDpiScaleFactorRoundingPolicy::Round); + qputenv("QT_SCALE_FACTOR_ROUNDING_POLICY", "PassThrough"); + { + std::unique_ptr<QGuiApplication> app(createStandardOffscreenApp(dpiValues)); + for (int i = 0; i < dpiValues.size(); ++i) + QCOMPARE(app->screens()[i]->devicePixelRatio(), dpiValues[i] / qreal(96)); + } +} + +void tst_QHighDpi::minimumDpr() +{ + QList<qreal> dpiValues { 40, 60, 95 }; + std::unique_ptr<QGuiApplication> app(createStandardOffscreenApp(dpiValues)); + for (QScreen *screen : app->screens()) { + // Qt does not currently support DPR values < 1. Make sure + // the minimum DPR value is 1, also when the screen reports + // a low DPI. + QCOMPARE(screen->devicePixelRatio(), 1); + QWindow window(screen); + QCOMPARE(window.devicePixelRatio(), 1); + } +} + +QT_BEGIN_NAMESPACE +extern int qt_defaultDpiX(); +extern int qt_defaultDpiY(); +QT_END_NAMESPACE + +void tst_QHighDpi::noscreens() +{ + // Create application object with a no-screens configuration (should not crash) + QJsonArray noScreens; + std::unique_ptr<QGuiApplication> app(createStandardOffscreenApp(noScreens)); + + QCOMPARE(qApp->devicePixelRatio(), 1); + + // Test calling qt_defaultDpiX/Y: These may be called early during QGuiApplication + // initialization, before the platform plugin has created screen objects. They + // should then 1) not crash and 2) return some default value. + QCOMPARE(qt_defaultDpiX(), qt_defaultDpiY()); +} + +void tst_QHighDpi::screenAt_data() +{ + standardScreenDpiTestData(); +} + +void tst_QHighDpi::screenAt() +{ + QFETCH(QList<qreal>, dpiValues); + std::unique_ptr<QGuiApplication> app(createStandardOffscreenApp(dpiValues)); + + QCOMPARE(app->screens().size(), 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().size(), 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<qreal>, dpiValues); + std::unique_ptr<QGuiApplication> 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<qreal>, dpiValues); + std::unique_ptr<QGuiApplication> 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<qreal>, dpiValues); + std::unique_ptr<QGuiApplication> 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<qreal>, dpiValues); + std::unique_ptr<QGuiApplication> 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); +} + +void tst_QHighDpi::mouseVelocity_data() +{ + standardScreenDpiTestData(); +} + +void tst_QHighDpi::mouseVelocity() +{ + QFETCH(QList<qreal>, dpiValues); + std::unique_ptr<QGuiApplication> app(createStandardOffscreenApp(dpiValues)); + + class MouseVelocityTestWindow : public QWindow { + public: + QVector2D velocity; + bool decel = false; + + bool event(QEvent *ev) override + { + if (!ev->isPointerEvent()) + qCDebug(lcTests) << ev; + return QWindow::event(ev); + } + + void mousePressEvent(QMouseEvent *ev) override + { + velocity = ev->points().first().velocity(); + qCDebug(lcTests) << "velocity" << velocity << ev; + } + + void mouseMoveEvent(QMouseEvent *ev) override + { + velocity = ev->points().first().velocity(); + if (ev->buttons()) + qDebug(lcTests) << "velocity" << velocity << ev; + } + }; + + // Verify velocity direction and sign on each screen + // FYI: Turn on the qt.pointer.velocity logging category to see how it's calculated + for (QScreen *screen : app->screens()) { + MouseVelocityTestWindow topLevelWindow; + topLevelWindow.resize(QSize(120, 120)); + topLevelWindow.setPosition(screen->geometry().center()); + topLevelWindow.show(); + + QPoint endP; + qreal maxVx = 0; + qreal maxVy = 0; + qreal minVx = INT_MAX; + qreal minVy = INT_MAX; + for (int xDelta = 10; xDelta >= -10; xDelta -= 10) { + for (int yDelta = 10; yDelta >= -10; yDelta -= 10) { + QPoint p(60, 60); + // move closer to p, decelerating, to get the velocity down to a small value + for (int i = 0; i < 12; ++i) { + endP += (p - endP) / 4; + QTest::mouseMove(&topLevelWindow, endP, 3 * i); + } + qCDebug(lcTests) << "beginning drag with dx" << xDelta << "dy" << yDelta; + QTest::mouseMove(&topLevelWindow, p, 10); + QTest::mousePress(&topLevelWindow, Qt::LeftButton, {}, p); + QVERIFY(qAbs(topLevelWindow.velocity.x()) < 50); + QVERIFY(qAbs(topLevelWindow.velocity.y()) < 50); + for (int i = 0; i < 4; ++i) { + p += QPoint(xDelta, yDelta); + QTest::mouseMove(&topLevelWindow, p, 10); + if (xDelta) { + // same sign and decent magnitude: + // 10 px in 10 ms =~ 1000 px / second; should be in logical coordinates on any screen + // but it's not exactly 1000 because of the Kalman filter + QVERIFY(topLevelWindow.velocity.x() * xDelta > 0); + QVERIFY(qAbs(topLevelWindow.velocity.x()) > 500); + } else { + QVERIFY(qAbs(topLevelWindow.velocity.x()) < 10); + } + if (yDelta) { + QVERIFY(topLevelWindow.velocity.y() * yDelta > 0); + QVERIFY(qAbs(topLevelWindow.velocity.y()) > 500); + } else { + QVERIFY(qAbs(topLevelWindow.velocity.y()) < 10); + } + maxVx = qMax(topLevelWindow.velocity.x(), maxVx); + maxVy = qMax(topLevelWindow.velocity.y(), maxVy); + minVx = qMin(topLevelWindow.velocity.x(), minVx); + minVy = qMin(topLevelWindow.velocity.y(), minVy); + } + QTest::mouseRelease(&topLevelWindow, Qt::LeftButton, {}, p); + endP = p; // QED + } + } + qCDebug(lcTests) << "mouse land speed record: forward" << maxVx << maxVy << "reverse" << minVx << minVy; + // all drags were at the same speed, so max speed should be equal in each direction + QVERIFY(qAbs(maxVx - maxVy) < 10); + QVERIFY(qAbs(minVx - minVy) < 10); + QVERIFY(maxVx + minVx < 10); + QVERIFY(maxVy + minVy < 10); + } +} + +void tst_QHighDpi::setCursor_data() +{ + standardScreenDpiTestData(); +} + +void tst_QHighDpi::setCursor() +{ + QFETCH(QList<qreal>, dpiValues); + std::unique_ptr<QGuiApplication> app(createStandardOffscreenApp(dpiValues)); + + for (QScreen *screen : app->screens()) { + QPoint center = screen->geometry().center(); + QCursor::setPos(center.x(), center.y()); + QCOMPARE(QCursor::pos(), center); + } +} + +void tst_QHighDpi::setGlobalFactorEmits() +{ + QList<qreal> dpiValues { 96, 96, 96 }; + std::unique_ptr<QGuiApplication> app(createStandardOffscreenApp(dpiValues)); + + std::vector<std::unique_ptr<QSignalSpy>> spies; + for (QScreen *screen : app->screens()) + spies.push_back(std::make_unique<QSignalSpy>(screen, &QScreen::geometryChanged)); + + QHighDpiScaling::setGlobalFactor(2); + + for (const auto &spy : spies) + QCOMPARE(spy->count(), 1); + + QHighDpiScaling::setGlobalFactor(1); +} + +void tst_QHighDpi::setScreenFactorEmits() +{ + QList<qreal> dpiValues { 96, 96, 96 }; + std::unique_ptr<QGuiApplication> app(createStandardOffscreenApp(dpiValues)); + + for (QScreen *screen : app->screens()) { + QSignalSpy spy(screen, &QScreen::geometryChanged); + QHighDpiScaling::setScreenFactor(screen, 2); + QCOMPARE(spy.count(), 1); + } +} + +#include "tst_qhighdpi.moc" +QTEST_APPLESS_MAIN(tst_QHighDpi); |