diff options
Diffstat (limited to 'tests/auto/widgets/kernel/qwidget/tst_qwidget.cpp')
-rw-r--r-- | tests/auto/widgets/kernel/qwidget/tst_qwidget.cpp | 3310 |
1 files changed, 2711 insertions, 599 deletions
diff --git a/tests/auto/widgets/kernel/qwidget/tst_qwidget.cpp b/tests/auto/widgets/kernel/qwidget/tst_qwidget.cpp index 014e31d1a9..3447c9af28 100644 --- a/tests/auto/widgets/kernel/qwidget/tst_qwidget.cpp +++ b/tests/auto/widgets/kernel/qwidget/tst_qwidget.cpp @@ -1,30 +1,5 @@ -/**************************************************************************** -** -** Copyright (C) 2016 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$ -** -****************************************************************************/ +// Copyright (C) 2016 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only #include "../../../shared/highdpi.h" @@ -38,6 +13,7 @@ #include <qlineedit.h> #include <qlistview.h> #include <qmessagebox.h> +#include <qmimedata.h> #include <qpainter.h> #include <qpoint.h> #include <qpushbutton.h> @@ -52,6 +28,7 @@ #include <qmainwindow.h> #include <qdockwidget.h> #include <qrandom.h> +#include <qsignalspy.h> #include <qstylehints.h> #include <qtoolbar.h> #include <qtoolbutton.h> @@ -61,6 +38,7 @@ #include <QtGui/qbackingstore.h> #include <QtGui/qguiapplication.h> #include <QtGui/qpa/qplatformwindow.h> +#include <QtGui/qpa/qplatformdrag.h> #include <QtGui/qscreen.h> #include <qmenubar.h> #include <qcompleter.h> @@ -73,15 +51,13 @@ #include <QtGui/qwindow.h> #include <qtimer.h> #include <QtWidgets/QDoubleSpinBox> - -#if defined(Q_OS_MACOS) -#include "tst_qwidget_mac_helpers.h" // Abstract the ObjC stuff out so not everyone must run an ObjC++ compile. -#endif +#include <QtWidgets/QComboBox> #include <QtTest/QTest> #include <QtTest/private/qtesthelpers_p.h> using namespace QTestPrivate; +using namespace Qt::StringLiterals; #if defined(Q_OS_WIN) # include <QtCore/qt_windows.h> @@ -146,6 +122,34 @@ static QByteArray msgComparisonFailed(T v1, const char *op, T v2) return s.toLocal8Bit(); } +template<class T> class EventSpy : public QObject +{ +public: + EventSpy(T *widget, QEvent::Type event) + : m_widget(widget), eventToSpy(event) + { + if (m_widget) + m_widget->installEventFilter(this); + } + + T *widget() const { return m_widget; } + int count() const { return m_count; } + void clear() { m_count = 0; } + +protected: + bool eventFilter(QObject *object, QEvent *event) override + { + if (event->type() == eventToSpy) + ++m_count; + return QObject::eventFilter(object, event); + } + +private: + T *m_widget; + const QEvent::Type eventToSpy; + int m_count = 0; +}; + Q_LOGGING_CATEGORY(lcTests, "qt.widgets.tests") class tst_QWidget : public QObject @@ -159,7 +163,10 @@ public: public slots: void initTestCase(); void cleanup(); + private slots: + void nativeWindowAttribute(); + void addActionOverloads(); void getSetCheck(); void fontPropagation(); void fontPropagation2(); @@ -184,10 +191,15 @@ private slots: void mapFromAndTo(); void focusChainOnHide(); void focusChainOnReparent(); + void focusAbstraction(); void defaultTabOrder(); void reverseTabOrder(); void tabOrderWithProxy(); + void tabOrderWithProxyDisabled(); + void tabOrderWithProxyOutOfOrder(); void tabOrderWithCompoundWidgets(); + void tabOrderWithCompoundWidgetsInflection_data(); + void tabOrderWithCompoundWidgetsInflection(); void tabOrderWithCompoundWidgetsNoFocusPolicy(); void tabOrderNoChange(); void tabOrderNoChange2(); @@ -195,12 +207,16 @@ private slots: void appFocusWidgetWhenLosingFocusProxy(); void explicitTabOrderWithComplexWidget(); void explicitTabOrderWithSpinBox_QTBUG81097(); + void tabOrderList(); + void tabOrderComboBox_data(); + void tabOrderComboBox(); #if defined(Q_OS_WIN) void activation(); #endif void reparent(); void setScreen(); void windowState(); + void resizePropagation(); void showMaximized(); void showFullScreen(); void showMinimized(); @@ -218,6 +234,8 @@ private slots: void saveRestoreGeometry(); void restoreVersion1Geometry_data(); void restoreVersion1Geometry(); + void restoreGeometryAfterScreenChange_data(); + void restoreGeometryAfterScreenChange(); void widgetAt(); #ifdef Q_OS_MACOS @@ -238,11 +256,14 @@ private slots: void ensureCreated(); void createAndDestroy(); + void eventsAndAttributesOnDestroy(); void winIdChangeEvent(); void persistentWinId(); void showNativeChild(); + void closeAndShowNativeChild(); + void closeAndShowWithNativeChild(); void transientParent(); - void qobject_castInDestroyedSlot(); + void qobject_castOnDestruction(); void showHideEvent_data(); void showHideEvent(); @@ -294,8 +315,11 @@ private slots: void childEvents(); void render(); + void renderChildFillsBackground(); + void renderTargetOffset(); void renderInvisible(); void renderWithPainter(); + void renderRTL(); void render_task188133(); void render_task211796(); void render_task217815(); @@ -328,6 +352,8 @@ private slots: void resizeInPaintEvent(); void opaqueChildren(); + void dumpObjectTree(); + void setMaskInResizeEvent(); void moveInResizeEvent(); @@ -356,7 +382,10 @@ private slots: void maskedUpdate(); #ifndef QT_NO_CURSOR void syntheticEnterLeave(); + void enterLeaveOnWindowShowHide_data(); + void enterLeaveOnWindowShowHide(); void taskQTBUG_4055_sendSyntheticEnterLeave(); + void hoverPosition(); void underMouse(); void taskQTBUG_27643_enterEvents(); #endif @@ -368,6 +397,8 @@ private slots: void focusWidget_task254563(); void rectOutsideCoordinatesLimit_task144779(); void setGraphicsEffect(); + void render_graphicsEffect_data(); + void render_graphicsEffect(); #ifdef QT_BUILD_INTERNAL void destroyBackingStore(); @@ -378,7 +409,8 @@ private slots: void openModal_taskQTBUG_5804(); void focusProxy(); - void focusProxyAndInputMethods(); + void imEnabledNotImplemented(); + #ifdef QT_BUILD_INTERNAL void scrollWithoutBackingStore(); #endif @@ -425,10 +457,27 @@ private slots: void receivesApplicationFontChangeEvent(); void receivesApplicationPaletteChangeEvent(); void deleteWindowInCloseEvent(); + void quitOnClose(); -private: - bool ensureScreenSize(int width, int height); + void setParentChangesFocus_data(); + void setParentChangesFocus(); + + void activateWhileModalHidden(); +#ifdef Q_OS_ANDROID + void showFullscreenAndroid(); +#endif + + void setVisibleDuringDestruction(); + + void explicitShowHide(); + + void dragEnterLeaveSymmetry(); + + void reparentWindowHandles_data(); + void reparentWindowHandles(); + +private: const QString m_platform; QSize m_testWidgetSize; QPoint m_availableTopLeft; @@ -436,13 +485,17 @@ private: const bool m_windowsAnimationsEnabled; QPointingDevice *m_touchScreen; const int m_fuzz; -}; + QPalette simplePalette(); -bool tst_QWidget::ensureScreenSize(int width, int height) -{ - const QSize available = QGuiApplication::primaryScreen()->availableGeometry().size(); - return (available.width() >= width && available.height() >= height); -} +private: + enum class ScreenPosition { + OffAbove, + OffLeft, + OffBelow, + OffRight, + Contained + }; +}; // Testing get/set functions void tst_QWidget::getSetCheck() @@ -458,18 +511,24 @@ void tst_QWidget::getSetCheck() QVERIFY(var1.data() != obj1.style()); QVERIFY(obj1.style() != nullptr); // style can never be 0 for a widget + const QRegularExpression negativeNotPossible(u"^.*Negative sizes \\(.*\\) are not possible$"_s); + const QRegularExpression largestAllowedSize(u"^.*The largest allowed size is \\(.*\\)$"_s); // int QWidget::minimumWidth() // void QWidget::setMinimumWidth(int) obj1.setMinimumWidth(0); QCOMPARE(obj1.minimumWidth(), 0); + QTest::ignoreMessage(QtWarningMsg, negativeNotPossible); obj1.setMinimumWidth(INT_MIN); QCOMPARE(obj1.minimumWidth(), 0); // A widgets width can never be less than 0 + QTest::ignoreMessage(QtWarningMsg, largestAllowedSize); obj1.setMinimumWidth(INT_MAX); child1.setMinimumWidth(0); QCOMPARE(child1.minimumWidth(), 0); + QTest::ignoreMessage(QtWarningMsg, negativeNotPossible); child1.setMinimumWidth(INT_MIN); QCOMPARE(child1.minimumWidth(), 0); // A widgets width can never be less than 0 + QTest::ignoreMessage(QtWarningMsg, largestAllowedSize); child1.setMinimumWidth(INT_MAX); QCOMPARE(child1.minimumWidth(), QWIDGETSIZE_MAX); // The largest minimum size should only be as big as the maximium @@ -477,14 +536,18 @@ void tst_QWidget::getSetCheck() // void QWidget::setMinimumHeight(int) obj1.setMinimumHeight(0); QCOMPARE(obj1.minimumHeight(), 0); + QTest::ignoreMessage(QtWarningMsg, negativeNotPossible); obj1.setMinimumHeight(INT_MIN); QCOMPARE(obj1.minimumHeight(), 0); // A widgets height can never be less than 0 + QTest::ignoreMessage(QtWarningMsg, largestAllowedSize); obj1.setMinimumHeight(INT_MAX); child1.setMinimumHeight(0); QCOMPARE(child1.minimumHeight(), 0); + QTest::ignoreMessage(QtWarningMsg, negativeNotPossible); child1.setMinimumHeight(INT_MIN); QCOMPARE(child1.minimumHeight(), 0); // A widgets height can never be less than 0 + QTest::ignoreMessage(QtWarningMsg, largestAllowedSize); child1.setMinimumHeight(INT_MAX); QCOMPARE(child1.minimumHeight(), QWIDGETSIZE_MAX); // The largest minimum size should only be as big as the maximium @@ -492,8 +555,10 @@ void tst_QWidget::getSetCheck() // void QWidget::setMaximumWidth(int) obj1.setMaximumWidth(0); QCOMPARE(obj1.maximumWidth(), 0); + QTest::ignoreMessage(QtWarningMsg, negativeNotPossible); obj1.setMaximumWidth(INT_MIN); QCOMPARE(obj1.maximumWidth(), 0); // A widgets width can never be less than 0 + QTest::ignoreMessage(QtWarningMsg, largestAllowedSize); obj1.setMaximumWidth(INT_MAX); QCOMPARE(obj1.maximumWidth(), QWIDGETSIZE_MAX); // QWIDGETSIZE_MAX is the abs max, not INT_MAX @@ -501,8 +566,10 @@ void tst_QWidget::getSetCheck() // void QWidget::setMaximumHeight(int) obj1.setMaximumHeight(0); QCOMPARE(obj1.maximumHeight(), 0); + QTest::ignoreMessage(QtWarningMsg, negativeNotPossible); obj1.setMaximumHeight(INT_MIN); QCOMPARE(obj1.maximumHeight(), 0); // A widgets height can never be less than 0 + QTest::ignoreMessage(QtWarningMsg, largestAllowedSize); obj1.setMaximumHeight(INT_MAX); QCOMPARE(obj1.maximumHeight(), QWIDGETSIZE_MAX); // QWIDGETSIZE_MAX is the abs max, not INT_MAX @@ -562,6 +629,7 @@ void tst_QWidget::getSetCheck() QBoxLayout *var11 = new QBoxLayout(QBoxLayout::LeftToRight); obj1.setLayout(var11); QCOMPARE(static_cast<QLayout *>(var11), obj1.layout()); + QTest::ignoreMessage(QtWarningMsg, "QWidget::setLayout: Cannot set layout to 0"); obj1.setLayout(nullptr); QCOMPARE(static_cast<QLayout *>(var11), obj1.layout()); // You cannot set a 0-pointer layout, that keeps the current delete var11; // This will remove the layout from the widget @@ -585,7 +653,7 @@ void tst_QWidget::getSetCheck() #if defined (Q_OS_WIN) obj1.setWindowFlags(Qt::FramelessWindowHint | Qt::WindowSystemMenuHint); const HWND handle = reinterpret_cast<HWND>(obj1.winId()); // explicitly create window handle - QVERIFY(GetWindowLong(handle, GWL_STYLE) & LONG(WS_POPUP)); + QVERIFY(GetWindowLongPtr(handle, GWL_STYLE) & LONG_PTR(WS_POPUP)); #endif } @@ -607,6 +675,9 @@ tst_QWidget::tst_QWidget() palette.setColor(QPalette::ToolTipBase, QColor(12, 13, 14)); palette.setColor(QPalette::Text, QColor(21, 22, 23)); QApplication::setPalette(palette, "QPropagationTestWidget"); + + if (QApplication::platformName().startsWith(QLatin1String("wayland"))) + qputenv("QT_WAYLAND_DISABLE_WINDOWDECORATION", "1"); } tst_QWidget::~tst_QWidget() @@ -619,6 +690,10 @@ tst_QWidget::~tst_QWidget() void tst_QWidget::initTestCase() { +#ifdef Q_OS_ANDROID + if (QNativeInterface::QAndroidApplication::sdkVersion() == 33) + QSKIP("Is flaky on Android 13 / RHEL 8.6 and 8.8 (QTQAINFRA-5606)"); +#endif // Size of reference widget, 200 for < 2000, scale up for larger screens // to avoid Windows warnings about minimum size for decorated windows. int width = 200; @@ -641,6 +716,90 @@ void tst_QWidget::cleanup() QTRY_VERIFY(QApplication::topLevelWidgets().isEmpty()); } +template <typename T> +struct ImplicitlyConvertibleTo { + T t; + operator const T() const { return t; } + operator T() { return t; } +}; + +void testFunction0() {} +void testFunction1(bool) {} + +void tst_QWidget::nativeWindowAttribute() +{ + QWidget parent; + QWidget child(&parent); + + QCOMPARE(parent.windowHandle(), nullptr); + QCOMPARE(child.windowHandle(), nullptr); + + // Setting WA_NativeWindow should create window handle + parent.setAttribute(Qt::WA_NativeWindow); + QVERIFY(parent.windowHandle() != nullptr); + // But not its child's window handle + QCOMPARE(child.windowHandle(), nullptr); + // Until the child also gains WA_NativeWindow + child.setAttribute(Qt::WA_NativeWindow); + QVERIFY(child.windowHandle() != nullptr); +} + +void tst_QWidget::addActionOverloads() +{ + // almost exhaustive check of addAction() overloads: + // (text), (icon, text), (icon, text, shortcut), (text, shortcut) + // each with a good sample of ways to QObject::connect() to + // QAction::triggered(bool) + QWidget w; + + // don't just pass QString etc - that'd be too easy (think QStringBuilder) + ImplicitlyConvertibleTo<QString> text = {QStringLiteral("foo")}; + ImplicitlyConvertibleTo<QIcon> icon; + + const auto check = [&](auto &...args) { // don't need to perfectly-forward, only lvalues passed + w.addAction(args...); + + w.addAction(args..., &w, SLOT(deleteLater())); + w.addAction(args..., &w, &QObject::deleteLater); + w.addAction(args..., testFunction0); + w.addAction(args..., &w, testFunction0); + w.addAction(args..., testFunction1); + w.addAction(args..., &w, testFunction1); + w.addAction(args..., [&](bool b) { w.setEnabled(b); }); + w.addAction(args..., &w, [&](bool b) { w.setEnabled(b); }); + + w.addAction(args..., &w, SLOT(deleteLater()), Qt::QueuedConnection); + w.addAction(args..., &w, &QObject::deleteLater, Qt::QueuedConnection); + // doesn't exist: w.addAction(args..., testFunction0, Qt::QueuedConnection); + w.addAction(args..., &w, testFunction0, Qt::QueuedConnection); + // doesn't exist: w.addAction(args..., testFunction1, Qt::QueuedConnection); + w.addAction(args..., &w, testFunction1, Qt::QueuedConnection); + // doesn't exist: w.addAction(args..., [&](bool b) { w.setEnabled(b); }, Qt::QueuedConnection); + w.addAction(args..., &w, [&](bool b) { w.setEnabled(b); }, Qt::QueuedConnection); + }; + const auto check1 = [&](auto &arg, auto &...args) { + check(arg, args...); + check(std::as_const(arg), args...); + }; + const auto check2 = [&](auto &arg1, auto &arg2, auto &...args) { + check1(arg1, arg2, args...); + check1(arg1, std::as_const(arg2), args...); + }; + [[maybe_unused]] + const auto check3 = [&](auto &arg1, auto &arg2, auto &arg3) { + check2(arg1, arg2, arg3); + check2(arg1, arg2, std::as_const(arg3)); + }; + + check1(text); + check2(icon, text); +#ifndef QT_NO_SHORTCUT + ImplicitlyConvertibleTo<QKeySequence> keySequence = {Qt::CTRL | Qt::Key_C}; + check2(text, keySequence); + check3(icon, text, keySequence); +#endif +} + void tst_QWidget::fontPropagation() { QScopedPointer<QWidget> testWidget(new QWidget); @@ -1163,6 +1322,8 @@ void tst_QWidget::ignoreKeyEventsWhenDisabled_QTBUG27417() centerOnScreen(&lineEdit); lineEdit.setDisabled(true); lineEdit.show(); + QTest::ignoreMessage(QtWarningMsg, "Keyboard event not accepted by receiving widget"); + QTest::ignoreMessage(QtWarningMsg, "Keyboard event not accepted by receiving widget"); QTest::keyClick(&lineEdit, Qt::Key_A); QTRY_VERIFY(lineEdit.text().isEmpty()); } @@ -1777,8 +1938,6 @@ void tst_QWidget::focusChainOnHide() QWidget::setTabOrder(child, parent.data()); parent->show(); - QApplication::setActiveWindow(parent->window()); - child->activateWindow(); child->setFocus(); QTRY_VERIFY(child->hasFocus()); @@ -1819,8 +1978,11 @@ public: setObjectName(name); lineEdit1 = new QLineEdit; + lineEdit1->setObjectName(name + "/lineEdit1"); lineEdit2 = new QLineEdit; + lineEdit2->setObjectName(name + "/lineEdit2"); lineEdit3 = new QLineEdit; + lineEdit3->setObjectName(name + "/lineEdit3"); lineEdit3->setEnabled(false); QHBoxLayout* hbox = new QHBoxLayout(this); @@ -1835,6 +1997,112 @@ public: QLineEdit *lineEdit3; }; +static QList<QWidget *> getFocusChain(QWidget *start, bool bForward) +{ + QList<QWidget *> ret; + QWidget *cur = start; + // detect infinite loop + int count = 100; + auto loopGuard = qScopeGuard([]{ + QFAIL("Inifinite loop detected in focus chain"); + }); + do { + ret += cur; + cur = bForward ? cur->nextInFocusChain() : cur->previousInFocusChain(); + if (!--count) + return ret; + } while (cur != start); + loopGuard.dismiss(); + return ret; +} + +void tst_QWidget::focusAbstraction() +{ + QLoggingCategory::setFilterRules("qt.widgets.focus=true"); + QWidget *widget1 = new QWidget; + widget1->setObjectName("Widget 1"); + QWidget *widget2 = new QWidget; + widget2->setObjectName("Widget 2"); + QWidget *widget3 = new QWidget; + widget3->setObjectName("Widget 3"); + QWidgetPrivate *priv1 = QWidgetPrivate::get(widget1); + QWidgetPrivate *priv2 = QWidgetPrivate::get(widget2); + QWidgetPrivate *priv3 = QWidgetPrivate::get(widget3); + + // Verify initialization + QVERIFY(!priv1->isInFocusChain()); + QVERIFY(!priv2->isInFocusChain()); + QVERIFY(!priv3->isInFocusChain()); + + // Verify, that parenting builds a focus chain. + QWidget parent; + parent.setObjectName("Parent"); + widget1->setParent(&parent); + widget2->setParent(&parent); + widget3->setParent(&parent); + QVERIFY(priv1->isInFocusChain()); + QVERIFY(priv2->isInFocusChain()); + QVERIFY(priv3->isInFocusChain()); + QWidgetList expected{widget1, widget2, widget3, &parent}; + QCOMPARE(getFocusChain(widget1, true), expected); + + // Verify, that reparented focus children end up behind parent. + widget1->setParent(widget2); + priv2->insertIntoFocusChainAfter(widget3); + priv2->reparentFocusChildren(QWidgetPrivate::FocusDirection::Next); + expected = {widget1, &parent, widget3, widget2}; + QCOMPARE(getFocusChain(widget1, true), expected); + QVERIFY(priv1->isInFocusChain()); + QVERIFY(priv2->isInFocusChain()); + QVERIFY(priv3->isInFocusChain()); + + // Check removal + priv3->removeFromFocusChain(QWidgetPrivate::FocusChainRemovalRule::AssertConsistency); + expected.removeOne(widget3); + QCOMPARE(getFocusChain(widget1, true), expected); + QVERIFY(priv1->isInFocusChain()); + QVERIFY(priv2->isInFocusChain()); + QVERIFY(!priv3->isInFocusChain()); + + // Check insert + priv3->insertIntoFocusChain(QWidgetPrivate::FocusDirection::Previous, widget1); + expected = {widget3, widget1, &parent, widget2}; + QCOMPARE(getFocusChain(widget3, true), expected); + + // Verify, that take doesn't break + const QWidgetList taken = QWidgetPrivate::takeFromFocusChain(widget1, widget2); + QVERIFY(priv1->isFocusChainConsistent()); + expected = {widget1, &parent, widget2}; + QCOMPARE(taken, expected); + QVERIFY(priv1->isInFocusChain()); + QVERIFY(priv2->isInFocusChain()); + QVERIFY(!priv3->isInFocusChain()); + + // Verify insertion of multiple widgets + QWidgetPrivate::insertIntoFocusChain(taken, QWidgetPrivate::FocusDirection::Next, widget3); + expected = {widget3, widget1, &parent, widget2}; + QCOMPARE(getFocusChain(widget3, true), expected); + QVERIFY(priv1->isInFocusChain()); + QVERIFY(priv2->isInFocusChain()); + QVERIFY(priv2->isInFocusChain()); + + // Verify broken chain identification + // d'tor asserts chain consistency => repair before going out of scope + auto guard = qScopeGuard([priv2, widget3]{ priv2->focus_next = widget3; }); + + // Nullptr is not allowed + priv2->focus_next = nullptr; + QVERIFY(!priv1->isFocusChainConsistent()); + + // Chain looping back in the middle + priv2->focus_next = widget1; + QVERIFY(!priv1->isFocusChainConsistent()); + + // "last" element pointing to itself + priv2->focus_next = widget2; + QVERIFY(!priv1->isFocusChainConsistent()); +} + void tst_QWidget::defaultTabOrder() { if (QGuiApplication::platformName().startsWith(QLatin1String("wayland"), Qt::CaseInsensitive)) @@ -1858,7 +2126,6 @@ void tst_QWidget::defaultTabOrder() container.setWindowTitle(QLatin1String(QTest::currentTestFunction())); container.show(); container.activateWindow(); - QApplication::setActiveWindow(&container); QVERIFY(QTest::qWaitForWindowActive(&container)); QTRY_VERIFY(firstEdit->hasFocus()); @@ -1894,23 +2161,29 @@ void tst_QWidget::defaultTabOrder() void tst_QWidget::reverseTabOrder() { - if (QGuiApplication::platformName().startsWith(QLatin1String("wayland"), Qt::CaseInsensitive)) + if (QGuiApplication::platformName().startsWith(QLatin1StringView("wayland"), Qt::CaseInsensitive)) QSKIP("Wayland: This fails. Figure out why."); const int compositeCount = 2; Container container; - container.setWindowTitle(QLatin1String(QTest::currentTestFunction())); + container.setObjectName(QLatin1StringView("Container")); + container.setWindowTitle(QLatin1StringView(QTest::currentTestFunction())); Composite* composite[compositeCount]; QLineEdit *firstEdit = new QLineEdit(); + firstEdit->setObjectName(QLatin1StringView("FirstEdit")); container.box->addWidget(firstEdit); + static constexpr QLatin1StringView comp("Composite-%1"); for (int i = 0; i < compositeCount; i++) { - composite[i] = new Composite(); + const QString name = QString(comp).arg(i); + composite[i] = new Composite(nullptr, name); + composite[i]->setObjectName(name); container.box->addWidget(composite[i]); } QLineEdit *lastEdit = new QLineEdit(); + lastEdit->setObjectName(QLatin1StringView("LastEdit")); container.box->addWidget(lastEdit); // Reverse tab order inside each composite @@ -1919,7 +2192,6 @@ void tst_QWidget::reverseTabOrder() container.show(); container.activateWindow(); - QApplication::setActiveWindow(&container); QVERIFY(QTest::qWaitForWindowActive(&container)); QTRY_VERIFY(firstEdit->hasFocus()); @@ -1954,6 +2226,139 @@ void tst_QWidget::reverseTabOrder() QVERIFY(firstEdit->hasFocus()); } +void tst_QWidget::tabOrderList() +{ + Composite c; + QCOMPARE(getFocusChain(&c, true), + QList<QWidget *>({&c, c.lineEdit1, c.lineEdit2, c.lineEdit3})); + QWidget::setTabOrder({c.lineEdit3, c.lineEdit2, c.lineEdit1}); + // not starting with 3 like one would maybe expect, but still 3, 2, 1 + QCOMPARE(getFocusChain(&c, true), + QList<QWidget *>({&c, c.lineEdit1, c.lineEdit3, c.lineEdit2})); +} + +void tst_QWidget::tabOrderComboBox_data() +{ + QTest::addColumn<const bool>("editableAtBeginning"); + QTest::addColumn<const QList<int>>("firstTabOrder"); + QTest::addColumn<const QList<int>>("secondTabOrder"); + + QTest::addRow("3 not editable") << false << QList<int>{2, 1, 0} << QList<int>{0, 1, 2}; + QTest::addRow("4 editable") << true << QList<int>{2, 1, 0, 3} << QList<int>{3, 0, 2, 1}; +} + +QWidgetList expectedFocusChain(const QList<QComboBox *> &boxes, const QList<int> &sequence) +{ + Q_ASSERT(boxes.count() == sequence.count()); + QWidgetList widgets; + for (int i : sequence) { + Q_ASSERT(i >= 0); + Q_ASSERT(i < boxes.count()); + QComboBox *box = boxes.at(i); + widgets.append(box); + if (box->lineEdit()) + widgets.append(box->lineEdit()); + } + + return widgets; +} + +QWidgetList realFocusChain(const QList<QComboBox *> &boxes, const QList<int> &sequence) +{ + const QWidgetList all = getFocusChain(boxes.at(sequence.at(0)), true); + QWidgetList chain; + // Filter everything with NoFocus + for (auto *widget : all) { + if (widget->focusPolicy() != Qt::NoFocus) + chain << widget; + } + return chain; +} + +void setTabOrder(const QList<QComboBox *> &boxes, const QList<int> &sequence) +{ + Q_ASSERT(boxes.count() == sequence.count()); + QWidget *previous = nullptr; + for (int i : sequence) { + Q_ASSERT(i >= 0); + Q_ASSERT(i < boxes.count()); + QWidget *box = boxes.at(i); + if (!previous) { + previous = box; + } else { + QWidget::setTabOrder(previous, box); + previous = box; + } + } +} + +void tst_QWidget::tabOrderComboBox() +{ + QFETCH(const bool, editableAtBeginning); + QFETCH(const QList<int>, firstTabOrder); + QFETCH(const QList<int>, secondTabOrder); + const int count = firstTabOrder.count(); + Q_ASSERT(count == secondTabOrder.count()); + Q_ASSERT(count > 1); + + QWidget w; + w.setObjectName("MainWidget"); + QVBoxLayout* layout = new QVBoxLayout(); + w.setLayout(layout); + + QList<QComboBox *> boxes; + for (int i = 0; i < count; ++i) { + auto box = new QComboBox; + box->setObjectName("ComboBox " + QString::number(i)); + if (editableAtBeginning) { + box->setEditable(true); + box->lineEdit()->setObjectName("LineEdit " + QString::number(i)); + } + boxes.append(box); + layout->addWidget(box); + } + layout->addStretch(); + +#define COMPARE(seq)\ + setTabOrder(boxes, seq);\ + QCOMPARE(realFocusChain(boxes, seq), expectedFocusChain(boxes, seq)) + + COMPARE(firstTabOrder); + + if (!editableAtBeginning) { + for (auto *box : boxes) + box->setEditable(box); + } + + COMPARE(secondTabOrder); + + // Remove the focus proxy of the first combobox's line edit. + QComboBox *box = boxes.at(0); + QLineEdit *lineEdit = box->lineEdit(); + const QWidget *prev = lineEdit->previousInFocusChain(); + const QWidget *next = lineEdit->nextInFocusChain(); + const QWidget *proxy = lineEdit->focusProxy(); + QCOMPARE(proxy, box); + lineEdit->setFocusProxy(nullptr); + QCOMPARE(lineEdit->focusProxy(), nullptr); + QCOMPARE(lineEdit->previousInFocusChain(), prev); + QCOMPARE(lineEdit->nextInFocusChain(), next); + + // Remove first item and check chain consistency + boxes.removeFirst(); + delete box; + + // Create new list with 0 removed and other indexes updated + QList<int> thirdTabOrder(secondTabOrder); + thirdTabOrder.removeIf([](int i){ return i == 0; }); + for (int &i : thirdTabOrder) + --i; + + COMPARE(thirdTabOrder); + +#undef COMPARE +} + void tst_QWidget::tabOrderWithProxy() { if (QGuiApplication::platformName().startsWith(QLatin1String("wayland"), Qt::CaseInsensitive)) @@ -1981,7 +2386,6 @@ void tst_QWidget::tabOrderWithProxy() container.show(); container.activateWindow(); - QApplication::setActiveWindow(&container); QVERIFY(QTest::qWaitForWindowActive(&container)); QTRY_VERIFY(firstEdit->hasFocus()); @@ -2015,6 +2419,81 @@ void tst_QWidget::tabOrderWithProxy() QVERIFY(firstEdit->hasFocus()); } +static QString focusWidgetName() +{ + return QApplication::focusWidget() != nullptr + ? QApplication::focusWidget()->objectName() + : QStringLiteral("No focus widget"); +} + +void tst_QWidget::tabOrderWithProxyDisabled() +{ + Container container; + container.setWindowTitle(QLatin1String(QTest::currentTestFunction())); + + QLineEdit lineEdit1; + lineEdit1.setObjectName("lineEdit1"); + + QWidget containingWidget; + containingWidget.setFocusPolicy(Qt::StrongFocus); + auto *containingLayout = new QVBoxLayout; + QLineEdit lineEdit2; + lineEdit2.setObjectName("lineEdit2"); + QLineEdit lineEdit3; + lineEdit3.setObjectName("lineEdit3"); + containingLayout->addWidget(&lineEdit2); + containingLayout->addWidget(&lineEdit3); + containingWidget.setLayout(containingLayout); + containingWidget.setFocusProxy(&lineEdit2); + lineEdit2.setEnabled(false); + + container.box->addWidget(&lineEdit1); + container.box->addWidget(&containingWidget); + + container.show(); + container.activateWindow(); + + if (!QTest::qWaitForWindowActive(&container)) + QSKIP("Window failed to activate, skipping test"); + + QVERIFY2(lineEdit1.hasFocus(), + qPrintable(focusWidgetName())); + container.tab(); + QVERIFY2(!lineEdit2.hasFocus(), + qPrintable(focusWidgetName())); + QVERIFY2(lineEdit3.hasFocus(), + qPrintable(focusWidgetName())); + container.tab(); + QVERIFY2(lineEdit1.hasFocus(), + qPrintable(focusWidgetName())); + container.backTab(); + QVERIFY2(lineEdit3.hasFocus(), + qPrintable(focusWidgetName())); + container.backTab(); + QVERIFY2(!lineEdit2.hasFocus(), + qPrintable(focusWidgetName())); + QVERIFY2(lineEdit1.hasFocus(), + qPrintable(focusWidgetName())); +} + +//#define DEBUG_FOCUS_CHAIN +static void dumpFocusChain(QWidget *start, bool bForward, const char *desc = nullptr) +{ +#ifdef DEBUG_FOCUS_CHAIN + qDebug() << "Dump focus chain, start:" << start << "isForward:" << bForward << desc; + QWidget *cur = start; + do { + qDebug() << "-" << cur; + auto widgetPrivate = static_cast<QWidgetPrivate *>(qt_widget_private(cur)); + cur = bForward ? widgetPrivate->focus_next : widgetPrivate->focus_prev; + } while (cur != start); +#else + Q_UNUSED(start); + Q_UNUSED(bForward); + Q_UNUSED(desc); +#endif +} + void tst_QWidget::tabOrderWithCompoundWidgets() { if (QGuiApplication::platformName().startsWith(QLatin1String("wayland"), Qt::CaseInsensitive)) @@ -2055,7 +2534,6 @@ void tst_QWidget::tabOrderWithCompoundWidgets() container.show(); container.activateWindow(); - QApplication::setActiveWindow(&container); QVERIFY(QTest::qWaitForWindowActive(&container)); lastEdit->setFocus(); @@ -2106,34 +2584,180 @@ void tst_QWidget::tabOrderWithCompoundWidgets() QVERIFY(lastEdit->hasFocus()); } -static QList<QWidget *> getFocusChain(QWidget *start, bool bForward) +void tst_QWidget::tabOrderWithProxyOutOfOrder() { - QList<QWidget *> ret; - QWidget *cur = start; - do { - ret += cur; - auto widgetPrivate = static_cast<QWidgetPrivate *>(qt_widget_private(cur)); - cur = bForward ? widgetPrivate->focus_next : widgetPrivate->focus_prev; - } while (cur != start); - return ret; + Container container; + container.setWindowTitle(QLatin1String(QTest::currentTestFunction())); + container.setObjectName(QLatin1StringView("Container")); + + // important to create the widgets with parent so that they are + // added to the focus chain already now, and with the buttonBox + // before the outsideButton. + QWidget buttonBox(&container); + buttonBox.setObjectName(QLatin1StringView("buttonBox")); + QPushButton outsideButton(&container); + outsideButton.setObjectName(QLatin1StringView("outsideButton")); + + container.box->addWidget(&outsideButton); + container.box->addWidget(&buttonBox); + QCOMPARE(getFocusChain(&container, true), + QList<QWidget*>({&container, &buttonBox, &outsideButton})); + + // this now adds okButon and cancelButton to the focus chain, + // after the outsideButton - so the outsideButton is in between + // the buttonBox and the children of the buttonBox! + QPushButton okButton(&buttonBox); + okButton.setObjectName("okButton"); + QPushButton cancelButton(&buttonBox); + cancelButton.setObjectName("cancelButton"); + QCOMPARE(getFocusChain(&container, true), + QList<QWidget*>({&container, &buttonBox, &outsideButton, &okButton, &cancelButton})); + + // by setting the okButton as the focusProxy, the outsideButton becomes + // unreachable when navigating the focus chain as the buttonBox is in front + // of, and proxies to the okButton behind the outsideButton. setFocusProxy + // must fix that by moving the buttonBox in front of the first sibling of + // the proxy. + buttonBox.setFocusProxy(&okButton); + QCOMPARE(getFocusChain(&container, true), + QList<QWidget*>({&container, &outsideButton, &buttonBox, &okButton, &cancelButton})); + + container.show(); + container.activateWindow(); + if (!QTest::qWaitForWindowActive(&container)) + QSKIP("Window failed to activate, skipping test"); + + QCOMPARE(QApplication::focusWidget(), &outsideButton); + container.tab(); + QCOMPARE(QApplication::focusWidget(), &okButton); + container.tab(); + QCOMPARE(QApplication::focusWidget(), &cancelButton); + container.tab(); + QCOMPARE(QApplication::focusWidget(), &outsideButton); + + container.backTab(); + QCOMPARE(QApplication::focusWidget(), &cancelButton); + container.backTab(); + QCOMPARE(QApplication::focusWidget(), &okButton); + container.backTab(); + QCOMPARE(QApplication::focusWidget(), &outsideButton); + container.backTab(); + QCOMPARE(QApplication::focusWidget(), &cancelButton); } -//#define DEBUG_FOCUS_CHAIN -static void dumpFocusChain(QWidget *start, bool bForward, const char *desc = nullptr) +static bool isFocusChainConsistent(QWidget *widget) { -#ifdef DEBUG_FOCUS_CHAIN - qDebug() << "Dump focus chain, start:" << start << "isForward:" << bForward << desc; - QWidget *cur = start; - do { - qDebug() << cur; - auto widgetPrivate = static_cast<QWidgetPrivate *>(qt_widget_private(cur)); - cur = bForward ? widgetPrivate->focus_next : widgetPrivate->focus_prev; - } while (cur != start); -#else - Q_UNUSED(start); - Q_UNUSED(bForward); - Q_UNUSED(desc); -#endif + auto forward = getFocusChain(widget, true); + auto backward = getFocusChain(widget, false); + auto logger = qScopeGuard([=]{ + qCritical("Focus chain is not consistent!"); + qWarning() << forward.size() << "forwards: " << forward; + qWarning() << backward.size() << "backwards:" << backward; + }); + // both lists start with the same, the widget + if (forward.takeFirst() != backward.takeFirst()) + return false; + const qsizetype chainLength = forward.size(); + if (backward.size() != chainLength) + return false; + for (qsizetype i = 0; i < chainLength; ++i) { + if (forward.at(i) != backward.at(chainLength - i - 1)) + return false; + } + logger.dismiss(); + return true; +} + +/* + This tests that we end up with consistent and complete chains when we set + the tab order from a widget (the lineEdit) inside a compound (the tabWidget) + to the compound, or visa versa. In that case, QWidget::setTabOrder will walk + the focus chain to the focus child inside the compound to replace the compound + itself when manipulating the tab order. If that last focus child is then + however also the lineEdit, then we must not create an inconsistent or + incomplete loop. + + The tabWidget is seen as a compound because QTabWidget sets the tab bar as + the focus proxy, and it has more widgets inside, like pages, toolbuttons etc. +*/ +void tst_QWidget::tabOrderWithCompoundWidgetsInflection_data() +{ + QTest::addColumn<QByteArrayList>("tabOrder"); + + QTest::addRow("forward") + << QByteArrayList{"dialog", "tabWidget", "lineEdit", "compound", "okButton", "cancelButton"}; + QTest::addRow("backward") + << QByteArrayList{"dialog", "cancelButton", "okButton", "compound", "lineEdit", "tabWidget"}; +} + +void tst_QWidget::tabOrderWithCompoundWidgetsInflection() +{ + QFETCH(const QByteArrayList, tabOrder); + + QDialog dialog; + dialog.setObjectName("dialog"); + QTabWidget *tabWidget = new QTabWidget; + tabWidget->setObjectName("tabWidget"); + tabWidget->setFocusPolicy(Qt::TabFocus); + QWidget *page = new QWidget; + page->setObjectName("page"); + QLineEdit *lineEdit = new QLineEdit; + lineEdit->setObjectName("lineEdit"); + QWidget *compound = new QWidget; + compound->setObjectName("compound"); + compound->setFocusPolicy(Qt::TabFocus); + QPushButton *okButton = new QPushButton("Ok"); + okButton->setObjectName("okButton"); + okButton->setFocusPolicy(Qt::TabFocus); + QPushButton *cancelButton = new QPushButton("Cancel"); + cancelButton->setObjectName("cancelButton"); + cancelButton->setFocusPolicy(Qt::TabFocus); + + QVBoxLayout *pageLayout = new QVBoxLayout; + pageLayout->addWidget(lineEdit); + page->setLayout(pageLayout); + tabWidget->addTab(page, "Tab"); + + QHBoxLayout *compoundLayout = new QHBoxLayout; + compoundLayout->addStretch(); + compoundLayout->addWidget(cancelButton); + compoundLayout->addWidget(okButton); + compound->setFocusProxy(okButton); + compound->setLayout(compoundLayout); + + QVBoxLayout *dialogLayout = new QVBoxLayout; + dialogLayout->addWidget(tabWidget); + dialogLayout->addWidget(compound); + dialog.setLayout(dialogLayout); + + QVERIFY(isFocusChainConsistent(&dialog)); + + QList<QWidget *> expectedFocusChain; + for (qsizetype i = 0; i < tabOrder.size() - 1; ++i) { + QWidget *first = dialog.findChild<QWidget *>(tabOrder.at(i)); + if (!first && tabOrder.at(i) == dialog.objectName()) + first = &dialog; + QVERIFY(first); + if (i == 0) + expectedFocusChain.append(first); + QWidget *second = dialog.findChild<QWidget *>(tabOrder.at(i + 1)); + QVERIFY(second); + expectedFocusChain.append(second); + QWidget::setTabOrder(first, second); + QVERIFY(isFocusChainConsistent(&dialog)); + } + + const auto forwardChain = getFocusChain(&dialog, true); + auto logger = qScopeGuard([=]{ + qCritical("Order of widgets in focus chain not matching:"); + qCritical() << " Actual :" << forwardChain; + qCritical() << " Expected:" << expectedFocusChain; + }); + for (qsizetype i = 0; i < expectedFocusChain.size() - 2; ++i) { + QCOMPARE_LT(forwardChain.indexOf(expectedFocusChain.at(i)), + forwardChain.indexOf(expectedFocusChain.at(i + 1))); + } + logger.dismiss(); } void tst_QWidget::tabOrderWithCompoundWidgetsNoFocusPolicy() @@ -2157,28 +2781,27 @@ void tst_QWidget::tabOrderWithCompoundWidgetsNoFocusPolicy() container.show(); container.activateWindow(); - QApplication::setActiveWindow(&container); if (!QTest::qWaitForWindowActive(&container)) QSKIP("Window failed to activate, skipping test"); QVERIFY2(spinbox1.hasFocus(), - qPrintable(QApplication::focusWidget()->objectName())); + qPrintable(focusWidgetName())); container.tab(); QVERIFY2(!spinbox2.hasFocus(), - qPrintable(QApplication::focusWidget()->objectName())); + qPrintable(focusWidgetName())); QVERIFY2(spinbox3.hasFocus(), - qPrintable(QApplication::focusWidget()->objectName())); + qPrintable(focusWidgetName())); container.tab(); QVERIFY2(spinbox1.hasFocus(), - qPrintable(QApplication::focusWidget()->objectName())); + qPrintable(focusWidgetName())); container.backTab(); QVERIFY2(spinbox3.hasFocus(), - qPrintable(QApplication::focusWidget()->objectName())); + qPrintable(focusWidgetName())); container.backTab(); QVERIFY2(!spinbox2.hasFocus(), - qPrintable(QApplication::focusWidget()->objectName())); + qPrintable(focusWidgetName())); QVERIFY2(spinbox1.hasFocus(), - qPrintable(QApplication::focusWidget()->objectName())); + qPrintable(focusWidgetName())); } void tst_QWidget::tabOrderNoChange() @@ -2263,7 +2886,6 @@ void tst_QWidget::appFocusWidgetWithFocusProxyLater() QLineEdit *lineEdit = new QLineEdit(&window); lineEdit->setFocus(); window.show(); - QApplication::setActiveWindow(&window); QVERIFY(QTest::qWaitForWindowActive(&window)); QCOMPARE(QApplication::focusWidget(), lineEdit); @@ -2291,7 +2913,6 @@ void tst_QWidget::appFocusWidgetWhenLosingFocusProxy() lineEdit->setFocusProxy(lineEditFocusProxy); lineEdit->setFocus(); window.show(); - QApplication::setActiveWindow(&window); QVERIFY(QTest::qWaitForWindowActive(&window)); QCOMPARE(QApplication::focusWidget(), lineEditFocusProxy); QVERIFY(lineEdit->hasFocus()); @@ -2318,7 +2939,6 @@ void tst_QWidget::explicitTabOrderWithComplexWidget() QWidget::setTabOrder(lineEditOne, lineEditTwo); lineEditOne->setFocus(); window.show(); - QApplication::setActiveWindow(&window); QVERIFY(QTest::qWaitForWindowActive(&window)); QTRY_COMPARE(QApplication::focusWidget(), lineEditOne); @@ -2347,7 +2967,6 @@ void tst_QWidget::explicitTabOrderWithSpinBox_QTBUG81097() QWidget::setTabOrder(spinBoxTwo, lineEdit); spinBoxOne->setFocus(); window.show(); - QApplication::setActiveWindow(&window); QVERIFY(QTest::qWaitForWindowActive(&window)); QTRY_COMPARE(QApplication::focusWidget(), spinBoxOne); @@ -2394,6 +3013,24 @@ void tst_QWidget::activation() } #endif // Q_OS_WIN +struct WindowStateChangeWatcher : public QObject +{ + WindowStateChangeWatcher(QWidget *widget) + { + Q_ASSERT(widget->window()->windowHandle()); + widget->window()->windowHandle()->installEventFilter(this); + lastWindowStates = widget->window()->windowHandle()->windowState(); + } + Qt::WindowStates lastWindowStates; +protected: + bool eventFilter(QObject *receiver, QEvent *event) override + { + if (event->type() == QEvent::WindowStateChange) + lastWindowStates = static_cast<QWindow *>(receiver)->windowState(); + return QObject::eventFilter(receiver, event); + } +}; + void tst_QWidget::windowState() { #ifdef Q_OS_MACOS @@ -2407,12 +3044,14 @@ void tst_QWidget::windowState() QPoint pos; QSize size = m_testWidgetSize; - if (QGuiApplicationPrivate::platformIntegration()->defaultWindowState(Qt::Widget) - == Qt::WindowFullScreen) { + const Qt::WindowState defaultWidgetState = + QGuiApplicationPrivate::platformIntegration()->defaultWindowState(Qt::Widget); + if (defaultWidgetState == Qt::WindowFullScreen) size = QGuiApplication::primaryScreen()->size(); - } else { + else if (defaultWidgetState == Qt::WindowMaximized) + size = QGuiApplication::primaryScreen()->availableSize(); + else pos = QPoint(10, 10); - } QWidget widget1; widget1.move(pos); @@ -2527,6 +3166,119 @@ void tst_QWidget::windowState() QTRY_COMPARE(widget1.size(), size); } +// Test propagation of size and state from platform window to QWidget +// Windows and linux/XCB only +void tst_QWidget::resizePropagation() +{ +#if !defined(Q_OS_LINUX) && !defined(Q_OS_WIN) + QSKIP("resizePropagation test is designed for Linux/XCB and Windows only"); +#endif + const bool xcb = (m_platform == QStringLiteral("xcb")); +#ifdef Q_OS_LINUX + if (!xcb) + QSKIP("resizePropagation test is designed for XCB only"); +#endif + + // Windows: + // When a widget is maximized after it has been resized, the widget retains its original size, + // while the window shows maximum size. + // windowStateChanged signal gets fired on a no-op change from/to WindowNoState + + // Initialize widget and signal spy for window handle + QWidget widget; + widget.showMaximized(); + QVERIFY(QTest::qWaitForWindowExposed(&widget)); + QWindow *window = widget.windowHandle(); + QTRY_VERIFY(window); + QSignalSpy spy(window, &QWindow::windowStateChanged); + int count = 0; + + const QSize screenSize = QGuiApplication::primaryScreen()->size(); + const QSize size1 = QSize(screenSize.width() * 0.5, screenSize.height() * 0.5); + const QSize size2 = QSize(screenSize.width() * 0.625, screenSize.height() * 0.833); + + enum CountIncrementCheck {Equal, Greater}; + enum TargetSizeCheck {Fail, Warn}; + auto verifyResize = [&](const QSize &size, Qt::WindowState windowState, + CountIncrementCheck checkCountIncrement, + TargetSizeCheck checkTargetSize) + { + // Capture count of latest async signals + if (checkCountIncrement == Equal) + count = spy.count(); + + // Resize if required + if (size.isValid()) + widget.resize(size); + + // Wait for the widget anyway + QVERIFY(QTest::qWaitForWindowExposed(&widget)); + + // Check signal count and qDebug output for fail analysis + switch (checkCountIncrement) { + case Greater: { + auto logger = qScopeGuard([&](){ + qDebug() << "spy count:" << spy.count() << "previous count:" << count; + }); + QTRY_VERIFY(spy.count() > count); + logger.dismiss(); + count = spy.count(); + } + break; + case Equal: { + auto logger = qScopeGuard([&](){ + qDebug() << spy << widget.windowState() << window->windowState(); + }); + QCOMPARE(spy.count(), count); + logger.dismiss(); + } + break; + } + + // QTRY necessary because state changes are propagated async + QTRY_COMPARE(widget.windowState(), windowState); + QTRY_COMPARE(window->windowState(), windowState); + + // Check target size with fail or warning + switch (checkTargetSize) { + case Fail: + QCOMPARE(widget.size(), window->size()); + break; + case Warn: + if (widget.size() != window->size()) { + qWarning() << m_platform << "size mismtach tolerated. Widget:" + << widget.size() << "Window:" << window->size(); + } + break; + } + }; + + // test state and size consistency of maximized window + verifyResize(QSize(), Qt::WindowMaximized, Equal, Fail); + if (QTest::currentTestFailed()) + return; + + // test state transition, state and size consistency after resize + verifyResize(size1, Qt::WindowNoState, Greater, xcb ? Warn : Fail ); + if (QTest::currentTestFailed()) + return; + + // test unchanged state, state and size consistency after resize + verifyResize(size2, Qt::WindowNoState, Equal, xcb ? Warn : Fail); + if (QTest::currentTestFailed()) + return; + + // test state transition, state and size consistency after maximize + widget.showMaximized(); + verifyResize(QSize(), Qt::WindowMaximized, Greater, xcb ? Fail : Warn); + if (QTest::currentTestFailed()) + return; + +#ifdef Q_OS_WIN + QCOMPARE(widget.size(), size2); +#endif +} + void tst_QWidget::showMaximized() { QWidget plain; @@ -2716,7 +3468,7 @@ void tst_QWidget::resizeEvent() wParent.setWindowTitle(QLatin1String(QTest::currentTestFunction())); wParent.resize(m_testWidgetSize); ResizeWidget wChild(&wParent); - wParent.show(); + QTestPrivate::androidCompatibleShow(&wParent); QVERIFY(QTest::qWaitForWindowExposed(&wParent)); QCOMPARE (wChild.m_resizeEventCount, 1); // initial resize event before paint wParent.hide(); @@ -2725,7 +3477,7 @@ void tst_QWidget::resizeEvent() safeSize.setWidth(639); wChild.resize(safeSize); QCOMPARE (wChild.m_resizeEventCount, 1); - wParent.show(); + QTestPrivate::androidCompatibleShow(&wParent); QCOMPARE (wChild.m_resizeEventCount, 2); } @@ -2733,7 +3485,7 @@ void tst_QWidget::resizeEvent() ResizeWidget wTopLevel; wTopLevel.setWindowTitle(QLatin1String(QTest::currentTestFunction())); wTopLevel.resize(m_testWidgetSize); - wTopLevel.show(); + QTestPrivate::androidCompatibleShow(&wTopLevel); QVERIFY(QTest::qWaitForWindowExposed(&wTopLevel)); QCOMPARE (wTopLevel.m_resizeEventCount, 1); // initial resize event before paint for toplevels wTopLevel.hide(); @@ -2742,7 +3494,7 @@ void tst_QWidget::resizeEvent() safeSize.setWidth(639); wTopLevel.resize(safeSize); QCOMPARE (wTopLevel.m_resizeEventCount, 1); - wTopLevel.show(); + QTestPrivate::androidCompatibleShow(&wTopLevel); QVERIFY(QTest::qWaitForWindowExposed(&wTopLevel)); QCOMPARE (wTopLevel.m_resizeEventCount, 2); } @@ -2818,7 +3570,7 @@ void tst_QWidget::showMinimizedKeepsFocus() child1.setFocusPolicy(Qt::StrongFocus); child2.setFocusPolicy(Qt::StrongFocus); window.show(); - QApplication::setActiveWindow(&window); + QApplicationPrivate::setActiveWindow(&window); QVERIFY(QTest::qWaitForWindowActive(&window)); child2.setFocus(); @@ -2842,7 +3594,6 @@ void tst_QWidget::showMinimizedKeepsFocus() QWidget *child = new QWidget(&window); child->setFocusPolicy(Qt::StrongFocus); window.show(); - QApplication::setActiveWindow(&window); QVERIFY(QTest::qWaitForWindowActive(&window)); child->setFocus(); QTRY_COMPARE(window.focusWidget(), child); @@ -2861,7 +3612,6 @@ void tst_QWidget::showMinimizedKeepsFocus() QWidget *child = new QWidget(&window); child->setFocusPolicy(Qt::StrongFocus); window.show(); - QApplication::setActiveWindow(&window); QVERIFY(QTest::qWaitForWindowActive(&window)); child->setFocus(); QTRY_COMPARE(window.focusWidget(), child); @@ -2881,7 +3631,6 @@ void tst_QWidget::showMinimizedKeepsFocus() QWidget *child = new QWidget(&window); child->setFocusPolicy(Qt::StrongFocus); window.show(); - QApplication::setActiveWindow(&window); QVERIFY(QTest::qWaitForWindowActive(&window)); child->setFocus(); QTRY_COMPARE(window.focusWidget(), child); @@ -2902,7 +3651,6 @@ void tst_QWidget::showMinimizedKeepsFocus() QWidget *child = new QWidget(&window); child->setFocusPolicy(Qt::StrongFocus); window.show(); - QApplication::setActiveWindow(&window); QVERIFY(QTest::qWaitForWindowActive(&window)); child->setFocus(); QTRY_COMPARE(window.focusWidget(), child); @@ -2919,7 +3667,6 @@ void tst_QWidget::showMinimizedKeepsFocus() QTRY_COMPARE(QApplication::focusWidget(), nullptr); window.showNormal(); - QApplication::setActiveWindow(&window); QVERIFY(QTest::qWaitForWindowActive(&window)); #ifdef Q_OS_MACOS if (!macHasAccessToWindowsServer()) @@ -2957,8 +3704,8 @@ void tst_QWidget::reparent() pal2.setColor(childTLW.backgroundRole(), Qt::yellow); childTLW.setPalette(pal2); - parent.show(); - childTLW.show(); + QTestPrivate::androidCompatibleShow(&parent); + QTestPrivate::androidCompatibleShow(&childTLW); QVERIFY(QTest::qWaitForWindowExposed(&parent)); parent.move(parentPosition); @@ -2968,7 +3715,7 @@ void tst_QWidget::reparent() child.setParent(nullptr, child.windowFlags() & ~Qt::WindowType_Mask); child.setGeometry(childPos.x(), childPos.y(), child.width(), child.height()); - child.show(); + QTestPrivate::androidCompatibleShow(&child); #if 0 // QTBUG-26424 if (m_platform == QStringLiteral("xcb")) @@ -2984,7 +3731,7 @@ void tst_QWidget::reparent() void tst_QWidget::setScreen() { const auto screens = QApplication::screens(); - if (screens.count() < 2) + if (screens.size() < 2) QSKIP("This test tests nothing on a machine with a single screen."); QScreen *screen0 = screens.at(0); @@ -3073,109 +3820,111 @@ void tst_QWidget::hideWhenFocusWidgetIsChild() void tst_QWidget::normalGeometry() { -#ifdef Q_OS_MACOS - QSKIP("QTBUG-52974"); -#endif - if (m_platform == QStringLiteral("wayland")) QSKIP("Wayland: This fails. Figure out why."); QWidget parent; + QCOMPARE(parent.normalGeometry(), parent.geometry()); parent.setWindowTitle("NormalGeometry parent"); QWidget *child = new QWidget(&parent); QCOMPARE(parent.normalGeometry(), parent.geometry()); QCOMPARE(child->normalGeometry(), QRect()); - const QRect testGeom = QRect(m_availableTopLeft + QPoint(100 ,100), m_testWidgetSize); - parent.setGeometry(testGeom); + parent.setGeometry(QRect(m_availableTopLeft + QPoint(100 ,100), m_testWidgetSize)); parent.showNormal(); QVERIFY(QTest::qWaitForWindowExposed(&parent)); - QApplication::processEvents(); + WindowStateChangeWatcher stateChangeWatcher(&parent); - QRect geom = parent.geometry(); - // ### the window manager places the top-left corner at - // ### 100,100... making geom something like 102,124 (offset by - // ### the frame/frame)... this indicates a rather large different - // ### between how X11 and Windows works - // QCOMPARE(geom, QRect(100, 100, 200, 200)); - QCOMPARE(parent.normalGeometry(), geom); + const QRect normalGeometry = parent.geometry(); + // We can't make any assumptions about the actual geometry compared to the + // requested geometry. In this test, we only care about normalGeometry. + QCOMPARE(parent.normalGeometry(), normalGeometry); parent.setWindowState(parent.windowState() ^ Qt::WindowMaximized); QTRY_VERIFY(parent.windowState() & Qt::WindowMaximized); - QTRY_VERIFY(parent.geometry() != geom); - QTRY_COMPARE(parent.normalGeometry(), geom); + QTRY_COMPARE(stateChangeWatcher.lastWindowStates, parent.windowState()); + QTRY_VERIFY(parent.geometry() != normalGeometry); + QTRY_COMPARE(parent.normalGeometry(), normalGeometry); parent.setWindowState(parent.windowState() ^ Qt::WindowMaximized); QTRY_VERIFY(!(parent.windowState() & Qt::WindowMaximized)); - QTRY_COMPARE(parent.geometry(), geom); - QTRY_COMPARE(parent.normalGeometry(), geom); + QTRY_COMPARE(stateChangeWatcher.lastWindowStates, parent.windowState()); + QTRY_COMPARE(parent.geometry(), normalGeometry); + QTRY_COMPARE(parent.normalGeometry(), normalGeometry); parent.showMaximized(); - QTRY_VERIFY(parent.windowState() & Qt::WindowMaximized); - QTRY_VERIFY(parent.geometry() != geom); - QCOMPARE(parent.normalGeometry(), geom); + QTRY_VERIFY(parent.windowHandle()->windowState() & Qt::WindowMaximized); + QTRY_COMPARE(stateChangeWatcher.lastWindowStates, parent.windowState()); + QTRY_VERIFY(parent.geometry() != normalGeometry); + QCOMPARE(parent.normalGeometry(), normalGeometry); parent.showNormal(); QTRY_VERIFY(!(parent.windowState() & Qt::WindowMaximized)); - QTRY_COMPARE(parent.geometry(), geom); - QCOMPARE(parent.normalGeometry(), geom); + QTRY_COMPARE(stateChangeWatcher.lastWindowStates, parent.windowState()); + QTRY_COMPARE(parent.geometry(), normalGeometry); + QCOMPARE(parent.normalGeometry(), normalGeometry); + + parent.setWindowState(parent.windowState() ^ Qt::WindowFullScreen); + QTRY_VERIFY(parent.windowState() & Qt::WindowFullScreen); + QTRY_COMPARE(stateChangeWatcher.lastWindowStates, parent.windowState()); + QTRY_VERIFY(parent.geometry() != normalGeometry); + QTRY_COMPARE(parent.normalGeometry(), normalGeometry); + + parent.setWindowState(Qt::WindowNoState); + QTRY_VERIFY(!(parent.windowState() & Qt::WindowFullScreen)); + QTRY_COMPARE(stateChangeWatcher.lastWindowStates, parent.windowState()); + QTRY_COMPARE(parent.geometry(), normalGeometry); + QTRY_COMPARE(parent.normalGeometry(), normalGeometry); + + parent.showFullScreen(); + QTRY_VERIFY(parent.window()->windowState() & Qt::WindowFullScreen); + QTRY_COMPARE(stateChangeWatcher.lastWindowStates, parent.windowState()); + QTRY_VERIFY(parent.geometry() != normalGeometry); + QTRY_COMPARE(parent.normalGeometry(), normalGeometry); + + parent.showNormal(); + QTRY_VERIFY(!(parent.windowHandle()->windowState() & Qt::WindowFullScreen)); + QTRY_COMPARE(stateChangeWatcher.lastWindowStates, parent.windowState()); + QTRY_COMPARE(parent.geometry(), normalGeometry); + QTRY_COMPARE(parent.normalGeometry(), normalGeometry); - parent.setWindowState(parent.windowState() ^ Qt::WindowMinimized); - QTest::qWait(10); - parent.setWindowState(parent.windowState() ^ Qt::WindowMaximized); - QTest::qWait(10); if (m_platform == QStringLiteral("xcb")) QSKIP("QTBUG-26424"); - QTRY_VERIFY(parent.windowState() & (Qt::WindowMinimized|Qt::WindowMaximized)); + + parent.setWindowState(parent.windowState() ^ Qt::WindowMaximized); + QTRY_VERIFY(stateChangeWatcher.lastWindowStates & Qt::WindowMaximized); + parent.setWindowState(parent.windowState() ^ Qt::WindowMinimized); + QTRY_VERIFY(stateChangeWatcher.lastWindowStates & Qt::WindowMinimized); + + QTRY_COMPARE(parent.windowState() & (Qt::WindowMinimized|Qt::WindowMaximized), Qt::WindowMinimized|Qt::WindowMaximized); + QTRY_VERIFY(stateChangeWatcher.lastWindowStates & (Qt::WindowMinimized|Qt::WindowMaximized)); // ### when minimized and maximized at the same time, the geometry // ### does *NOT* have to be the normal geometry, it could be the // ### maximized geometry. // QCOMPARE(parent.geometry(), geom); - QTRY_COMPARE(parent.normalGeometry(), geom); + QTRY_COMPARE(parent.normalGeometry(), normalGeometry); parent.setWindowState(parent.windowState() ^ Qt::WindowMinimized); - QTest::qWait(10); QTRY_VERIFY(!(parent.windowState() & Qt::WindowMinimized)); QTRY_VERIFY(parent.windowState() & Qt::WindowMaximized); - QTRY_VERIFY(parent.geometry() != geom); - QTRY_COMPARE(parent.normalGeometry(), geom); + QTRY_COMPARE(stateChangeWatcher.lastWindowStates, parent.windowState()); + QTRY_VERIFY(parent.geometry() != normalGeometry); + QTRY_COMPARE(parent.normalGeometry(), normalGeometry); parent.setWindowState(parent.windowState() ^ Qt::WindowMaximized); - QTest::qWait(10); QTRY_VERIFY(!(parent.windowState() & Qt::WindowMaximized)); - QTRY_COMPARE(parent.geometry(), geom); - QTRY_COMPARE(parent.normalGeometry(), geom); - - parent.setWindowState(parent.windowState() ^ Qt::WindowFullScreen); - QTest::qWait(10); - QTRY_VERIFY(parent.windowState() & Qt::WindowFullScreen); - QTRY_VERIFY(parent.geometry() != geom); - QTRY_COMPARE(parent.normalGeometry(), geom); - - parent.setWindowState(parent.windowState() ^ Qt::WindowFullScreen); - QTest::qWait(10); - QVERIFY(!(parent.windowState() & Qt::WindowFullScreen)); - QTRY_COMPARE(parent.geometry(), geom); - QTRY_COMPARE(parent.normalGeometry(), geom); - - parent.showFullScreen(); - QTest::qWait(10); - QTRY_VERIFY(parent.windowState() & Qt::WindowFullScreen); - QTRY_VERIFY(parent.geometry() != geom); - QTRY_COMPARE(parent.normalGeometry(), geom); - - parent.showNormal(); - QTest::qWait(10); - QTRY_VERIFY(!(parent.windowState() & Qt::WindowFullScreen)); - QTRY_COMPARE(parent.geometry(), geom); - QTRY_COMPARE(parent.normalGeometry(), geom); + QTRY_COMPARE(stateChangeWatcher.lastWindowStates, parent.windowState()); + QTRY_COMPARE(parent.geometry(), normalGeometry); + QTRY_COMPARE(parent.normalGeometry(), normalGeometry); parent.showNormal(); + stateChangeWatcher.lastWindowStates = {}; parent.setWindowState(Qt:: WindowFullScreen | Qt::WindowMaximized); parent.setWindowState(Qt::WindowMinimized | Qt:: WindowFullScreen | Qt::WindowMaximized); parent.setWindowState(Qt:: WindowFullScreen | Qt::WindowMaximized); - QTest::qWait(10); - QTRY_COMPARE(parent.normalGeometry(), geom); + // the actual window will be either fullscreen or maximized + QTRY_VERIFY(stateChangeWatcher.lastWindowStates & (Qt:: WindowFullScreen | Qt::WindowMaximized)); + QTRY_COMPARE(parent.normalGeometry(), normalGeometry); } void tst_QWidget::setGeometry() @@ -3184,9 +3933,8 @@ void tst_QWidget::setGeometry() tlw.setWindowTitle(QLatin1String(QTest::currentTestFunction())); QWidget child(&tlw); - const QPoint topLeft = QGuiApplication::primaryScreen()->availableGeometry().topLeft(); const QSize initialSize = 2 * m_testWidgetSize; - QRect tr(topLeft + QPoint(100,100), initialSize); + QRect tr(m_availableTopLeft + QPoint(100,100), initialSize); QRect cr(50,50,50,50); tlw.setGeometry(tr); child.setGeometry(cr); @@ -3195,15 +3943,14 @@ void tst_QWidget::setGeometry() QCOMPARE(child.geometry(), cr); tlw.setParent(nullptr, Qt::Window|Qt::FramelessWindowHint); - tr = QRect(topLeft, initialSize / 2); + tr = QRect(m_availableTopLeft, initialSize / 2); tlw.setGeometry(tr); QCOMPARE(tlw.geometry(), tr); tlw.showNormal(); - QTest::qWait(50); - if (tlw.frameGeometry() != tlw.geometry()) + if (!QTest::qWaitFor([&tlw]{ return tlw.frameGeometry() == tlw.geometry(); })) QSKIP("Your window manager is too broken for this test"); - if (m_platform == QStringLiteral("xcb")) - QSKIP("QTBUG-26424"); + if (m_platform == QStringLiteral("xcb") && tlw.geometry() != tr) + QEXPECT_FAIL("", "QTBUG-26424", Continue); QCOMPARE(tlw.geometry(), tr); } @@ -3378,9 +4125,9 @@ void tst_QWidget::raise() QObjectList list1{child1, child2, child3, child4}; QCOMPARE(parentPtr->children(), list1); - QCOMPARE(allChildren.count(), list1.count()); + QCOMPARE(allChildren.size(), list1.size()); - for (UpdateWidget *child : qAsConst(allChildren)) { + for (UpdateWidget *child : std::as_const(allChildren)) { int expectedPaintEvents = child == child4 ? 1 : 0; if (expectedPaintEvents == 0) { QCOMPARE(child->numPaintEvents, 0); @@ -3394,9 +4141,10 @@ void tst_QWidget::raise() for (int i = 0; i < 5; ++i) child2->raise(); - QTest::qWait(50); + QVERIFY(QTest::qWaitForWindowExposed(child2)); + QApplication::processEvents(); // process events that could be triggered by raise(); - for (UpdateWidget *child : qAsConst(allChildren)) { + for (UpdateWidget *child : std::as_const(allChildren)) { int expectedPaintEvents = child == child2 ? 1 : 0; int expectedZOrderChangeEvents = child == child2 ? 1 : 0; QTRY_COMPARE(child->numPaintEvents, expectedPaintEvents); @@ -3423,15 +4171,17 @@ void tst_QWidget::raise() onTop->show(); QVERIFY(QTest::qWaitForWindowExposed(&topLevel)); QTRY_VERIFY(onTop->numPaintEvents > 0); + QApplication::processEvents(); // process remaining paint events if there's more than one onTop->reset(); // Reset all the children. - for (UpdateWidget *child : qAsConst(allChildren)) + for (UpdateWidget *child : std::as_const(allChildren)) child->reset(); for (int i = 0; i < 5; ++i) child3->raise(); - QTest::qWait(50); + QVERIFY(QTest::qWaitForWindowExposed(child3)); + QApplication::processEvents(); // process events that could be triggered by raise(); QCOMPARE(onTop->numPaintEvents, 0); QCOMPARE(onTop->numZOrderChangeEvents, 0); @@ -3439,7 +4189,7 @@ void tst_QWidget::raise() QObjectList list3{child1, child4, child2, child3}; QCOMPARE(parent->children(), list3); - for (UpdateWidget *child : qAsConst(allChildren)) { + for (UpdateWidget *child : std::as_const(allChildren)) { int expectedPaintEvents = 0; int expectedZOrderChangeEvents = child == child3 ? 1 : 0; QTRY_COMPARE(child->numPaintEvents, expectedPaintEvents); @@ -3477,9 +4227,9 @@ void tst_QWidget::lower() QObjectList list1{child1, child2, child3, child4}; QCOMPARE(parent->children(), list1); - QCOMPARE(allChildren.count(), list1.count()); + QCOMPARE(allChildren.size(), list1.size()); - for (UpdateWidget *child : qAsConst(allChildren)) { + for (UpdateWidget *child : std::as_const(allChildren)) { int expectedPaintEvents = child == child4 ? 1 : 0; if (expectedPaintEvents == 0) { QCOMPARE(child->numPaintEvents, 0); @@ -3496,7 +4246,7 @@ void tst_QWidget::lower() QTest::qWait(100); - for (UpdateWidget *child : qAsConst(allChildren)) { + for (UpdateWidget *child : std::as_const(allChildren)) { int expectedPaintEvents = child == child3 ? 1 : 0; int expectedZOrderChangeEvents = child == child4 ? 1 : 0; QTRY_COMPARE(child->numZOrderChangeEvents, expectedZOrderChangeEvents); @@ -3542,7 +4292,7 @@ void tst_QWidget::stackUnder() QObjectList list1{child1, child2, child3, child4}; QCOMPARE(parent->children(), list1); - for (UpdateWidget *child : qAsConst(allChildren)) { + for (UpdateWidget *child : std::as_const(allChildren)) { int expectedPaintEvents = child == child4 ? 1 : 0; #if defined(Q_OS_WIN) || defined(Q_OS_MACOS) if (expectedPaintEvents == 1 && child->numPaintEvents == 2) @@ -3560,7 +4310,7 @@ void tst_QWidget::stackUnder() QObjectList list2{child1, child4, child2, child3}; QCOMPARE(parent->children(), list2); - for (UpdateWidget *child : qAsConst(allChildren)) { + for (UpdateWidget *child : std::as_const(allChildren)) { int expectedPaintEvents = child == child3 ? 1 : 0; int expectedZOrderChangeEvents = child == child4 ? 1 : 0; QTRY_COMPARE(child->numPaintEvents, expectedPaintEvents); @@ -3575,7 +4325,7 @@ void tst_QWidget::stackUnder() QObjectList list3{child4, child2, child1, child3}; QCOMPARE(parent->children(), list3); - for (UpdateWidget *child : qAsConst(allChildren)) { + for (UpdateWidget *child : std::as_const(allChildren)) { int expectedZOrderChangeEvents = child == child1 ? 1 : 0; if (child == child3) { #ifndef Q_OS_MACOS @@ -3697,6 +4447,13 @@ void tst_QWidget::saveRestoreGeometry() QVERIFY(QTest::qWaitForWindowExposed(&widget)); QApplication::processEvents(); + + /* --------------------------------------------------------------------- + * This test function is likely to flake when debugged with Qt Creator. + * (29px offset making the following QTRY_VERIFY2 fail) + * --------------------------------------------------------------------- + */ + QTRY_VERIFY2(HighDpi::fuzzyCompare(widget.pos(), position, m_fuzz), qPrintable(HighDpi::msgPointMismatch(widget.pos(), position))); QCOMPARE(widget.size(), size); @@ -3852,10 +4609,9 @@ void tst_QWidget::restoreVersion1Geometry() const Qt::WindowStates WindowStateMask = Qt::WindowFullScreen | Qt::WindowMaximized | Qt::WindowMinimized; QFile f(fileName); - QVERIFY(f.exists()); - f.open(QIODevice::ReadOnly); + QVERIFY(f.open(QIODevice::ReadOnly)); const QByteArray savedGeometry = f.readAll(); - QCOMPARE(savedGeometry.count(), 46); + QCOMPARE(savedGeometry.size(), 46); f.close(); QWidget widget; @@ -3909,6 +4665,68 @@ void tst_QWidget::restoreVersion1Geometry() #endif } +void tst_QWidget::restoreGeometryAfterScreenChange_data() +{ + QTest::addColumn<ScreenPosition>("screenPosition"); + QTest::addColumn<int>("deltaWidth"); + QTest::addColumn<int>("deltaHeight"); + QTest::addColumn<int>("frameMargin"); + QTest::addColumn<bool>("outside"); + + QTest::newRow("offAboveLarge") << ScreenPosition::OffAbove << 200 << 250 << 20 << true; + QTest::newRow("fitting") << ScreenPosition::Contained << 80 << 80 << 20 << false; + QTest::newRow("offRightWide") << ScreenPosition::OffRight << 150 << 80 << 20 << false; + QTest::newRow("offLeftFitting") << ScreenPosition::OffLeft << 70 << 70 << 20 << true; + QTest::newRow("offBelowHigh") << ScreenPosition::OffBelow << 80 << 200 << 20 << false; +} + +void tst_QWidget::restoreGeometryAfterScreenChange() +{ + const QList<QScreen *> &screens = QApplication::screens(); + QVERIFY2(!screens.isEmpty(), "No screens found."); + const QRect screenGeometry = screens.at(0)->geometry(); + + QFETCH(ScreenPosition, screenPosition); + QFETCH(int, deltaWidth); + QFETCH(int, deltaHeight); + QFETCH(int, frameMargin); + QFETCH(bool, outside); + + QRect restoredGeometry = screenGeometry; + restoredGeometry.setHeight(screenGeometry.height() * deltaHeight / 100); + restoredGeometry.setWidth(screenGeometry.width() * deltaWidth / 100); + const float moveMargin = outside ? 1.2 : 0.75; + + switch (screenPosition) { + case ScreenPosition::OffLeft: + restoredGeometry.setLeft(restoredGeometry.width() * (-moveMargin)); + break; + case ScreenPosition::OffAbove: + restoredGeometry.setTop(restoredGeometry.height() * (-moveMargin)); + break; + case ScreenPosition::OffRight: + restoredGeometry.setRight(restoredGeometry.width() * moveMargin); + break; + case ScreenPosition::OffBelow: + restoredGeometry.setBottom(restoredGeometry.height() * moveMargin); + break; + case ScreenPosition::Contained: + break; + } + + // If restored geometry fits into screen and has not been moved, + // it is changed only by frame margin plus one pixel at each edge + const QRect originalGeometry = restoredGeometry.adjusted(1, frameMargin + 1, 1, frameMargin + 1); + + QWidgetPrivate::checkRestoredGeometry(screenGeometry, &restoredGeometry, frameMargin); + + if (deltaHeight < 100 && deltaWidth < 100 && screenPosition == ScreenPosition::Contained) + QCOMPARE(originalGeometry, restoredGeometry); + + // new geometry has to fit on the screen + QVERIFY(screenGeometry.contains(restoredGeometry)); +} + void tst_QWidget::widgetAt() { #ifdef Q_OS_MACOS @@ -4067,7 +4885,8 @@ void tst_QWidget::testDeletionInEventHandlers() w = new Widget; w->show(); w->deleteThis = true; - QMouseEvent me(QEvent::MouseButtonRelease, QPoint(1, 1), Qt::LeftButton, Qt::LeftButton, Qt::KeyboardModifiers()); + QMouseEvent me(QEvent::MouseButtonRelease, QPoint(1, 1), w->mapToGlobal(QPoint(1, 1)), + Qt::LeftButton, Qt::LeftButton, Qt::KeyboardModifiers()); qApp->notify(w, &me); QVERIFY(w.isNull()); delete w; @@ -4106,7 +4925,8 @@ void tst_QWidget::testDeletionInEventHandlers() w->setMouseTracking(true); w->show(); w->deleteThis = true; - QMouseEvent me2 = QMouseEvent(QEvent::MouseMove, QPoint(0, 0), Qt::NoButton, Qt::NoButton, Qt::NoModifier); + QMouseEvent me2 = QMouseEvent(QEvent::MouseMove, QPoint(0, 0), w->mapToGlobal(QPoint(0, 0)), + Qt::NoButton, Qt::NoButton, Qt::NoModifier); QApplication::sendEvent(w, &me2); QVERIFY(w.isNull()); delete w; @@ -4172,22 +4992,20 @@ class StaticWidget : public QWidget Q_OBJECT public: bool partial = false; - bool gotPaintEvent = false; QRegion paintedRegion; - explicit StaticWidget(QWidget *parent = nullptr) : QWidget(parent) + explicit StaticWidget(const QPalette &palette, QWidget *parent = nullptr) : QWidget(parent) { setAttribute(Qt::WA_StaticContents); setAttribute(Qt::WA_OpaquePaintEvent); - setPalette(Qt::red); // Make sure we have an opaque palette. + setPalette(palette); setAutoFillBackground(true); } void paintEvent(QPaintEvent *e) override { paintedRegion += e->region(); - gotPaintEvent = true; -// qDebug() << "paint" << e->region(); + ++paintEvents; // Look for a full update, set partial to false if found. for (QRect r : e->region()) { partial = (r != rect()); @@ -4195,107 +5013,114 @@ public: break; } } + + // Wait timeout ms until at least one paint event has been consumed + // and the counter is no longer increasing. + // => making sure to consume multiple paint events relating to one operation + // before returning true. + bool waitForPaintEvent(int timeout = 100) + { + QDeadlineTimer deadline(timeout); + int count = -1; + while (!deadline.hasExpired() && count != paintEvents) { + count = paintEvents; + QCoreApplication::processEvents(); + if (count == paintEvents && count > 0) { + paintEvents = 0; + return true; + } + } + paintEvents = 0; + return false; + } +private: + int paintEvents = 0; }; /* Test that widget resizes and moves can be done with minimal repaints when WA_StaticContents - and WA_OpaquePaintEvent is set. Test is mac-only for now. + and WA_OpaquePaintEvent is set. */ void tst_QWidget::optimizedResizeMove() { - if (m_platform == QStringLiteral("wayland")) - QSKIP("Wayland: This fails. Figure out why."); + const bool wayland = QGuiApplication::platformName().startsWith(QLatin1String("wayland"), Qt::CaseInsensitive); + QWidget parent; - parent.setWindowTitle(QLatin1String(QTest::currentTestFunction())); + parent.setPalette(simplePalette()); + parent.setWindowTitle(QTest::currentTestFunction()); parent.resize(400, 400); - StaticWidget staticWidget(&parent); - staticWidget.gotPaintEvent = false; + StaticWidget staticWidget(simplePalette(), &parent); staticWidget.move(150, 150); staticWidget.resize(150, 150); parent.show(); QVERIFY(QTest::qWaitForWindowExposed(&parent)); - QTRY_VERIFY(staticWidget.gotPaintEvent); + QVERIFY(staticWidget.waitForPaintEvent()); - staticWidget.gotPaintEvent = false; staticWidget.move(staticWidget.pos() + QPoint(10, 10)); - QTest::qWait(20); - QCOMPARE(staticWidget.gotPaintEvent, false); + if (!wayland) { + QVERIFY(!staticWidget.waitForPaintEvent()); + } else { + if (staticWidget.waitForPaintEvent()) + QSKIP("Wayland is not optimising paint events. Skipping test."); + } - staticWidget.gotPaintEvent = false; staticWidget.move(staticWidget.pos() + QPoint(-10, -10)); - QTest::qWait(20); - QCOMPARE(staticWidget.gotPaintEvent, false); + QVERIFY(!staticWidget.waitForPaintEvent()); - staticWidget.gotPaintEvent = false; staticWidget.move(staticWidget.pos() + QPoint(-10, 10)); - QTest::qWait(20); - QCOMPARE(staticWidget.gotPaintEvent, false); + QVERIFY(!staticWidget.waitForPaintEvent()); - staticWidget.gotPaintEvent = false; staticWidget.resize(staticWidget.size() + QSize(10, 10)); - QTRY_VERIFY(staticWidget.gotPaintEvent); + QVERIFY(staticWidget.waitForPaintEvent()); QCOMPARE(staticWidget.partial, true); - staticWidget.gotPaintEvent = false; staticWidget.resize(staticWidget.size() + QSize(-10, -10)); - QTest::qWait(20); - QCOMPARE(staticWidget.gotPaintEvent, false); + QVERIFY(!staticWidget.waitForPaintEvent()); - staticWidget.gotPaintEvent = false; staticWidget.resize(staticWidget.size() + QSize(10, -10)); - QTRY_VERIFY(staticWidget.gotPaintEvent); + QVERIFY(staticWidget.waitForPaintEvent()); QCOMPARE(staticWidget.partial, true); - staticWidget.gotPaintEvent = false; staticWidget.move(staticWidget.pos() + QPoint(10, 10)); staticWidget.resize(staticWidget.size() + QSize(-10, -10)); - QTest::qWait(20); - QCOMPARE(staticWidget.gotPaintEvent, false); + QVERIFY(!staticWidget.waitForPaintEvent()); - staticWidget.gotPaintEvent = false; staticWidget.move(staticWidget.pos() + QPoint(10, 10)); staticWidget.resize(staticWidget.size() + QSize(10, 10)); - QTRY_VERIFY(staticWidget.gotPaintEvent); + QVERIFY(staticWidget.waitForPaintEvent()); QCOMPARE(staticWidget.partial, true); - staticWidget.gotPaintEvent = false; staticWidget.move(staticWidget.pos() + QPoint(-10, -10)); staticWidget.resize(staticWidget.size() + QSize(-10, -10)); - QTest::qWait(20); - QCOMPARE(staticWidget.gotPaintEvent, false); + QVERIFY(!staticWidget.waitForPaintEvent()); staticWidget.setAttribute(Qt::WA_StaticContents, false); - staticWidget.gotPaintEvent = false; staticWidget.move(staticWidget.pos() + QPoint(-10, -10)); staticWidget.resize(staticWidget.size() + QSize(-10, -10)); - QTRY_VERIFY(staticWidget.gotPaintEvent); + QVERIFY(staticWidget.waitForPaintEvent()); QCOMPARE(staticWidget.partial, false); staticWidget.setAttribute(Qt::WA_StaticContents, true); staticWidget.setAttribute(Qt::WA_StaticContents, false); - staticWidget.gotPaintEvent = false; staticWidget.move(staticWidget.pos() + QPoint(10, 10)); - QTest::qWait(20); - QCOMPARE(staticWidget.gotPaintEvent, false); + QVERIFY(!staticWidget.waitForPaintEvent()); staticWidget.setAttribute(Qt::WA_StaticContents, true); } void tst_QWidget::optimizedResize_topLevel() { - if (QGuiApplication::platformName().startsWith(QLatin1String("wayland"), Qt::CaseInsensitive)) - QSKIP("Wayland: This fails. Figure out why."); + const bool wayland = QGuiApplication::platformName().startsWith(QLatin1String("wayland"), Qt::CaseInsensitive); if (QHighDpiScaling::isActive()) QSKIP("Skip due to rounding errors in the regions."); - StaticWidget topLevel; + StaticWidget topLevel(simplePalette()); + topLevel.setPalette(simplePalette()); topLevel.setWindowTitle(QLatin1String(QTest::currentTestFunction())); - topLevel.gotPaintEvent = false; topLevel.show(); QVERIFY(QTest::qWaitForWindowExposed(&topLevel)); - QTRY_VERIFY(topLevel.gotPaintEvent); + QVERIFY(topLevel.waitForPaintEvent()); - topLevel.gotPaintEvent = false; topLevel.partial = false; topLevel.paintedRegion = QRegion(); @@ -4320,10 +5145,15 @@ void tst_QWidget::optimizedResize_topLevel() QRegion expectedUpdateRegion(topLevel.rect()); expectedUpdateRegion -= QRect(QPoint(), topLevel.size() - QSize(10, 10)); - QTRY_VERIFY(topLevel.gotPaintEvent); + QVERIFY(topLevel.waitForPaintEvent()); if (m_platform == QStringLiteral("xcb") || m_platform == QStringLiteral("offscreen")) QSKIP("QTBUG-26424"); - QCOMPARE(topLevel.partial, true); + if (!wayland) { + QCOMPARE(topLevel.partial, true); + } else { + if (!topLevel.partial) + QSKIP("Wayland does repaint partially. Skipping test."); + } QCOMPARE(topLevel.paintedRegion, expectedUpdateRegion); } @@ -4493,7 +5323,7 @@ protected: } public: QList<WId> m_winIdList; - int winIdChangeEventCount() const { return m_winIdList.count(); } + int winIdChangeEventCount() const { return m_winIdList.size(); } }; class CreateDestroyWidget : public WinIdChangeWidget @@ -4548,6 +5378,84 @@ void tst_QWidget::createAndDestroy() QVERIFY(widget.internalWinId()); } +void tst_QWidget::eventsAndAttributesOnDestroy() +{ + // The events and attributes when destroying a widget should + // include those of hiding the widget. + + CreateDestroyWidget widget; + EventSpy<QWidget> showEventSpy(&widget, QEvent::Show); + EventSpy<QWidget> hideEventSpy(&widget, QEvent::Hide); + + QCOMPARE(widget.testAttribute(Qt::WA_WState_Created), false); + QCOMPARE(widget.testAttribute(Qt::WA_WState_Visible), false); + QCOMPARE(widget.testAttribute(Qt::WA_Mapped), false); + + widget.show(); + QCOMPARE(widget.testAttribute(Qt::WA_WState_Created), true); + QCOMPARE(widget.testAttribute(Qt::WA_WState_Visible), true); + QTRY_COMPARE(widget.testAttribute(Qt::WA_Mapped), true); + QCOMPARE(showEventSpy.count(), 1); + QCOMPARE(hideEventSpy.count(), 0); + + widget.hide(); + QCOMPARE(widget.testAttribute(Qt::WA_WState_Created), true); + QCOMPARE(widget.testAttribute(Qt::WA_WState_Visible), false); + QCOMPARE(widget.testAttribute(Qt::WA_Mapped), false); + QCOMPARE(showEventSpy.count(), 1); + QCOMPARE(hideEventSpy.count(), 1); + + widget.show(); + QCOMPARE(widget.testAttribute(Qt::WA_WState_Created), true); + QCOMPARE(widget.testAttribute(Qt::WA_WState_Visible), true); + QTRY_COMPARE(widget.testAttribute(Qt::WA_Mapped), true); + QCOMPARE(showEventSpy.count(), 2); + QCOMPARE(hideEventSpy.count(), 1); + + widget.destroy(); + QCOMPARE(widget.testAttribute(Qt::WA_WState_Created), false); + QCOMPARE(widget.testAttribute(Qt::WA_WState_Visible), false); + QCOMPARE(widget.testAttribute(Qt::WA_Mapped), false); + QCOMPARE(showEventSpy.count(), 2); + QCOMPARE(hideEventSpy.count(), 2); + + const int hideEventsAfterDestroy = hideEventSpy.count(); + + widget.create(); + QCOMPARE(widget.testAttribute(Qt::WA_WState_Created), true); + QCOMPARE(widget.testAttribute(Qt::WA_WState_Visible), false); + QCOMPARE(widget.testAttribute(Qt::WA_Mapped), false); + QCOMPARE(showEventSpy.count(), 2); + QCOMPARE(hideEventSpy.count(), hideEventsAfterDestroy); + + QWidgetPrivate::get(&widget)->setVisible(true); + QCOMPARE(widget.testAttribute(Qt::WA_WState_Created), true); + QCOMPARE(widget.testAttribute(Qt::WA_WState_Visible), true); + QTRY_COMPARE(widget.testAttribute(Qt::WA_Mapped), true); + QCOMPARE(showEventSpy.count(), 3); + QCOMPARE(hideEventSpy.count(), hideEventsAfterDestroy); + + // Make sure the destroy that happens when a top level + // is moved to being a child does not prevent the child + // being shown again. + + QWidget parent; + QWidget child; + parent.show(); + QVERIFY(QTest::qWaitForWindowExposed(&parent)); + child.show(); + QVERIFY(QTest::qWaitForWindowExposed(&child)); + + child.setParent(&parent); + QCOMPARE(child.testAttribute(Qt::WA_WState_Created), false); + QCOMPARE(child.testAttribute(Qt::WA_WState_Visible), false); + + child.show(); + QCOMPARE(child.testAttribute(Qt::WA_WState_Created), true); + QCOMPARE(child.testAttribute(Qt::WA_WState_Visible), true); + QVERIFY(QTest::qWaitForWindowExposed(&child)); +} + void tst_QWidget::winIdChangeEvent() { { @@ -4687,6 +5595,75 @@ void tst_QWidget::showNativeChild() QVERIFY(QTest::qWaitForWindowExposed(&topLevel)); } +void tst_QWidget::closeAndShowNativeChild() +{ + QWidget topLevel; + QWidget *nativeChild = new QWidget; + nativeChild->winId(); + nativeChild->setFixedSize(200, 200); + + QHBoxLayout *layout = new QHBoxLayout; + layout->addWidget(nativeChild); + topLevel.setLayout(layout); + + topLevel.show(); + QVERIFY(!nativeChild->isHidden()); + nativeChild->close(); + QVERIFY(nativeChild->isHidden()); + nativeChild->show(); + QVERIFY(!nativeChild->isHidden()); +} + +void tst_QWidget::closeAndShowWithNativeChild() +{ + bool dontCreateNativeWidgetSiblings = QApplication::testAttribute(Qt::AA_DontCreateNativeWidgetSiblings); + auto resetAttribute = qScopeGuard([&]{ + QApplication::setAttribute(Qt::AA_DontCreateNativeWidgetSiblings, dontCreateNativeWidgetSiblings); + }); + QApplication::setAttribute(Qt::AA_DontCreateNativeWidgetSiblings); + + QWidget topLevel; + topLevel.setObjectName("TopLevel"); + QWidget *nativeChild = new QWidget; + nativeChild->setObjectName("NativeChild"); + nativeChild->setFixedSize(200, 200); + QWidget *normalChild = new QWidget; + normalChild->setObjectName("NormalChild"); + normalChild->setFixedSize(200, 200); + + QHBoxLayout *layout = new QHBoxLayout; + layout->addWidget(nativeChild); + layout->addWidget(normalChild); + topLevel.setLayout(layout); + + nativeChild->setAttribute(Qt::WA_NativeWindow); + + QCOMPARE(normalChild->testAttribute(Qt::WA_WState_Hidden), false); + QCOMPARE(normalChild->testAttribute(Qt::WA_WState_ExplicitShowHide), false); + + QCOMPARE(nativeChild->testAttribute(Qt::WA_WState_Hidden), false); + QCOMPARE(nativeChild->testAttribute(Qt::WA_WState_ExplicitShowHide), false); + + topLevel.show(); + QVERIFY(QTest::qWaitForWindowExposed(&topLevel)); + const QSize originalSize = topLevel.size(); + topLevel.close(); + + // all children must have the same state + QCOMPARE(nativeChild->isHidden(), normalChild->isHidden()); + QCOMPARE(nativeChild->isVisible(), normalChild->isVisible()); + QCOMPARE(nativeChild->testAttribute(Qt::WA_WState_Visible), + normalChild->testAttribute(Qt::WA_WState_Visible)); + QCOMPARE(nativeChild->testAttribute(Qt::WA_WState_Hidden), + normalChild->testAttribute(Qt::WA_WState_Hidden)); + QCOMPARE(nativeChild->testAttribute(Qt::WA_WState_ExplicitShowHide), + normalChild->testAttribute(Qt::WA_WState_ExplicitShowHide)); + + topLevel.show(); + QVERIFY(QTest::qWaitForWindowExposed(&topLevel)); + QCOMPARE(topLevel.size(), originalSize); +} + class ShowHideEventWidget : public QWidget { public: @@ -4836,6 +5813,7 @@ void tst_QWidget::update() Q_CHECK_PAINTEVENTS UpdateWidget w; + w.setPalette(simplePalette()); w.setWindowTitle(QLatin1String(QTest::currentTestFunction())); w.resize(100, 100); centerOnScreen(&w); @@ -4849,6 +5827,7 @@ void tst_QWidget::update() w.reset(); UpdateWidget child(&w); + child.setPalette(simplePalette()); child.setGeometry(10, 10, 80, 80); child.show(); @@ -4920,6 +5899,7 @@ void tst_QWidget::update() // overlapping sibling UpdateWidget sibling(&w); + sibling.setPalette(simplePalette()); child.setGeometry(10, 10, 20, 20); sibling.setGeometry(15, 15, 20, 20); sibling.show(); @@ -4999,9 +5979,11 @@ void tst_QWidget::isOpaque() { #ifndef Q_OS_MACOS QWidget w; + w.setPalette(simplePalette()); QVERIFY(::isOpaque(&w)); QWidget child(&w); + child.setPalette(simplePalette()); QVERIFY(!::isOpaque(&child)); child.setAutoFillBackground(true); @@ -5083,11 +6065,11 @@ void tst_QWidget::scroll() const int h = qMin(500, screen->availableGeometry().height() / 2); UpdateWidget updateWidget; + updateWidget.setPalette(simplePalette()); updateWidget.resize(w, h); updateWidget.reset(); - updateWidget.move(QGuiApplication::primaryScreen()->geometry().center() - QPoint(250, 250)); + updateWidget.move(m_availableTopLeft); updateWidget.showNormal(); - QApplication::setActiveWindow(&updateWidget); QVERIFY(QTest::qWaitForWindowActive(&updateWidget)); QVERIFY(updateWidget.numPaintEvents > 0); @@ -5162,34 +6144,74 @@ void tst_QWidget::scrollNativeChildren() #endif // Mac OS -class DestroyedSlotChecker : public QObject +/* + This class is used as a slot object to test two different steps of + QWidget destruction. + + The first step is connecting the destroyed() signal to an object of + this class (through its operator()). In widgets, destroyed() is + emitted by ~QWidget, and not by ~QObject. This means that in our + operator() we expect the sender of the signal to still be a + QWidget. + + The connection realized at the first step means that now there's + an instance of this class owned by the sender object. That instance + is destroyed when the signal/slot connections are destroyed. + That happens in ~QObject, not in ~QWidget. Therefore, in the + destructor of this class, check that indeed the target is no longer + a QWidget but just a QObject. +*/ +class QObjectCastChecker { - Q_OBJECT - public: - bool wasQWidget = false; + explicit QObjectCastChecker(QWidget *target) + : m_target(target) + { + } -public slots: - void destroyedSlot(QObject *object) + ~QObjectCastChecker() { - wasQWidget = (qobject_cast<QWidget *>(object) != nullptr || object->isWidgetType()); + if (!m_target) + return; + + // When ~QObject is reached, check that indeed the object is no + // longer a QWidget. This relies on slots being disconnected in + // ~QObject (and this "slot object" being destroyed there). + QVERIFY(!qobject_cast<QWidget *>(m_target)); + QVERIFY(!dynamic_cast<QWidget *>(m_target)); + QVERIFY(!m_target->isWidgetType()); } -}; -/* - Test that qobject_cast<QWidget*> returns 0 in a slot - connected to QObject::destroyed. -*/ -void tst_QWidget::qobject_castInDestroyedSlot() -{ - DestroyedSlotChecker checker; + QObjectCastChecker(QObjectCastChecker &&other) noexcept + : m_target(std::exchange(other.m_target, nullptr)) + {} - QWidget *widget = new QWidget(); + QT_MOVE_ASSIGNMENT_OPERATOR_IMPL_VIA_MOVE_AND_SWAP(QObjectCastChecker) - QObject::connect(widget, &QObject::destroyed, &checker, &DestroyedSlotChecker::destroyedSlot); - delete widget; + void swap(QObjectCastChecker &other) noexcept + { + qSwap(m_target, other.m_target); + } + + void operator()(QObject *object) const + { + // Test that in a slot connected to destroyed() the emitter is + // still a QWidget. This is because ~QWidget() itself emits the + // signal. + QVERIFY(qobject_cast<QWidget *>(object)); + QVERIFY(dynamic_cast<QWidget *>(object)); + QVERIFY(object->isWidgetType()); + } + +private: + Q_DISABLE_COPY(QObjectCastChecker) + QObject *m_target; +}; - QVERIFY(checker.wasQWidget); +void tst_QWidget::qobject_castOnDestruction() +{ + QWidget widget; + QObject::connect(&widget, &QObject::destroyed, QObjectCastChecker(&widget)); } // Since X11 WindowManager operations are all async, and we have no way to know if the window @@ -5235,7 +6257,7 @@ void tst_QWidget::setWindowGeometry_data() const Qt::WindowFlags windowFlags[] = {Qt::WindowFlags(), Qt::FramelessWindowHint}; const bool skipEmptyRects = (m_platform == QStringLiteral("windows")); - for (Rects l : qAsConst(rects)) { + for (Rects l : std::as_const(rects)) { if (skipEmptyRects) l.removeIf([] (const QRect &r) { return r.isEmpty(); }); const QRect &rect = l.constFirst(); @@ -5254,8 +6276,8 @@ void tst_QWidget::setWindowGeometry_data() void tst_QWidget::setWindowGeometry() { - if (m_platform == QStringLiteral("xcb")) - QSKIP("X11: Skip this test due to Window manager positioning issues."); + if (m_platform == QStringLiteral("xcb") || m_platform.startsWith(QLatin1String("wayland"), Qt::CaseInsensitive)) + QSKIP("X11/Wayland: Skip this test due to Window manager positioning issues."); QFETCH(Rects, rects); QFETCH(int, windowFlags); @@ -5272,7 +6294,7 @@ void tst_QWidget::setWindowGeometry() QCOMPARE(widget.geometry(), rect); // setGeometry() without showing - for (const QRect &r : qAsConst(rects)) { + for (const QRect &r : std::as_const(rects)) { widget.setGeometry(r); QTest::qWait(100); QCOMPARE(widget.geometry(), r); @@ -5298,7 +6320,7 @@ void tst_QWidget::setWindowGeometry() QTRY_COMPARE(widget.geometry(), rect); // setGeometry() while shown - for (const QRect &r : qAsConst(rects)) { + for (const QRect &r : std::as_const(rects)) { widget.setGeometry(r); QTest::qWait(10); QTRY_COMPARE(widget.geometry(), r); @@ -5313,7 +6335,7 @@ void tst_QWidget::setWindowGeometry() QTRY_COMPARE(widget.geometry(), rect); // setGeometry() after hide() - for (const QRect &r : qAsConst(rects)) { + for (const QRect &r : std::as_const(rects)) { widget.setGeometry(r); QTest::qWait(10); QTRY_COMPARE(widget.geometry(), r); @@ -5323,7 +6345,7 @@ void tst_QWidget::setWindowGeometry() QTRY_COMPARE(widget.geometry(), rect); // show() again, geometry() should still be the same - widget.show(); + QTestPrivate::androidCompatibleShow(&widget); if (rect.isValid()) QVERIFY(QTest::qWaitForWindowExposed(&widget)); QTRY_COMPARE(widget.geometry(), rect); @@ -5349,7 +6371,7 @@ void tst_QWidget::setWindowGeometry() QTRY_COMPARE(widget.geometry(), rect); // setGeometry() while shown - for (const QRect &r : qAsConst(rects)) { + for (const QRect &r : std::as_const(rects)) { widget.setGeometry(r); QTest::qWait(10); QTRY_COMPARE(widget.geometry(), r); @@ -5364,7 +6386,7 @@ void tst_QWidget::setWindowGeometry() QTRY_COMPARE(widget.geometry(), rect); // setGeometry() after hide() - for (const QRect &r : qAsConst(rects)) { + for (const QRect &r : std::as_const(rects)) { widget.setGeometry(r); QTest::qWait(10); QTRY_COMPARE(widget.geometry(), r); @@ -5374,7 +6396,7 @@ void tst_QWidget::setWindowGeometry() QTRY_COMPARE(widget.geometry(), rect); // show() again, geometry() should still be the same - widget.show(); + QTestPrivate::androidCompatibleShow(&widget); if (rect.isValid()) QVERIFY(QTest::qWaitForWindowExposed(&widget)); QTest::qWait(10); @@ -5443,7 +6465,7 @@ void tst_QWidget::windowMoveResize() QTRY_COMPARE(widget.size(), rect.size()); // move() without showing - for (const QRect &r : qAsConst(rects)) { + for (const QRect &r : std::as_const(rects)) { widget.move(r.topLeft()); widget.resize(r.size()); QApplication::processEvents(); @@ -5473,7 +6495,7 @@ void tst_QWidget::windowMoveResize() QTRY_COMPARE(widget.size(), rect.size()); // move() while shown - for (const QRect &r : qAsConst(rects)) { + for (const QRect &r : std::as_const(rects)) { // XCB: First resize after show of zero-sized gets wrong win_gravity. const bool expectMoveFail = !windowFlags && ((widget.width() == 0 || widget.height() == 0) && r.width() != 0 && r.height() != 0) @@ -5502,7 +6524,7 @@ void tst_QWidget::windowMoveResize() QTRY_COMPARE(widget.size(), rect.size()); // move() after hide() - for (const QRect &r : qAsConst(rects)) { + for (const QRect &r : std::as_const(rects)) { widget.move(r.topLeft()); widget.resize(r.size()); QApplication::processEvents(); @@ -5522,7 +6544,7 @@ void tst_QWidget::windowMoveResize() QTRY_COMPARE(widget.size(), rect.size()); // show() again, pos() should be the same - widget.show(); + QTestPrivate::androidCompatibleShow(&widget); if (rect.isValid()) QVERIFY(QTest::qWaitForWindowExposed(&widget)); QApplication::processEvents(); @@ -5553,7 +6575,7 @@ void tst_QWidget::windowMoveResize() QTRY_COMPARE(widget.size(), rect.size()); // move() while shown - for (const QRect &r : qAsConst(rects)) { + for (const QRect &r : std::as_const(rects)) { widget.move(r.topLeft()); widget.resize(r.size()); QApplication::processEvents(); @@ -5573,7 +6595,7 @@ void tst_QWidget::windowMoveResize() QTRY_COMPARE(widget.size(), rect.size()); // move() after hide() - for (const QRect &r : qAsConst(rects)) { + for (const QRect &r : std::as_const(rects)) { widget.move(r.topLeft()); widget.resize(r.size()); QApplication::processEvents(); @@ -5593,7 +6615,7 @@ void tst_QWidget::windowMoveResize() QTRY_COMPARE(widget.size(), rect.size()); // show() again, pos() should be the same - widget.show(); + QTestPrivate::androidCompatibleShow(&widget); if (rect.isValid()) QVERIFY(QTest::qWaitForWindowExposed(&widget)); QTest::qWait(10); @@ -5735,8 +6757,7 @@ void tst_QWidget::moveChild() parent.setStyle(style.data()); ColorWidget child(&parent, Qt::Widget, Qt::blue); - parent.setGeometry(QRect(parent.screen()->availableGeometry().topLeft() + QPoint(50, 50), - QSize(200, 200))); + parent.setGeometry(QRect(m_availableTopLeft + QPoint(50, 50), QSize(200, 200))); child.setGeometry(25, 25, 50, 50); #ifndef QT_NO_CURSOR // Try to make sure the cursor is not in a taskbar area to prevent tooltips or window highlighting QCursor::setPos(parent.geometry().topRight() + QPoint(50 , 50)); @@ -5772,16 +6793,18 @@ void tst_QWidget::moveChild() QTRY_COMPARE(pos, child.pos()); QTRY_COMPARE(parent.r, QRegion(oldGeometry) - child.geometry()); -#if !defined(Q_OS_MACOS) + // should be scrolled in backingstore QCOMPARE(child.r, QRegion()); -#endif VERIFY_COLOR(child, child.rect(), child.color); VERIFY_COLOR(parent, QRegion(parent.rect()) - child.geometry(), parent.color); } void tst_QWidget::showAndMoveChild() { +#ifdef ANDROID + QSKIP("Fails on Android due to removed grabWindow(): QTBUG-118849"); +#endif if (m_platform == QStringLiteral("wayland")) QSKIP("Wayland: This fails. Figure out why."); QWidget parent(nullptr, Qt::Window | Qt::WindowStaysOnTopHint); @@ -5799,7 +6822,6 @@ void tst_QWidget::showAndMoveChild() parent.setGeometry(desktopDimensions); parent.setPalette(Qt::red); parent.show(); - QApplication::setActiveWindow(&parent); QVERIFY(QTest::qWaitForWindowActive(&parent)); QWidget child(&parent); @@ -5918,17 +6940,17 @@ void tst_QWidget::multipleToplevelFocusCheck() w2.show(); QVERIFY(QTest::qWaitForWindowExposed(&w2)); - QApplication::setActiveWindow(&w1); w1.activateWindow(); + QApplicationPrivate::setActiveWindow(&w1); QVERIFY(QTest::qWaitForWindowActive(&w1)); - QCOMPARE(QApplication::activeWindow(), static_cast<QWidget *>(&w1)); + QTRY_COMPARE(QApplication::activeWindow(), static_cast<QWidget *>(&w1)); QTest::mouseDClick(&w1, Qt::LeftButton); QTRY_COMPARE(QApplication::focusWidget(), static_cast<QWidget *>(w1.edit)); w2.activateWindow(); - QApplication::setActiveWindow(&w2); + QApplicationPrivate::setActiveWindow(&w2); QVERIFY(QTest::qWaitForWindowActive(&w2)); - QCOMPARE(QApplication::activeWindow(), static_cast<QWidget *>(&w2)); + QTRY_COMPARE(QApplication::activeWindow(), static_cast<QWidget *>(&w2)); QTest::mouseClick(&w2, Qt::LeftButton); QTRY_COMPARE(QApplication::focusWidget(), nullptr); @@ -5936,16 +6958,16 @@ void tst_QWidget::multipleToplevelFocusCheck() QTRY_COMPARE(QApplication::focusWidget(), static_cast<QWidget *>(w2.edit)); w1.activateWindow(); - QApplication::setActiveWindow(&w1); + QApplicationPrivate::setActiveWindow(&w1); QVERIFY(QTest::qWaitForWindowActive(&w1)); - QCOMPARE(QApplication::activeWindow(), static_cast<QWidget *>(&w1)); + QTRY_COMPARE(QApplication::activeWindow(), static_cast<QWidget *>(&w1)); QTest::mouseDClick(&w1, Qt::LeftButton); QTRY_COMPARE(QApplication::focusWidget(), static_cast<QWidget *>(w1.edit)); w2.activateWindow(); - QApplication::setActiveWindow(&w2); + QApplicationPrivate::setActiveWindow(&w2); QVERIFY(QTest::qWaitForWindowActive(&w2)); - QCOMPARE(QApplication::activeWindow(), static_cast<QWidget *>(&w2)); + QTRY_COMPARE(QApplication::activeWindow(), static_cast<QWidget *>(&w2)); QTest::mouseClick(&w2, Qt::LeftButton); QTRY_COMPARE(QApplication::focusWidget(), nullptr); } @@ -6008,7 +7030,7 @@ void tst_QWidget::setFocus() { // move focus to another window testWidget->activateWindow(); - QApplication::setActiveWindow(testWidget.data()); + QApplicationPrivate::setActiveWindow(testWidget.data()); if (testWidget->focusWidget()) testWidget->focusWidget()->clearFocus(); else @@ -6054,7 +7076,7 @@ void tst_QWidget::setFocus() // note: window may be active, but we don't want it to be testWidget->activateWindow(); - QApplication::setActiveWindow(testWidget.data()); + QApplicationPrivate::setActiveWindow(testWidget.data()); if (testWidget->focusWidget()) testWidget->focusWidget()->clearFocus(); else @@ -6230,34 +7252,6 @@ void tst_QWidget::setFocus() } } -template<class T> class EventSpy : public QObject -{ -public: - EventSpy(T *widget, QEvent::Type event) - : m_widget(widget), eventToSpy(event) - { - if (m_widget) - m_widget->installEventFilter(this); - } - - T *widget() const { return m_widget; } - int count() const { return m_count; } - void clear() { m_count = 0; } - -protected: - bool eventFilter(QObject *object, QEvent *event) override - { - if (event->type() == eventToSpy) - ++m_count; - return QObject::eventFilter(object, event); - } - -private: - T *m_widget; - const QEvent::Type eventToSpy; - int m_count = 0; -}; - #ifndef QT_NO_CURSOR void tst_QWidget::setCursor() { @@ -6383,6 +7377,9 @@ void tst_QWidget::setCursor() void tst_QWidget::setToolTip() { + if (QApplication::platformName().startsWith(QLatin1String("wayland"))) + QSKIP("Setting mouse cursor position is not possible on Wayland"); + QWidget widget; widget.resize(200, 200); // Showing the widget is not required for the tooltip event count test @@ -6404,6 +7401,9 @@ void tst_QWidget::setToolTip() QCOMPARE(widget.toolTip(), QString()); QCOMPARE(spy.count(), 2); + const int wakeUpDelay = widget.style()->styleHint(QStyle::SH_ToolTip_WakeUpDelay); + const int fallAsleepDelay = widget.style()->styleHint(QStyle::SH_ToolTip_FallAsleepDelay); + for (int pass = 0; pass < 2; ++pass) { QCursor::setPos(m_safeCursorPos); QScopedPointer<QWidget> popup(new QWidget(nullptr, Qt::Popup)); @@ -6423,12 +7423,12 @@ void tst_QWidget::setToolTip() QWindow *popupWindow = popup->windowHandle(); QTest::qWait(10); QTest::mouseMove(popupWindow, QPoint(25, 25)); - QTest::qWait(900); // delay is 700 + QTest::qWait(wakeUpDelay + 200); QCOMPARE(spy1.count(), 1); QCOMPARE(spy2.count(), 0); if (pass == 0) - QTest::qWait(2200); // delay is 2000 + QTest::qWait(fallAsleepDelay + 200); QTest::mouseMove(popupWindow); } @@ -6455,7 +7455,7 @@ void tst_QWidget::testWindowIconChangeEventPropagation() QWidgetList widgets; widgets << &topLevelWidget << &topLevelChild << &dialog << &dialogChild; - QCOMPARE(widgets.count(), 4); + QCOMPARE(widgets.size(), 4); topLevelWidget.show(); dialog.show(); @@ -6469,13 +7469,13 @@ void tst_QWidget::testWindowIconChangeEventPropagation() // Create spy lists. QList <EventSpyPtr> applicationEventSpies; QList <EventSpyPtr> widgetEventSpies; - for (QWidget *widget : qAsConst(widgets)) { + for (QWidget *widget : std::as_const(widgets)) { applicationEventSpies.append(EventSpyPtr::create(widget, QEvent::ApplicationWindowIconChange)); widgetEventSpies.append(EventSpyPtr::create(widget, QEvent::WindowIconChange)); } QList <WindowEventSpyPtr> appWindowEventSpies; QList <WindowEventSpyPtr> windowEventSpies; - for (QWindow *window : qAsConst(windows)) { + for (QWindow *window : std::as_const(windows)) { appWindowEventSpies.append(WindowEventSpyPtr::create(window, QEvent::ApplicationWindowIconChange)); windowEventSpies.append(WindowEventSpyPtr::create(window, QEvent::WindowIconChange)); } @@ -6484,7 +7484,7 @@ void tst_QWidget::testWindowIconChangeEventPropagation() const QIcon windowIcon = qApp->style()->standardIcon(QStyle::SP_TitleBarMenuButton); qApp->setWindowIcon(windowIcon); - for (int i = 0; i < widgets.count(); ++i) { + for (int i = 0; i < widgets.size(); ++i) { // Check QEvent::ApplicationWindowIconChange EventSpyPtr spy = applicationEventSpies.at(i); QWidget *widget = spy->widget(); @@ -6501,7 +7501,7 @@ void tst_QWidget::testWindowIconChangeEventPropagation() QCOMPARE(spy->count(), 1); spy->clear(); } - for (int i = 0; i < windows.count(); ++i) { + for (int i = 0; i < windows.size(); ++i) { // Check QEvent::ApplicationWindowIconChange (sent to QWindow) // QWidgetWindows don't get this event, since the widget takes care of changing the icon WindowEventSpyPtr spy = appWindowEventSpies.at(i); @@ -6519,7 +7519,7 @@ void tst_QWidget::testWindowIconChangeEventPropagation() // Set icon on a top-level widget. topLevelWidget.setWindowIcon(QIcon()); - for (int i = 0; i < widgets.count(); ++i) { + for (int i = 0; i < widgets.size(); ++i) { // Check QEvent::ApplicationWindowIconChange EventSpyPtr spy = applicationEventSpies.at(i); QCOMPARE(spy->count(), 0); @@ -6676,14 +7676,12 @@ void tst_QWidget::clean_qt_x11_enforce_cursor() child->setAttribute(Qt::WA_SetCursor, true); window.show(); - QApplication::setActiveWindow(&window); QVERIFY(QTest::qWaitForWindowActive(&window)); QTest::qWait(100); QCursor::setPos(window.geometry().center()); QTest::qWait(100); child->setFocus(); - QApplication::processEvents(); QTest::qWait(100); delete w; @@ -6719,8 +7717,19 @@ public: bool eventFilter(QObject *object, QEvent *event) override { QWidget *widget = qobject_cast<QWidget *>(object); - if (widget && !event->spontaneous()) - events.append(qMakePair(widget, event->type())); + if (widget && !event->spontaneous()) { + switch (event->type()) { + // we might get those events if we couldn't move the cursor + case QEvent::Enter: + case QEvent::Leave: + // we might get this on systems that have an input method installed + case QEvent::InputMethodQuery: + break; + default: + events.append(qMakePair(widget, event->type())); + break; + } + } return false; } @@ -6762,11 +7771,6 @@ void tst_QWidget::childEvents() { EventRecorder::EventList expected; - // Move away the cursor; otherwise it might result in an enter event if it's - // inside the widget when the widget is shown. - QCursor::setPos(m_safeCursorPos); - QTest::qWait(100); - { // no children created, not shown QWidget widget; @@ -6806,7 +7810,9 @@ void tst_QWidget::childEvents() << qMakePair(&widget, QEvent::Move) << qMakePair(&widget, QEvent::Resize) << qMakePair(&widget, QEvent::Show) +#ifndef Q_OS_ANDROID << qMakePair(&widget, QEvent::CursorChange) +#endif << qMakePair(&widget, QEvent::ShowToParent); QVERIFY2(spy.eventList() == expected, @@ -6895,7 +7901,9 @@ void tst_QWidget::childEvents() << qMakePair(&widget, QEvent::Move) << qMakePair(&widget, QEvent::Resize) << qMakePair(&widget, QEvent::Show) +#ifndef Q_OS_ANDROID << qMakePair(&widget, QEvent::CursorChange) +#endif << qMakePair(&widget, QEvent::ShowToParent); QVERIFY2(spy.eventList() == expected, @@ -6987,7 +7995,9 @@ void tst_QWidget::childEvents() << qMakePair(&widget, QEvent::Move) << qMakePair(&widget, QEvent::Resize) << qMakePair(&widget, QEvent::Show) +#ifndef Q_OS_ANDROID << qMakePair(&widget, QEvent::CursorChange) +#endif << qMakePair(&widget, QEvent::ShowToParent); QVERIFY2(spy.eventList() == expected, @@ -7042,7 +8052,9 @@ private: void tst_QWidget::render() { - return; +#ifdef Q_OS_ANDROID + QSKIP("QTBUG-118984: crashes on Android."); +#endif QCalendarWidget source; source.setWindowTitle(QLatin1String(QTest::currentTestFunction())); // disable anti-aliasing to eliminate potential differences when subpixel antialiasing @@ -7057,16 +8069,11 @@ void tst_QWidget::render() RenderWidget target(&source); target.resize(source.size()); target.show(); - - QCoreApplication::processEvents(); - QCoreApplication::sendPostedEvents(); - QTest::qWait(250); + QVERIFY(QTest::qWaitForWindowExposed(&target)); const QImage sourceImage = source.grab(QRect(QPoint(0, 0), QSize(-1, -1))).toImage(); - QCoreApplication::processEvents(); QImage targetImage = target.grab(QRect(QPoint(0, 0), QSize(-1, -1))).toImage(); - QCoreApplication::processEvents(); - QCOMPARE(sourceImage, targetImage); + QTRY_COMPARE(sourceImage, targetImage); // Fill target.rect() will Qt::red and render // QRegion(0, 0, source->width(), source->height() / 2, QRegion::Ellipse) @@ -7079,66 +8086,73 @@ void tst_QWidget::render() QVERIFY(sourceImage != targetImage); QCOMPARE(targetImage.pixel(target.width() / 2, 29), QColor(Qt::red).rgb()); + if (targetImage.devicePixelRatioF() > 1) + QEXPECT_FAIL("", "This test fails on high-DPI displays", Continue); QCOMPARE(targetImage.pixel(target.width() / 2, 30), sourceImage.pixel(source.width() / 2, 0)); +} - // Test that a child widget properly fills its background - { - QWidget window; - window.setWindowTitle(QLatin1String(QTest::currentTestFunction())); - window.resize(100, 100); - // prevent custom styles - window.setStyle(QStyleFactory::create(QLatin1String("Windows"))); - window.show(); - QVERIFY(QTest::qWaitForWindowExposed(&window)); - QWidget child(&window); - child.resize(window.size()); - child.show(); +// Test that a child widget properly fills its background +void tst_QWidget::renderChildFillsBackground() +{ + QWidget window; + window.setWindowTitle(QLatin1String(QTest::currentTestFunction())); + window.resize(100, 100); + // prevent custom styles + window.setStyle(QStyleFactory::create(QLatin1String("Windows"))); + window.show(); + QVERIFY(QTest::qWaitForWindowExposed(&window)); + QWidget child(&window); + child.resize(window.size()); + child.show(); - QCoreApplication::processEvents(); - const QPixmap childPixmap = child.grab(QRect(QPoint(0, 0), QSize(-1, -1))); - const QPixmap windowPixmap = window.grab(QRect(QPoint(0, 0), QSize(-1, -1))); - QCOMPARE(childPixmap, windowPixmap); - } + QCoreApplication::processEvents(); + const QPixmap childPixmap = child.grab(QRect(QPoint(0, 0), QSize(-1, -1))); + const QPixmap windowPixmap = window.grab(QRect(QPoint(0, 0), QSize(-1, -1))); +#ifndef Q_OS_ANDROID + // On Android all widgets are shown maximized, so the pixmaps + // will be similar + if (!m_platform.startsWith(QLatin1String("wayland"), Qt::CaseInsensitive)) + QEXPECT_FAIL("", "This test fails on all platforms", Continue); +#endif + QCOMPARE(childPixmap, windowPixmap); +} - { // Check that the target offset is correct. - QWidget widget; - widget.setWindowTitle(QLatin1String(QTest::currentTestFunction())); - widget.resize(200, 200); - widget.setAutoFillBackground(true); - widget.setPalette(Qt::red); - // prevent custom styles - widget.setStyle(QStyleFactory::create(QLatin1String("Windows"))); - widget.show(); - QVERIFY(QTest::qWaitForWindowExposed(&widget)); - QImage image(widget.size(), QImage::Format_RGB32); - image.fill(QColor(Qt::blue).rgb()); - - // Target offset (0, 0) - widget.render(&image, QPoint(), QRect(20, 20, 100, 100)); - QCOMPARE(image.pixel(0, 0), QColor(Qt::red).rgb()); - QCOMPARE(image.pixel(99, 99), QColor(Qt::red).rgb()); - QCOMPARE(image.pixel(100, 100), QColor(Qt::blue).rgb()); - - // Target offset (20, 20). - image.fill(QColor(Qt::blue).rgb()); - widget.render(&image, QPoint(20, 20), QRect(20, 20, 100, 100)); - QCOMPARE(image.pixel(0, 0), QColor(Qt::blue).rgb()); - QCOMPARE(image.pixel(19, 19), QColor(Qt::blue).rgb()); - QCOMPARE(image.pixel(20, 20), QColor(Qt::red).rgb()); - QCOMPARE(image.pixel(119, 119), QColor(Qt::red).rgb()); - QCOMPARE(image.pixel(120, 120), QColor(Qt::blue).rgb()); - } -} - -// On Windows the active palette is used instead of the inactive palette even -// though the widget is invisible. This is probably related to task 178507/168682, -// but for the renderInvisible test it doesn't matter, we're mostly interested -// in testing the geometry so just workaround the palette issue for now. +void tst_QWidget::renderTargetOffset() +{ // Check that the target offset is correct. + QWidget widget; + widget.setWindowTitle(QLatin1String(QTest::currentTestFunction())); + widget.resize(200, 200); + widget.setAutoFillBackground(true); + widget.setPalette(Qt::red); + // prevent custom styles + widget.setStyle(QStyleFactory::create(QLatin1String("Windows"))); + widget.show(); + QVERIFY(QTest::qWaitForWindowExposed(&widget)); + QImage image(widget.size(), QImage::Format_RGB32); + image.fill(QColor(Qt::blue).rgb()); + + // Target offset (0, 0) + widget.render(&image, QPoint(), QRect(20, 20, 100, 100)); + QCOMPARE(image.pixel(0, 0), QColor(Qt::red).rgb()); + QCOMPARE(image.pixel(99, 99), QColor(Qt::red).rgb()); + QCOMPARE(image.pixel(100, 100), QColor(Qt::blue).rgb()); + + // Target offset (20, 20). + image.fill(QColor(Qt::blue).rgb()); + widget.render(&image, QPoint(20, 20), QRect(20, 20, 100, 100)); + QCOMPARE(image.pixel(0, 0), QColor(Qt::blue).rgb()); + QCOMPARE(image.pixel(19, 19), QColor(Qt::blue).rgb()); + QCOMPARE(image.pixel(20, 20), QColor(Qt::red).rgb()); + QCOMPARE(image.pixel(119, 119), QColor(Qt::red).rgb()); + QCOMPARE(image.pixel(120, 120), QColor(Qt::blue).rgb()); +} + +// On some platforms the active palette is used instead of the inactive palette even +// though the widget is invisible, but for the renderInvisible test it doesn't matter, +// as we're mostly interested in testing the geometry, so just workaround the palette +// issue for now. static void workaroundPaletteIssue(QWidget *widget) { -#ifndef Q_OS_WIN - return; -#endif if (!widget) return; @@ -7159,6 +8173,9 @@ void tst_QWidget::renderInvisible() if (m_platform == QStringLiteral("xcb")) QSKIP("QTBUG-26424"); + if (m_platform.startsWith(QLatin1String("wayland"), Qt::CaseInsensitive)) + QSKIP("Wayland: Skip this test, see also QTBUG-107157"); + QScopedPointer<QCalendarWidget> calendar(new QCalendarWidget); calendar->move(m_availableTopLeft + QPoint(100, 100)); calendar->setWindowTitle(QLatin1String(QTest::currentTestFunction())); @@ -7176,12 +8193,10 @@ void tst_QWidget::renderInvisible() dummyFocusWidget.move(calendar->geometry().bottomLeft() + QPoint(0, 100)); dummyFocusWidget.show(); QVERIFY(QTest::qWaitForWindowExposed(&dummyFocusWidget)); - QCoreApplication::processEvents(); - QTest::qWait(120); // Create normal reference image. const QSize calendarSize = calendar->size(); - QImage referenceImage(calendarSize, QImage::Format_ARGB32); + QImage referenceImage(calendarSize, QImage::Format_ARGB32_Premultiplied); calendar->render(&referenceImage); #ifdef RENDER_DEBUG referenceImage.save("referenceImage.png"); @@ -7191,9 +8206,8 @@ void tst_QWidget::renderInvisible() // Create resized reference image. const QSize calendarSizeResized = calendar->size() + QSize(50, 50); calendar->resize(calendarSizeResized); - QCoreApplication::processEvents(); QTest::qWait(30); - QImage referenceImageResized(calendarSizeResized, QImage::Format_ARGB32); + QImage referenceImageResized(calendarSizeResized, QImage::Format_ARGB32_Premultiplied); calendar->render(&referenceImageResized); #ifdef RENDER_DEBUG referenceImageResized.save("referenceImageResized.png"); @@ -7202,12 +8216,11 @@ void tst_QWidget::renderInvisible() // Explicitly hide the calendar. calendar->hide(); - QCoreApplication::processEvents(); QTest::qWait(30); workaroundPaletteIssue(calendar.data()); { // Make sure we get the same image when the calendar is explicitly hidden. - QImage testImage(calendarSizeResized, QImage::Format_ARGB32); + QImage testImage(calendarSizeResized, QImage::Format_ARGB32_Premultiplied); calendar->render(&testImage); #ifdef RENDER_DEBUG testImage.save("explicitlyHiddenCalendarResized.png"); @@ -7223,7 +8236,7 @@ void tst_QWidget::renderInvisible() workaroundPaletteIssue(calendar.data()); { // Never been visible, created or laid out. - QImage testImage(calendarSize, QImage::Format_ARGB32); + QImage testImage(calendarSize, QImage::Format_ARGB32_Premultiplied); calendar->render(&testImage); #ifdef RENDER_DEBUG testImage.save("neverBeenVisibleCreatedOrLaidOut.png"); @@ -7232,11 +8245,10 @@ void tst_QWidget::renderInvisible() } calendar->hide(); - QCoreApplication::processEvents(); QTest::qWait(30); { // Calendar explicitly hidden. - QImage testImage(calendarSize, QImage::Format_ARGB32); + QImage testImage(calendarSize, QImage::Format_ARGB32_Premultiplied); calendar->render(&testImage); #ifdef RENDER_DEBUG testImage.save("explicitlyHiddenCalendar.png"); @@ -7250,7 +8262,7 @@ void tst_QWidget::renderInvisible() navigationBar->hide(); { // Check that the navigation bar isn't drawn when rendering the entire calendar. - QImage testImage(calendarSize, QImage::Format_ARGB32); + QImage testImage(calendarSize, QImage::Format_ARGB32_Premultiplied); calendar->render(&testImage); #ifdef RENDER_DEBUG testImage.save("calendarWithoutNavigationBar.png"); @@ -7259,7 +8271,7 @@ void tst_QWidget::renderInvisible() } { // Make sure the navigation bar renders correctly even though it's hidden. - QImage testImage(navigationBar->size(), QImage::Format_ARGB32); + QImage testImage(navigationBar->size(), QImage::Format_ARGB32_Premultiplied); navigationBar->render(&testImage); #ifdef RENDER_DEBUG testImage.save("explicitlyHiddenNavigationBar.png"); @@ -7273,7 +8285,7 @@ void tst_QWidget::renderInvisible() { // Render next month button. // Fill test image with correct background color. - QImage testImage(nextMonthButton->size(), QImage::Format_ARGB32); + QImage testImage(nextMonthButton->size(), QImage::Format_ARGB32_Premultiplied); navigationBar->render(&testImage, QPoint(), QRegion(), QWidget::RenderFlags()); #ifdef RENDER_DEBUG testImage.save("nextMonthButtonBackground.png"); @@ -7300,7 +8312,6 @@ void tst_QWidget::renderInvisible() // Navigation bar isn't explicitly hidden anymore. navigationBar->show(); - QCoreApplication::processEvents(); QTest::qWait(30); QVERIFY(!calendar->isVisible()); @@ -7312,7 +8323,7 @@ void tst_QWidget::renderInvisible() QCoreApplication::processEvents(); { // Make sure we get an image equal to the resized reference image. - QImage testImage(calendarSizeResized, QImage::Format_ARGB32); + QImage testImage(calendarSizeResized, QImage::Format_ARGB32_Premultiplied); calendar->render(&testImage); #ifdef RENDER_DEBUG testImage.save("calendarResized.png"); @@ -7324,7 +8335,7 @@ void tst_QWidget::renderInvisible() QCalendarWidget calendar; const QSize calendarSize = calendar.sizeHint(); - QImage image(2 * calendarSize, QImage::Format_ARGB32); + QImage image(2 * calendarSize, QImage::Format_ARGB32_Premultiplied); image.fill(QColor(Qt::red).rgb()); calendar.render(&image); @@ -7464,6 +8475,47 @@ void tst_QWidget::renderWithPainter() QCOMPARE(painter.renderHints(), oldRenderHints); } +void tst_QWidget::renderRTL() +{ + QFont f; + f.setStyleStrategy(QFont::NoAntialias); + const QScopedPointer<QStyle> style(QStyleFactory::create(QLatin1String("Windows"))); + + QMenu menu; + menu.setMinimumWidth(200); + menu.setFont(f); + menu.setStyle(style.data()); + menu.addAction("I"); + menu.show(); + menu.setLayoutDirection(Qt::LeftToRight); + QVERIFY(QTest::qWaitForWindowExposed(&menu)); + + QImage imageLTR(menu.size(), QImage::Format_ARGB32); + menu.render(&imageLTR); + //imageLTR.save("/tmp/rendered_1.png"); + + menu.setLayoutDirection(Qt::RightToLeft); + QImage imageRTL(menu.size(), QImage::Format_ARGB32); + menu.render(&imageRTL); + imageRTL = imageRTL.mirrored(true, false); + //imageRTL.save("/tmp/rendered_2.png"); + + QCOMPARE(imageLTR.height(), imageRTL.height()); + QCOMPARE(imageLTR.width(), imageRTL.width()); + static constexpr auto border = 4; + for (int h = border; h < imageRTL.height() - border; ++h) { + // there should be no difference on the right (aka no text) + for (int w = imageRTL.width() / 2; w < imageRTL.width() - border; ++w) { + auto pixLTR = imageLTR.pixel(w, h); + auto pixRTL = imageRTL.pixel(w, h); + if (pixLTR != pixRTL) + qDebug() << "Pixel do not match at" << w << h << ":" + << Qt::hex << pixLTR << "<->" << pixRTL; + QCOMPARE(pixLTR, pixRTL); + } + } +} + void tst_QWidget::render_task188133() { QMainWindow mainWindow; @@ -8011,7 +9063,7 @@ void tst_QWidget::moveWindowInShowEvent_data() QTest::addColumn<QPoint>("initial"); QTest::addColumn<QPoint>("position"); - QPoint p = QGuiApplication::primaryScreen()->availableGeometry().topLeft(); + QPoint p = m_availableTopLeft; QTest::newRow("1") << p << (p + QPoint(10, 10)); QTest::newRow("2") << (p + QPoint(10,10)) << p; @@ -8048,21 +9100,15 @@ void tst_QWidget::moveWindowInShowEvent() // show it widget.showNormal(); QVERIFY(QTest::qWaitForWindowExposed(&widget)); - QTest::qWait(100); // it should have moved QCOMPARE(widget.pos(), position); } void tst_QWidget::repaintWhenChildDeleted() { -#ifdef Q_OS_WIN - QTest::qWait(1000); -#endif ColorWidget w(nullptr, Qt::FramelessWindowHint, Qt::red); w.setWindowTitle(QLatin1String(QTest::currentTestFunction())); - QPoint startPoint = w.screen()->availableGeometry().topLeft(); - startPoint.rx() += 50; - startPoint.ry() += 50; + const QPoint startPoint = m_availableTopLeft + QPoint(50, 50); w.setGeometry(QRect(startPoint, QSize(100, 100))); w.show(); QVERIFY(QTest::qWaitForWindowExposed(&w)); @@ -8085,9 +9131,7 @@ void tst_QWidget::hideOpaqueChildWhileHidden() { ColorWidget w(nullptr, Qt::FramelessWindowHint, Qt::red); w.setWindowTitle(QLatin1String(QTest::currentTestFunction())); - QPoint startPoint = w.screen()->availableGeometry().topLeft(); - startPoint.rx() += 50; - startPoint.ry() += 50; + const QPoint startPoint = m_availableTopLeft + QPoint(50, 50); w.setGeometry(QRect(startPoint, QSize(100, 100))); ColorWidget child(&w, Qt::Widget, Qt::blue); @@ -8136,6 +9180,7 @@ void tst_QWidget::updateWhileMinimized() QSKIP("Platform does not support showMinimized()"); #endif UpdateWidget widget; + widget.setPalette(simplePalette()); widget.setWindowTitle(QLatin1String(QTest::currentTestFunction())); // Filter out activation change and focus events to avoid update() calls in QWidget. widget.updateOnActivationChangeAndFocusIn = false; @@ -8163,7 +9208,8 @@ void tst_QWidget::updateWhileMinimized() qDebug() << "xcb: XDG_CURRENT_DESKTOP=" << desktop; if (desktop == QStringLiteral("ubuntu:GNOME") || desktop == QStringLiteral("GNOME-Classic:GNOME") - || desktop == QStringLiteral("GNOME")) + || desktop == QStringLiteral("GNOME") + || desktop.isEmpty()) // on local VMs count = 1; } QCOMPARE(widget.numPaintEvents, count); @@ -8708,6 +9754,7 @@ void tst_QWidget::doubleRepaint() QSKIP("Not having window server access causes the wrong number of repaints to be issues"); #endif UpdateWidget widget; + widget.setPalette(simplePalette()); widget.setWindowTitle(QLatin1String(QTest::currentTestFunction())); centerOnScreen(&widget); widget.setFocusPolicy(Qt::StrongFocus); @@ -8724,9 +9771,6 @@ void tst_QWidget::doubleRepaint() // Minmize: Should not trigger a repaint. widget.showMinimized(); QTest::qWait(10); -#if defined(Q_OS_QNX) - QEXPECT_FAIL("", "Platform does not support showMinimized()", Continue); -#endif QCOMPARE(widget.numPaintEvents, 0); widget.numPaintEvents = 0; @@ -8742,9 +9786,10 @@ void tst_QWidget::resizeInPaintEvent() QWidget window; window.setWindowTitle(QLatin1String(QTest::currentTestFunction())); UpdateWidget widget(&window); + widget.setPalette(simplePalette()); window.resize(200, 200); window.show(); - QApplication::setActiveWindow(&window); + QApplicationPrivate::setActiveWindow(&window); QVERIFY(QTest::qWaitForWindowExposed(&window)); QTRY_VERIFY(widget.numPaintEvents > 0); @@ -8798,6 +9843,46 @@ void tst_QWidget::opaqueChildren() QCOMPARE(qt_widget_private(&grandChild)->getOpaqueChildren(), QRegion()); } +void tst_QWidget::dumpObjectTree() +{ + if (!QGuiApplicationPrivate::platformIntegration()->hasCapability(QPlatformIntegration::WindowActivation)) + QSKIP("QWindow::requestActivate() is not supported."); + + QWidget w; + w.setWindowTitle(QLatin1String(QTest::currentTestFunction())); + Q_SET_OBJECT_NAME(w); + w.move(100, 100); + w.resize(200, 200); + + QLineEdit le(&w); + Q_SET_OBJECT_NAME(le); + le.resize(200, 200); + + { + const char * const expected[] = { + "QWidget::w I", + " QLineEdit::le I", + " QWidgetLineControl:: ", + }; + for (const char *line : expected) + QTest::ignoreMessage(QtDebugMsg, line); + w.dumpObjectTree(); + } + + QTestPrivate::androidCompatibleShow(&w); + QVERIFY(QTest::qWaitForWindowActive(&w)); + + { + const char * const expected[] = { + "QWidget::w <200x200+100+100>", + " QLineEdit::le F<200x200+0+0>", + " QWidgetLineControl:: ", + }; + for (const char *line : expected) + QTest::ignoreMessage(QtDebugMsg, line); + w.dumpObjectTree(); + } +} class MaskSetWidget : public QWidget { @@ -8812,6 +9897,8 @@ public: paintedRegion += event->region(); for (const QRect &r : event->region()) p.fillRect(r, Qt::red); + + repainted = true; } void resizeEvent(QResizeEvent *) override @@ -8820,6 +9907,7 @@ public: } QRegion paintedRegion; + bool repainted = false; public slots: void resizeDown() { setGeometry(QRect(0, 50, 50, 50)); } @@ -8829,6 +9917,7 @@ public slots: void tst_QWidget::setMaskInResizeEvent() { UpdateWidget w; + w.setPalette(simplePalette()); w.setWindowTitle(QLatin1String(QTest::currentTestFunction())); w.reset(); w.resize(200, 200); @@ -8846,19 +9935,19 @@ void tst_QWidget::setMaskInResizeEvent() w.reset(); testWidget.paintedRegion = QRegion(); - QTimer::singleShot(0, &testWidget, SLOT(resizeDown())); - QTest::qWait(100); + testWidget.resizeDown(); QRegion expectedParentUpdate(0, 0, 100, 10); // Old testWidget area. expectedParentUpdate += testWidget.geometry(); // New testWidget area. + QTRY_VERIFY(testWidget.repainted); QTRY_COMPARE(w.paintedRegion, expectedParentUpdate); QTRY_COMPARE(testWidget.paintedRegion, testWidget.mask()); testWidget.paintedRegion = QRegion(); - // Now resize the widget again, but in the oposite direction - QTimer::singleShot(0, &testWidget, SLOT(resizeUp())); - QTest::qWait(100); - + testWidget.repainted = false; + // Now resize the widget again, but in the opposite direction + testWidget.resizeUp(); + QTRY_VERIFY(testWidget.repainted); QTRY_COMPARE(testWidget.paintedRegion, testWidget.mask()); } @@ -8908,6 +9997,7 @@ void tst_QWidget::immediateRepaintAfterInvalidateBackingStore() QSKIP("We don't support immediate repaint right after show on other platforms."); QScopedPointer<UpdateWidget> widget(new UpdateWidget); + widget->setPalette(simplePalette()); widget->setWindowTitle(QLatin1String(QTest::currentTestFunction())); centerOnScreen(widget.data()); widget->show(); @@ -9292,28 +10382,32 @@ void tst_QWidget::translucentWidget() label.setWindowTitle(QLatin1String(QTest::currentTestFunction())); label.setFixedSize(16,16); label.setAttribute(Qt::WA_TranslucentBackground); - const QPoint labelPos = QGuiApplication::primaryScreen()->availableGeometry().topLeft(); - label.move(labelPos); + label.move(m_availableTopLeft); label.show(); QVERIFY(QTest::qWaitForWindowExposed(&label)); QPixmap widgetSnapshot = -#ifdef Q_OS_WIN - QGuiApplication::primaryScreen()->grabWindow(0, labelPos.x(), labelPos.y(), label.width(), label.height()); -#else label.grab(QRect(QPoint(0, 0), label.size())); -#endif const QImage actual = widgetSnapshot.toImage().convertToFormat(QImage::Format_RGB32); QImage expected = pm.toImage().scaled(label.devicePixelRatio() * pm.size()); expected.setDevicePixelRatio(label.devicePixelRatio()); +#ifdef Q_OS_ANDROID + // Android uses Format_ARGB32_Premultiplied by default + expected = expected.convertToFormat(QImage::Format_RGB32); +#endif QCOMPARE(actual.size(),expected.size()); QCOMPARE(actual,expected); const QWindow *window = label.windowHandle(); - const QSurfaceFormat translucentFormat = window->requestedFormat(); + const QSurfaceFormat translucentFormat = window->format(); label.setAttribute(Qt::WA_TranslucentBackground, false); - const QSurfaceFormat opaqueFormat = window->requestedFormat(); - QVERIFY(translucentFormat != opaqueFormat); + // Changing WA_TranslucentBackground with an already created native window + // has no effect since Qt 5.0 due to the introduction of QWindow et al. + // This means that the change must *not* be reflected in the + // QSurfaceFormat, because there is no change when it comes to the + // underlying native window. Otherwise the state would no longer + // describe reality (the native window) See QTBUG-85714. + QVERIFY(translucentFormat == window->format()); } class MaskResizeTestWidget : public QWidget @@ -9355,11 +10449,12 @@ void tst_QWidget::setClearAndResizeMask() QSKIP("Wayland: This fails. Figure out why."); UpdateWidget topLevel; + topLevel.setPalette(simplePalette()); topLevel.setWindowTitle(QLatin1String(QTest::currentTestFunction())); topLevel.resize(160, 160); centerOnScreen(&topLevel); topLevel.show(); - QApplication::setActiveWindow(&topLevel); + QApplicationPrivate::setActiveWindow(&topLevel); QVERIFY(QTest::qWaitForWindowExposed(&topLevel)); QTRY_VERIFY(topLevel.numPaintEvents > 0); topLevel.reset(); @@ -9389,6 +10484,7 @@ void tst_QWidget::setClearAndResizeMask() } UpdateWidget child(&topLevel); + child.setPalette(simplePalette()); child.setAutoFillBackground(true); // NB! Opaque child. child.setPalette(Qt::red); child.resize(100, 100); @@ -9668,6 +10764,8 @@ void tst_QWidget::syntheticEnterLeave() }; QCursor::setPos(m_safeCursorPos); + if (!QTest::qWaitFor([this]{ return QCursor::pos() == m_safeCursorPos; })) + QSKIP("Can't move cursor"); MyWidget window; window.setWindowTitle(QLatin1String(QTest::currentTestFunction())); @@ -9708,7 +10806,8 @@ void tst_QWidget::syntheticEnterLeave() // Position the cursor in the middle of the window. const QPoint globalPos = window.mapToGlobal(QPoint(100, 100)); QCursor::setPos(globalPos); // Enter child2 and grandChild. - QTest::qWait(300); + if (!QTest::qWaitFor([globalPos]{ return QCursor::pos() == globalPos; })) + QSKIP("Can't move cursor"); QCOMPARE(window.numLeaveEvents, 0); QCOMPARE(child2->numLeaveEvents, 0); @@ -9759,6 +10858,129 @@ void tst_QWidget::syntheticEnterLeave() #endif #ifndef QT_NO_CURSOR +void tst_QWidget::enterLeaveOnWindowShowHide_data() +{ + QTest::addColumn<Qt::WindowType>("windowType"); + QTest::addRow("dialog") << Qt::Dialog; + QTest::addRow("popup") << Qt::Popup; +} + + +/*! + Verify that a window that has the mouse gets a leave event + when a dialog or popup opens (even if that dialog or popup is + not under the mouse), and an enter event when the secondary window + closes again (while the mouse is still over the original widget. + + Since mouse grabbing might cause some event interaction, simulate + the opening of the secondary window from a mouse press, like we would with + a button or context menu. See QTBUG-78970. +*/ +void tst_QWidget::enterLeaveOnWindowShowHide() +{ + if (!QGuiApplicationPrivate::platformIntegration()->hasCapability(QPlatformIntegration::WindowActivation)) + QSKIP("QWindow::requestActivate() is not supported."); + + QFETCH(Qt::WindowType, windowType); + class Widget : public QWidget + { + public: + int numEnterEvents = 0; + int numLeaveEvents = 0; + QPoint enterPosition; + Qt::WindowType secondaryWindowType = {}; + protected: + void enterEvent(QEnterEvent *e) override + { + enterPosition = e->position().toPoint(); + ++numEnterEvents; + } + void leaveEvent(QEvent *) override + { + enterPosition = {}; + ++numLeaveEvents; + } + void mousePressEvent(QMouseEvent *e) override + { + QWidget *secondary = nullptr; + switch (secondaryWindowType) { + case Qt::Dialog: { + QDialog *dialog = new QDialog(this); + dialog->setModal(true); + dialog->setWindowModality(Qt::ApplicationModal); + secondary = dialog; + break; + } + case Qt::Popup: { + QMenu *menu = new QMenu(this); + menu->addAction("Action 1"); + menu->addAction("Action 2"); + secondary = menu; + break; + } + default: + QVERIFY2(false, "Test case not implemented for window type"); + break; + } + + QPoint secondaryPos = e->globalPosition().toPoint(); + if (e->button() == Qt::LeftButton) + secondaryPos += QPoint(10, 10); // cursor outside secondary + else + secondaryPos -= QPoint(10, 10); // cursor inside secondary + secondary->move(secondaryPos); + secondary->show(); + if (!QTest::qWaitForWindowExposed(secondary)) + QEXPECT_FAIL("", "Secondary window failed to show, test will fail", Abort); + if (secondaryWindowType == Qt::Dialog && QGuiApplication::platformName() == "windows") + QTest::qWait(1000); // on Windows, we have to wait for fade-in effects + } + }; + + int expectedEnter = 0; + int expectedLeave = 0; + + Widget widget; + widget.secondaryWindowType = windowType; + const QRect screenGeometry = widget.screen()->availableGeometry(); + const QPoint cursorPos = screenGeometry.topLeft() + QPoint(50, 50); + widget.setGeometry(QRect(cursorPos - QPoint(50, 50), screenGeometry.size() / 4)); + QCursor::setPos(cursorPos); + + if (!QTest::qWaitFor([&]{ return widget.geometry().contains(QCursor::pos()); })) + QSKIP("We can't move the cursor"); + widget.show(); + QVERIFY(QTest::qWaitForWindowActive(&widget)); + + ++expectedEnter; + QTRY_COMPARE_WITH_TIMEOUT(widget.numEnterEvents, expectedEnter, 1000); + QCOMPARE(widget.enterPosition, widget.mapFromGlobal(cursorPos)); + QVERIFY(widget.underMouse()); + + QTest::mouseClick(&widget, Qt::LeftButton, {}, widget.mapFromGlobal(cursorPos)); + ++expectedLeave; + QTRY_COMPARE_WITH_TIMEOUT(widget.numLeaveEvents, expectedLeave, 1000); + QVERIFY(!widget.underMouse()); + QTRY_VERIFY(QApplication::activeModalWidget() || QApplication::activePopupWidget()); + if (QApplication::activeModalWidget()) + QApplication::activeModalWidget()->close(); + else if (QApplication::activePopupWidget()) + QApplication::activePopupWidget()->close(); + ++expectedEnter; + // Use default timeout, the test is flaky on Windows otherwise. + QTRY_VERIFY(widget.numEnterEvents >= expectedEnter); + // When a modal dialog closes we might get more than one enter event on macOS. + // This seems to depend on timing, so we tolerate that flakiness for now. + if (widget.numEnterEvents > expectedEnter && QGuiApplication::platformName() == "cocoa") + QEXPECT_FAIL("dialog", "On macOS, we might get more than one Enter event", Continue); + + QCOMPARE(widget.numEnterEvents, expectedEnter); + QCOMPARE(widget.enterPosition, widget.mapFromGlobal(cursorPos)); + QVERIFY(widget.underMouse()); +} +#endif + +#ifndef QT_NO_CURSOR void tst_QWidget::taskQTBUG_4055_sendSyntheticEnterLeave() { if (m_platform == QStringLiteral("wayland")) @@ -9788,69 +11010,175 @@ void tst_QWidget::taskQTBUG_4055_sendSyntheticEnterLeave() int numEnterEvents = 0, numMouseMoveEvents = 0; }; - QCursor::setPos(m_safeCursorPos); - - SELParent parent; - parent.setWindowTitle(QLatin1String(QTest::currentTestFunction())); - parent.move(200, 200); - parent.resize(200, 200); - SELChild child(&parent); - child.resize(200, 200); - parent.show(); - QVERIFY(QTest::qWaitForWindowActive(&parent)); - - QCursor::setPos(child.mapToGlobal(QPoint(100, 100))); - // Make sure the cursor has entered the child. - QTRY_VERIFY(child.numEnterEvents > 0); - - child.hide(); - child.reset(); - child.show(); - - // Make sure the child gets enter event and no mouse move event. - QTRY_COMPARE(child.numEnterEvents, 1); - QCOMPARE(child.numMouseMoveEvents, 0); - - child.hide(); - child.reset(); - child.setMouseTracking(true); - child.show(); - - // Make sure the child gets enter event. - // Note that we verify event->button() and event->buttons() - // in SELChild::mouseMoveEvent(). - QTRY_COMPARE(child.numEnterEvents, 1); - QCOMPARE(child.numMouseMoveEvents, 0); - - // Sending synthetic enter/leave trough the parent's mousePressEvent handler. - parent.child = &child; - - child.hide(); - child.reset(); - QTest::mouseClick(&parent, Qt::LeftButton); - - // Make sure the child gets enter event. - QTRY_COMPARE(child.numEnterEvents, 1); - QCOMPARE(child.numMouseMoveEvents, 0); - - child.hide(); - child.reset(); - QTest::keyPress(&parent, Qt::Key_Shift); - QTest::mouseClick(&parent, Qt::LeftButton); - - // Make sure the child gets enter event - QTRY_COMPARE(child.numEnterEvents, 1); - QCOMPARE(child.numMouseMoveEvents, 0); - QTest::keyRelease(&child, Qt::Key_Shift); - child.hide(); - child.reset(); - child.setMouseTracking(false); - QTest::mouseClick(&parent, Qt::LeftButton); - - // Make sure the child gets enter event and no mouse move event. - QTRY_COMPARE(child.numEnterEvents, 1); - QCOMPARE(child.numMouseMoveEvents, 0); + QCursor::setPos(m_safeCursorPos); + if (!QTest::qWaitFor([this]{ return QCursor::pos() == m_safeCursorPos; })) + QSKIP("Can't move cursor"); + + SELParent parent; + parent.setWindowTitle(QLatin1String(QTest::currentTestFunction())); + parent.move(200, 200); + parent.resize(200, 200); + SELChild child(&parent); + child.resize(200, 200); + parent.show(); + QVERIFY(QTest::qWaitForWindowActive(&parent)); + + const QPoint childPos = child.mapToGlobal(QPoint(100, 100)); + QCursor::setPos(childPos); + if (!QTest::qWaitFor([childPos]{ return QCursor::pos() == childPos; })) + QSKIP("Can't move cursor"); + + // Make sure the cursor has entered the child. + QTRY_VERIFY(child.numEnterEvents > 0); + + child.hide(); + child.reset(); + child.show(); + + // Make sure the child gets enter event and no mouse move event. + QTRY_COMPARE(child.numEnterEvents, 1); + QCOMPARE(child.numMouseMoveEvents, 0); + + child.hide(); + child.reset(); + child.setMouseTracking(true); + child.show(); + + // Make sure the child gets enter event. + // Note that we verify event->button() and event->buttons() + // in SELChild::mouseMoveEvent(). + QTRY_COMPARE(child.numEnterEvents, 1); + QCOMPARE(child.numMouseMoveEvents, 0); + + // Sending synthetic enter/leave through the parent's mousePressEvent handler. + parent.child = &child; + + child.hide(); + child.reset(); + QTest::mouseClick(&parent, Qt::LeftButton); + + // Make sure the child gets enter event. + QTRY_COMPARE(child.numEnterEvents, 1); + QCOMPARE(child.numMouseMoveEvents, 0); + + child.hide(); + child.reset(); + QTest::keyPress(&parent, Qt::Key_Shift); + QTest::mouseClick(&parent, Qt::LeftButton); + + // Make sure the child gets enter event + QTRY_COMPARE(child.numEnterEvents, 1); + QCOMPARE(child.numMouseMoveEvents, 0); + QTest::keyRelease(&child, Qt::Key_Shift); + child.hide(); + child.reset(); + child.setMouseTracking(false); + QTest::mouseClick(&parent, Qt::LeftButton); + + // Make sure the child gets enter event and no mouse move event. + QTRY_COMPARE(child.numEnterEvents, 1); + QCOMPARE(child.numMouseMoveEvents, 0); } + +void tst_QWidget::hoverPosition() +{ + if (m_platform == QStringLiteral("wayland")) + QSKIP("Wayland: Clients can't set cursor position on wayland."); + + class HoverWidget : public QWidget + { + public: + HoverWidget(QWidget *parent = nullptr) : QWidget(parent) { + setMouseTracking(true); + setAttribute(Qt::WA_Hover); + } + bool event(QEvent *ev) override { + switch (ev->type()) { + case QEvent::HoverMove: + // The docs say that WA_Hover will cause a paint event on enter and leave, but not on move. + update(); + Q_FALLTHROUGH(); + case QEvent::HoverEnter: + case QEvent::HoverLeave: { + qCDebug(lcTests) << ev; + lastHoverType = ev->type(); + ++hoverEventCount; + QHoverEvent *hov = static_cast<QHoverEvent *>(ev); + mousePos = hov->position().toPoint(); + mouseScenePos = hov->scenePosition().toPoint(); + if (ev->type() == QEvent::HoverEnter) + mouseEnterScenePos = hov->scenePosition().toPoint(); + break; + } + default: + break; + } + return QWidget::event(ev); + } + void paintEvent(QPaintEvent *) override { + ++paintEventCount; + QPainter painter(this); + if (mousePos.x() > 0) + painter.setPen(Qt::red); + painter.drawRect(0, 0, width(), height()); + painter.setPen(Qt::darkGreen); + painter.drawLine(mousePos - QPoint(crossHalfWidth, 0), mousePos + QPoint(crossHalfWidth, 0)); + painter.drawLine(mousePos - QPoint(0, crossHalfWidth), mousePos + QPoint(0, crossHalfWidth)); + } + + QEvent::Type lastHoverType = QEvent::None; + int hoverEventCount = 0; + int paintEventCount = 0; + QPoint mousePos; + QPoint mouseScenePos; + QPoint mouseEnterScenePos; + + private: + const int crossHalfWidth = 5; + }; + + QCursor::setPos(m_safeCursorPos); + if (!QTest::qWaitFor([this]{ return QCursor::pos() == m_safeCursorPos; })) + QSKIP("Can't move cursor"); + + QWidget root; + root.resize(300, 300); + HoverWidget h(&root); + h.setGeometry(100, 100, 100, 100); + root.show(); + QVERIFY(QTest::qWaitForWindowExposed(&root)); + + const QPoint middle(50, 50); + QPoint curpos = h.mapToGlobal(middle); + QCursor::setPos(curpos); + if (!QTest::qWaitFor([curpos]{ return QCursor::pos() == curpos; })) + QSKIP("Can't move cursor"); + QTRY_COMPARE_GE(h.hoverEventCount, 1); // HoverEnter and then probably HoverMove, so usually 2 + QTRY_COMPARE_GE(h.paintEventCount, 2); + const int enterHoverEventCount = h.hoverEventCount; + qCDebug(lcTests) << "hover enter events:" << enterHoverEventCount << "last was" << h.lastHoverType + << "; paint events:" << h.paintEventCount; + QCOMPARE(h.mousePos, middle); + QCOMPARE(h.mouseEnterScenePos, h.mapToParent(middle)); + QCOMPARE(h.mouseScenePos, h.mapToParent(middle)); + QCOMPARE(h.lastHoverType, enterHoverEventCount == 1 ? QEvent::HoverEnter : QEvent::HoverMove); + + curpos += {10, 10}; + QCursor::setPos(curpos); + if (!QTest::qWaitFor([curpos]{ return QCursor::pos() == curpos; })) + QSKIP("Can't move cursor"); + QTRY_COMPARE(h.hoverEventCount, enterHoverEventCount + 1); + QCOMPARE(h.lastHoverType, QEvent::HoverMove); + QTRY_COMPARE_GE(h.paintEventCount, 3); + + curpos += {50, 50}; // in the outer widget, but leaving the inner widget + QCursor::setPos(curpos); + if (!QTest::qWaitFor([curpos]{ return QCursor::pos() == curpos; })) + QSKIP("Can't move cursor"); + QTRY_COMPARE(h.lastHoverType, QEvent::HoverLeave); + QCOMPARE_GE(h.hoverEventCount, enterHoverEventCount + 2); + QTRY_COMPARE_GE(h.paintEventCount, 4); +} #endif void tst_QWidget::windowFlags() @@ -9956,6 +11284,7 @@ void tst_QWidget::focusWidget_task254563() void tst_QWidget::destroyBackingStore() { UpdateWidget w; + w.setPalette(simplePalette()); w.setWindowTitle(QLatin1String(QTest::currentTestFunction())); centerOnScreen(&w); w.reset(); @@ -10080,6 +11409,157 @@ void tst_QWidget::setGraphicsEffect() QVERIFY(!blurEffect); } + +class TestGraphicsEffect : public QGraphicsEffect +{ +public: + TestGraphicsEffect(QObject *parent = nullptr) + : QGraphicsEffect(parent) + { + m_pattern = QPixmap(10, 10); + m_pattern.fill(Qt::lightGray); + QPainter p(&m_pattern); + p.fillRect(QRectF(0, 0, 5, 5), QBrush(Qt::darkGray)); + p.fillRect(QRectF(5, 5, 5, 5), QBrush(Qt::darkGray)); + } + void setExtent(int extent) + { + m_extent = extent; + } + QRectF boundingRectFor(const QRectF &sr) const override + { + return QRectF(sr.x() - m_extent, sr.y() - m_extent, + sr.width() + 2 * m_extent, sr.height() + 2 * m_extent); + } +protected: + void draw(QPainter *painter) override + { + QBrush brush; + brush.setTexture(m_pattern); + brush.setStyle(Qt::TexturePattern); + QPaintDevice *p = painter->device(); + painter->fillRect(QRect(-m_extent, -m_extent, + p->width() + m_extent, p->height() + m_extent), brush); + } + QPixmap m_pattern; + int m_extent = 0; +}; + +static QImage fillExpected1() +{ + QImage expected(QSize(40, 40), QImage::Format_RGB32); + QPainter p(&expected); + p.fillRect(QRect{{0, 0}, expected.size()}, QBrush(Qt::gray)); + p.fillRect(QRect(10, 10, 10, 10), QBrush(Qt::red)); + p.fillRect(QRect(20, 20, 10, 10), QBrush(Qt::blue)); + return expected; +} +static QImage fillExpected2() +{ + QImage expected = fillExpected1(); + QPainter p(&expected); + p.fillRect(QRect(10, 10, 5, 5), QBrush(Qt::darkGray)); + p.fillRect(QRect(15, 15, 5, 5), QBrush(Qt::darkGray)); + p.fillRect(QRect(15, 10, 5, 5), QBrush(Qt::lightGray)); + p.fillRect(QRect(10, 15, 5, 5), QBrush(Qt::lightGray)); + return expected; +} +static QImage fillExpected3() +{ + QImage expected(QSize(40, 40), QImage::Format_RGB32); + QPixmap pattern; + pattern = QPixmap(10, 10); + pattern.fill(Qt::lightGray); + QPainter p(&pattern); + p.fillRect(QRectF(0, 0, 5, 5), QBrush(Qt::darkGray)); + p.fillRect(QRectF(5, 5, 5, 5), QBrush(Qt::darkGray)); + QBrush brush; + brush.setTexture(pattern); + brush.setStyle(Qt::TexturePattern); + QPainter p2(&expected); + p2.fillRect(QRect{{0, 0}, expected.size()}, brush); + return expected; +} +static QImage fillExpected4() +{ + QImage expected = fillExpected1(); + QPixmap pattern; + pattern = QPixmap(10, 10); + pattern.fill(Qt::lightGray); + QPainter p(&pattern); + p.fillRect(QRectF(0, 0, 5, 5), QBrush(Qt::darkGray)); + p.fillRect(QRectF(5, 5, 5, 5), QBrush(Qt::darkGray)); + QBrush brush; + brush.setTexture(pattern); + brush.setStyle(Qt::TexturePattern); + QPainter p2(&expected); + p2.fillRect(QRect{{15, 15}, QSize{20, 20}}, brush); + return expected; +} + +void tst_QWidget::render_graphicsEffect_data() +{ + QTest::addColumn<QImage>("expected"); + QTest::addColumn<bool>("topLevelEffect"); + QTest::addColumn<bool>("child1Effect"); + QTest::addColumn<bool>("child2Effect"); + QTest::addColumn<int>("extent"); + + QTest::addRow("no_effect") << fillExpected1() << false << false << false << 0; + QTest::addRow("first_child_effect") << fillExpected2() << false << true << false << 0; + QTest::addRow("top_level_effect") << fillExpected3() << true << false << false << 0; + QTest::addRow("effect_with_extent") << fillExpected4() << false << false << true << 5; +} + +void tst_QWidget::render_graphicsEffect() +{ + QFETCH(QImage, expected); + QFETCH(bool, topLevelEffect); + QFETCH(bool, child1Effect); + QFETCH(bool, child2Effect); + QFETCH(int, extent); + + QScopedPointer<QWidget> topLevel(new QWidget); + topLevel->setPalette(Qt::gray); + topLevel->resize(40, 40); + topLevel->setWindowTitle(QLatin1String(QTest::currentTestFunction()) + QLatin1String("::") + + QLatin1String(QTest::currentDataTag())); + + // Render widget with 2 child widgets + QImage image(topLevel->size(), QImage::Format_RGB32); + image.fill(QColor(Qt::gray).rgb()); + + QPainter painter(&image); + + QWidget *childWidget1(new QWidget(topLevel.data())); + childWidget1->setAutoFillBackground(true); + childWidget1->setPalette(Qt::red); + childWidget1->resize(10, 10); + childWidget1->move(10, 10); + QWidget *childWidget2(new QWidget(topLevel.data())); + childWidget2->setAutoFillBackground(true); + childWidget2->setPalette(Qt::blue); + childWidget2->resize(10, 10); + childWidget2->move(20, 20); + + TestGraphicsEffect *graphicsEffect(new TestGraphicsEffect(topLevel.data())); + if (topLevelEffect) + topLevel->setGraphicsEffect(graphicsEffect); + if (child1Effect) + childWidget1->setGraphicsEffect(graphicsEffect); + if (child2Effect) + childWidget2->setGraphicsEffect(graphicsEffect); + graphicsEffect->setExtent(extent); + + // Render without effect + topLevel->render(&painter); +#ifdef RENDER_DEBUG + image.save("render_GraphicsEffect" + QTest::currentDataTag() + ".png"); + expected.save("render_GraphicsEffect_expected" + QTest::currentDataTag() + ".png"); +#endif + QCOMPARE(image, expected); +} + void tst_QWidget::activateWindow() { if (!QGuiApplicationPrivate::platformIntegration()->hasCapability(QPlatformIntegration::WindowActivation)) @@ -10125,6 +11605,9 @@ void tst_QWidget::activateWindow() void tst_QWidget::openModal_taskQTBUG_5804() { +#ifdef Q_OS_ANDROID + QSKIP("This test hangs on Android"); +#endif class Widget : public QWidget { public: @@ -10207,9 +11690,16 @@ void tst_QWidget::focusProxy() window.setFocus(); window.show(); - window.activateWindow(); - if (!QTest::qWaitForWindowExposed(&window) || !QTest::qWaitForWindowActive(&window)) - QSKIP("Window activation failed"); + if (!QTest::qWaitForWindowExposed(&window)) + QSKIP("Window exposed failed"); + if (QGuiApplicationPrivate::platformIntegration()->hasCapability(QPlatformIntegration::WindowActivation)) { + window.activateWindow(); + if (!QTest::qWaitForWindowActive(&window)) + QSKIP("Window activation failed"); + } else { + if (!QTest::qWaitFor([&]() { return window.windowHandle()->isActive(); }, 5000)) + QSKIP("Window activation failed"); + } // given a widget without focus proxy QVERIFY(window.hasFocus()); @@ -10265,32 +11755,42 @@ void tst_QWidget::focusProxy() QCOMPARE(container2->focusOutCount, 1); } -void tst_QWidget::focusProxyAndInputMethods() +void tst_QWidget::imEnabledNotImplemented() { if (!QGuiApplicationPrivate::platformIntegration()->hasCapability(QPlatformIntegration::WindowActivation)) - QSKIP("Window activation is not supported."); - QScopedPointer<QWidget> toplevel(new QWidget(nullptr, Qt::X11BypassWindowManagerHint)); - toplevel->setWindowTitle(QLatin1String(QTest::currentTestFunction())); - toplevel->resize(200, 200); - toplevel->setAttribute(Qt::WA_InputMethodEnabled, true); + QSKIP("QWindow::requestActivate() is not supported."); - QWidget *child = new QWidget(toplevel.data()); - child->setFocusProxy(toplevel.data()); - child->setAttribute(Qt::WA_InputMethodEnabled, true); - - toplevel->setFocusPolicy(Qt::WheelFocus); - child->setFocusPolicy(Qt::WheelFocus); - - QVERIFY(!child->hasFocus()); - QVERIFY(!toplevel->hasFocus()); + // Check that a plain widget doesn't report that it supports IM. Only + // widgets that implements either Qt::ImEnabled, or the Qt4 backup + // solution, Qt::ImSurroundingText, should do so. + QWidget topLevel; + QWidget plain(&topLevel); + QLineEdit edit(&topLevel); + topLevel.show(); - toplevel->show(); - QVERIFY(QTest::qWaitForWindowExposed(toplevel.data())); - QApplication::setActiveWindow(toplevel.data()); - QVERIFY(QTest::qWaitForWindowActive(toplevel.data())); - QVERIFY(toplevel->hasFocus()); - QVERIFY(child->hasFocus()); - QCOMPARE(qApp->focusObject(), toplevel.data()); + QVERIFY(QTest::qWaitForWindowExposed(&topLevel)); + QVERIFY(QTest::qWaitForWindowActive(&topLevel)); + + // A plain widget should return false for ImEnabled + plain.setFocus(Qt::OtherFocusReason); + QCOMPARE(QApplication::focusWidget(), &plain); + QVariant imEnabled = QApplication::inputMethod()->queryFocusObject(Qt::ImEnabled, QVariant()); + QVERIFY(imEnabled.isValid()); + QVERIFY(!imEnabled.toBool()); + + // But a lineedit should return true + edit.setFocus(Qt::OtherFocusReason); + QCOMPARE(QApplication::focusWidget(), &edit); + imEnabled = QApplication::inputMethod()->queryFocusObject(Qt::ImEnabled, QVariant()); + QVERIFY(imEnabled.isValid()); + QVERIFY(imEnabled.toBool()); + + // ImEnabled should be false when a lineedit is read-only since + // ImEnabled indicates the widget accepts input method _input_. + edit.setReadOnly(true); + imEnabled = QApplication::inputMethod()->queryFocusObject(Qt::ImEnabled, QVariant()); + QVERIFY(imEnabled.isValid()); + QVERIFY(!imEnabled.toBool()); } #ifdef QT_BUILD_INTERNAL @@ -10349,7 +11849,7 @@ void tst_QWidget::taskQTBUG_7532_tabOrderWithFocusProxy() void tst_QWidget::movedAndResizedAttributes() { // Use Qt::Tool as fully decorated windows have a minimum width of 160 on - QWidget w(nullptr, Qt::Tool); + QWidget w; w.setWindowTitle(QLatin1String(QTest::currentTestFunction())); w.show(); @@ -10387,11 +11887,11 @@ void tst_QWidget::movedAndResizedAttributes() QVERIFY(!w.testAttribute(Qt::WA_Resized)); w.showNormal(); - w.move(10,10); + w.move(m_availableTopLeft); QVERIFY(w.testAttribute(Qt::WA_Moved)); QVERIFY(!w.testAttribute(Qt::WA_Resized)); - w.resize(100, 100); + w.resize(m_testWidgetSize); QVERIFY(w.testAttribute(Qt::WA_Moved)); QVERIFY(w.testAttribute(Qt::WA_Resized)); } @@ -10649,7 +12149,6 @@ void tst_QWidget::grabMouse() layout->addWidget(grabber); centerOnScreen(&w); w.show(); - QApplication::setActiveWindow(&w); QVERIFY(QTest::qWaitForWindowActive(&w)); QStringList expectedLog; @@ -10686,7 +12185,6 @@ void tst_QWidget::grabKeyboard() layout->addWidget(nonGrabber); centerOnScreen(&w); w.show(); - QApplication::setActiveWindow(&w); QVERIFY(QTest::qWaitForWindowActive(&w)); nonGrabber->setFocus(); grabber->grabKeyboard(); @@ -10780,6 +12278,9 @@ public: void tst_QWidget::touchEventSynthesizedMouseEvent() { + if (m_platform.startsWith(QLatin1String("wayland"), Qt::CaseInsensitive)) + QSKIP("This test failed on Wayland. See also QTBUG-107157."); + { // Simple case, we ignore the touch events, we get mouse events instead TouchMouseWidget widget; @@ -11194,9 +12695,10 @@ void tst_QWidget::underMouse() QCOMPARE(QApplication::activePopupWidget(), &popupWidget); // Send an artificial leave event for window, as it won't get generated automatically - // due to cursor not actually being over the window. - QWindowSystemInterface::handleLeaveEvent(window); - QApplication::processEvents(); + // due to cursor not actually being over the window. The Cocoa and offscreen plugins + // do this for us. + if (QGuiApplication::platformName() != "cocoa" && QGuiApplication::platformName() != "offscreen") + QWindowSystemInterface::handleLeaveEvent<QWindowSystemInterface::SynchronousDelivery>(window); // If there is an active popup, undermouse should not be reported (QTBUG-27478), // but opening a popup causes leave for widgets under mouse. @@ -11298,10 +12800,6 @@ void tst_QWidget::underMouse() // Mouse leaves popup and enters topLevelWidget, should cause leave for popup // but no enter to topLevelWidget. -#ifdef Q_OS_DARWIN - // Artificial leave event needed for Cocoa. - QWindowSystemInterface::handleLeaveEvent(popupWindow); -#endif QTest::mouseMove(popupWindow, popupWindow->mapFromGlobal(window->mapToGlobal(inWindowPoint))); QApplication::processEvents(); QVERIFY(!topLevelWidget.underMouse()); @@ -11385,9 +12883,6 @@ public: // when mousing over it. void tst_QWidget::taskQTBUG_27643_enterEvents() { -#ifdef Q_OS_MACOS - QSKIP("QTBUG-52974: this test can crash!"); -#endif // Move the mouse cursor to a safe location so it won't interfere QCursor::setPos(m_safeCursorPos); @@ -11494,6 +12989,7 @@ void tst_QWidget::resizeStaticContentsChildWidget_QTBUG35282() widget.resize(200,200); UpdateWidget childWidget(&widget); + childWidget.setPalette(simplePalette()); childWidget.setAttribute(Qt::WA_StaticContents); childWidget.setAttribute(Qt::WA_OpaquePaintEvent); childWidget.setGeometry(250, 250, 500, 500); @@ -11592,60 +13088,54 @@ void tst_QWidget::testForOutsideWSRangeFlag() } } -class TabletWidget : public QWidget +void tst_QWidget::tabletTracking() { -public: - TabletWidget(QWidget *parent) : QWidget(parent) { } + class TabletWidget : public QWidget + { + public: + using QWidget::QWidget; - int tabletEventCount = 0; - int pressEventCount = 0; - int moveEventCount = 0; - int releaseEventCount = 0; - int trackingChangeEventCount = 0; - qint64 uid = -1; + int tabletEventCount = 0; + int pressEventCount = 0; + int moveEventCount = 0; + int releaseEventCount = 0; + int trackingChangeEventCount = 0; + qint64 uid = -1; -protected: - void tabletEvent(QTabletEvent *event) override { - ++tabletEventCount; - uid = event->pointingDevice()->uniqueId().numericId(); - switch (event->type()) { - case QEvent::TabletMove: - ++moveEventCount; - break; - case QEvent::TabletPress: - ++pressEventCount; - break; - case QEvent::TabletRelease: - ++releaseEventCount; - break; - default: - break; + protected: + void tabletEvent(QTabletEvent *event) override { + ++tabletEventCount; + uid = event->pointingDevice()->uniqueId().numericId(); + switch (event->type()) { + case QEvent::TabletMove: + ++moveEventCount; + break; + case QEvent::TabletPress: + ++pressEventCount; + break; + case QEvent::TabletRelease: + ++releaseEventCount; + break; + default: + break; + } } - } - bool event(QEvent *ev) override { - if (ev->type() == QEvent::TabletTrackingChange) - ++trackingChangeEventCount; - return QWidget::event(ev); - } -}; - -void tst_QWidget::tabletTracking() -{ - QWidget parent; - parent.setWindowTitle(QLatin1String(QTest::currentTestFunction())); - parent.resize(200,200); - // QWidgetWindow::handleTabletEvent doesn't deliver tablet events to the window's widget, only to a child. - // So it doesn't do any good to show a TabletWidget directly: it needs a parent. - TabletWidget widget(&parent); + bool event(QEvent *ev) override { + if (ev->type() == QEvent::TabletTrackingChange) + ++trackingChangeEventCount; + return QWidget::event(ev); + } + } widget; + widget.setWindowTitle(QLatin1String(QTest::currentTestFunction())); widget.resize(200,200); - parent.showNormal(); - QVERIFY(QTest::qWaitForWindowExposed(&parent)); + widget.showNormal(); + QVERIFY(QTest::qWaitForWindowExposed(&widget)); widget.setAttribute(Qt::WA_TabletTracking); QTRY_COMPARE(widget.trackingChangeEventCount, 1); QVERIFY(widget.hasTabletTracking()); - QWindow *window = parent.windowHandle(); + QWindow *window = widget.windowHandle(); QPointF local(10, 10); QPointF global = window->mapToGlobal(local.toPoint()); QPointF deviceLocal = QHighDpi::toNativeLocalPosition(local, window); @@ -11721,6 +13211,36 @@ void tst_QWidget::closeEvent() widget.windowHandle()->close(); widget.windowHandle()->close(); QCOMPARE(widget.closeCount, 1); + + CloseCountingWidget widget2; + widget2.show(); + QVERIFY(QTest::qWaitForWindowExposed(&widget2)); + widget2.close(); + widget2.close(); + QCOMPARE(widget2.closeCount, 1); + widget2.closeCount = 0; + + widget2.show(); + QVERIFY(QTest::qWaitForWindowExposed(&widget2)); + widget2.close(); + QCOMPARE(widget2.closeCount, 1); + + CloseCountingWidget widget3; + widget3.close(); + widget3.close(); + QEXPECT_FAIL("", "Closing a widget without a window will unconditionally send close events", Continue); + QCOMPARE(widget3.closeCount, 0); + + QWidget parent; + CloseCountingWidget child; + child.setParent(&parent); + parent.show(); + QVERIFY(QTest::qWaitForWindowExposed(&parent)); + child.close(); + QCOMPARE(child.closeCount, 1); + child.close(); + QEXPECT_FAIL("", "Closing a widget without a window will unconditionally send close events", Continue); + QCOMPARE(child.closeCount, 1); } void tst_QWidget::closeWithChildWindow() @@ -11922,10 +13442,602 @@ protected: void tst_QWidget::deleteWindowInCloseEvent() { - // Just checking if closing this widget causes a crash +#ifdef Q_OS_ANDROID + QSKIP("This test crashes on Android"); +#endif + QSignalSpy quitSpy(qApp, &QGuiApplication::lastWindowClosed); + + // Closing this widget should not cause a crash auto widget = new DeleteOnCloseEventWidget; - widget->close(); - QVERIFY(true); + widget->show(); + QVERIFY(QTest::qWaitForWindowExposed(widget)); + QTimer::singleShot(0, widget, [&]{ + widget->close(); + }); + QApplication::exec(); + + // It should still result in a single lastWindowClosed emit + QCOMPARE(quitSpy.size(), 1); +} + +/*! + Verify that both closing and deleting the last (only) window-widget + exits the application event loop. +*/ +void tst_QWidget::quitOnClose() +{ + QSignalSpy quitSpy(qApp, &QGuiApplication::lastWindowClosed); + + std::unique_ptr<QWidget>widget(new QWidget); + widget->show(); + QVERIFY(QTest::qWaitForWindowExposed(widget.get())); + + // QGuiApplication::lastWindowClosed is documented to only be emitted + // when we are in exec() + QTimer::singleShot(0, widget.get(), [&]{ + widget->close(); + }); + QApplication::exec(); + QCOMPARE(quitSpy.size(), 1); + + widget->show(); + QVERIFY(QTest::qWaitForWindowExposed(widget.get())); + QTimer::singleShot(0, widget.get(), [&]{ + widget.reset(); + }); + QApplication::exec(); + QCOMPARE(quitSpy.size(), 2); +} + +void tst_QWidget::setParentChangesFocus_data() +{ + QTest::addColumn<Qt::WindowType>("initialType"); + QTest::addColumn<bool>("initialParent"); + QTest::addColumn<Qt::WindowType>("targetType"); + QTest::addColumn<bool>("targetParent"); + QTest::addColumn<bool>("reparentBeforeShow"); + QTest::addColumn<QString>("focusWidget"); + + for (const bool before : {true, false}) { + const char *tag = before ? "before" : "after"; + QTest::addRow("give dialog parent, %s", tag) + << Qt::Dialog << false << Qt::Dialog << true << before << "lineEdit"; + QTest::addRow("make dialog parentless, %s", tag) + << Qt::Dialog << true << Qt::Dialog << false << before << "lineEdit"; + QTest::addRow("dialog to sheet, %s", tag) + << Qt::Dialog << true << Qt::Sheet << true << before << "lineEdit"; + QTest::addRow("window to widget, %s", tag) + << Qt::Window << true << Qt::Widget << true << before << "windowEdit"; + QTest::addRow("widget to window, %s", tag) + << Qt::Widget << true << Qt::Window << true << before << "lineEdit"; + } +} + +void tst_QWidget::setParentChangesFocus() +{ + QFETCH(Qt::WindowType, initialType); + QFETCH(bool, initialParent); + QFETCH(Qt::WindowType, targetType); + QFETCH(bool, targetParent); + QFETCH(bool, reparentBeforeShow); + QFETCH(QString, focusWidget); + + QWidget window; + window.setObjectName("window"); + QLineEdit *windowEdit = new QLineEdit(&window); + windowEdit->setObjectName("windowEdit"); + windowEdit->setFocus(); + + std::unique_ptr<QWidget> secondary(new QWidget(initialParent ? &window : nullptr, initialType)); + secondary->setObjectName("secondary"); + QLineEdit *lineEdit = new QLineEdit(secondary.get()); + lineEdit->setObjectName("lineEdit"); + QPushButton *pushButton = new QPushButton(secondary.get()); + pushButton->setObjectName("pushButton"); + lineEdit->setFocus(); + + window.show(); + QVERIFY(QTest::qWaitForWindowActive(&window)); + + if (reparentBeforeShow) { + secondary->setParent(targetParent ? &window : nullptr, targetType); + // making a widget into a window doesn't set a focusWidget until shown + if (secondary->focusWidget()) + QCOMPARE(secondary->focusWidget()->objectName(), focusWidget); + } + secondary->show(); + QApplicationPrivate::setActiveWindow(secondary.get()); + QVERIFY(QTest::qWaitForWindowActive(secondary.get())); + + if (!reparentBeforeShow) { + secondary->setParent(targetParent ? &window : nullptr, targetType); + secondary->show(); // reparenting hides, so show again + QApplicationPrivate::setActiveWindow(secondary.get()); + QVERIFY(QTest::qWaitForWindowActive(secondary.get())); + } + QVERIFY(QTest::qWaitFor([]{ return QApplication::focusWidget(); })); + QCOMPARE(QApplication::focusWidget()->objectName(), focusWidget); +} + +void tst_QWidget::activateWhileModalHidden() +{ + if (!QGuiApplicationPrivate::platformIntegration()->hasCapability(QPlatformIntegration::WindowActivation)) + QSKIP("QWindow::requestActivate() is not supported."); + + QDialog dialog; + dialog.setWindowModality(Qt::ApplicationModal); + dialog.show(); + QVERIFY(QTest::qWaitForWindowActive(&dialog)); + QVERIFY(dialog.isActiveWindow()); + QCOMPARE(QApplication::activeWindow(), &dialog); + + dialog.hide(); + QTRY_VERIFY(!dialog.isVisible()); + + QMainWindow window; + window.show(); + QVERIFY(QTest::qWaitForWindowActive(&window)); + QVERIFY(window.isActiveWindow()); + QCOMPARE(QApplication::activeWindow(), &window); +} + +// Create a simple palette to prevent multiple paint events +QPalette tst_QWidget::simplePalette() +{ + static QPalette simplePalette = []{ + const QColor windowText = Qt::black; + const QColor backGround = QColor(239, 239, 239); + const QColor light = backGround.lighter(150); + const QColor mid = (backGround.darker(130)); + const QColor midLight = mid.lighter(110); + const QColor base = Qt::white; + const QColor dark = backGround.darker(150); + const QColor text = Qt::black; + const QColor highlight = QColor(48, 140, 198); + const QColor hightlightedText = Qt::white; + const QColor button = backGround; + const QColor shadow = dark.darker(135); + + QPalette defaultPalette(windowText, backGround, light, dark, mid, text, base); + defaultPalette.setBrush(QPalette::Midlight, midLight); + defaultPalette.setBrush(QPalette::Button, button); + defaultPalette.setBrush(QPalette::Shadow, shadow); + defaultPalette.setBrush(QPalette::HighlightedText, hightlightedText); + defaultPalette.setBrush(QPalette::Active, QPalette::Highlight, highlight); + return defaultPalette; + }(); + + return simplePalette; +} + +#ifdef Q_OS_ANDROID +void tst_QWidget::showFullscreenAndroid() +{ + QWidget w; + w.setAutoFillBackground(true); + QPalette p = w.palette(); + p.setColor(QPalette::Window, Qt::red); + w.setPalette(p); + + // Need to toggle showFullScreen() twice, see QTBUG-101968 + w.showFullScreen(); + QVERIFY(QTest::qWaitForWindowExposed(&w)); + w.show(); + QVERIFY(QTest::qWaitForWindowExposed(&w)); + w.showFullScreen(); + QVERIFY(QTest::qWaitForWindowExposed(&w)); + + // Make sure that the lower part of the screen contains the red widget, not + // the buttons. + + const QRect fullGeometry = w.screen()->geometry(); + // Take a rect of (20 x 20) from the bottom area + const QRect grabArea(10, fullGeometry.height() - 30, 20, 20); + const QImage img = grabFromWidget(&w, grabArea).toImage().convertedTo(QImage::Format_RGB32); + + QPixmap expectedPix(20, 20); + expectedPix.fill(Qt::red); + const QImage expectedImg = expectedPix.toImage().convertedTo(QImage::Format_RGB32); + + QCOMPARE(img, expectedImg); +} +#endif // Q_OS_ANDROID + +void tst_QWidget::setVisibleDuringDestruction() +{ + CreateDestroyWidget widget; + widget.create(); + QVERIFY(widget.windowHandle()); + + QSignalSpy signalSpy(widget.windowHandle(), &QWindow::visibleChanged); + EventSpy<QWindow> showEventSpy(widget.windowHandle(), QEvent::Show); + widget.show(); + QTRY_COMPARE(showEventSpy.count(), 1); + QTRY_COMPARE(signalSpy.count(), 1); + + EventSpy<QWindow> hideEventSpy(widget.windowHandle(), QEvent::Hide); + widget.hide(); + QTRY_COMPARE(hideEventSpy.count(), 1); + QTRY_COMPARE(signalSpy.count(), 2); + + widget.show(); + QTRY_COMPARE(showEventSpy.count(), 2); + QTRY_COMPARE(signalSpy.count(), 3); + + widget.destroy(); + QTRY_COMPARE(hideEventSpy.count(), 2); + QTRY_COMPARE(signalSpy.count(), 4); +} + +void tst_QWidget::explicitShowHide() +{ + { + QWidget parent; + parent.setObjectName("Parent"); + QWidget child(&parent); + child.setObjectName("Child"); + + QCOMPARE(parent.testAttribute(Qt::WA_WState_ExplicitShowHide), false); + QCOMPARE(parent.testAttribute(Qt::WA_WState_Hidden), true); + QCOMPARE(child.testAttribute(Qt::WA_WState_ExplicitShowHide), false); + QCOMPARE(child.testAttribute(Qt::WA_WState_Hidden), false); + + parent.show(); + QCOMPARE(parent.testAttribute(Qt::WA_WState_ExplicitShowHide), true); + QCOMPARE(parent.testAttribute(Qt::WA_WState_Hidden), false); + QCOMPARE(child.testAttribute(Qt::WA_WState_ExplicitShowHide), false); + QCOMPARE(child.testAttribute(Qt::WA_WState_Hidden), false); + + // Fix up earlier expected failure + child.setAttribute(Qt::WA_WState_ExplicitShowHide, false); + + parent.hide(); + QCOMPARE(parent.testAttribute(Qt::WA_WState_ExplicitShowHide), true); + QCOMPARE(parent.testAttribute(Qt::WA_WState_Hidden), true); + QCOMPARE(child.testAttribute(Qt::WA_WState_ExplicitShowHide), false); + QCOMPARE(child.testAttribute(Qt::WA_WState_Hidden), false); + } + + { + // Test what happens when a child is reparented after showing it + + QWidget parent; + parent.setObjectName("Parent"); + QWidget child; + child.setObjectName("Child"); + + child.show(); + QCOMPARE(child.isVisible(), true); + QCOMPARE(child.testAttribute(Qt::WA_WState_ExplicitShowHide), true); + QCOMPARE(child.testAttribute(Qt::WA_WState_Hidden), false); + + child.setParent(&parent); + // As documented, a widget becomes invisible as part of changing + // its parent, even if it was previously visible. The user must call + // show() to make the widget visible again. + QCOMPARE(child.isVisible(), false); + + // However, the widget does not end up with Qt::WA_WState_Hidden, + // as QWidget::setParent treats it as a child, which normally will + // not get Qt::WA_WState_Hidden out of the box. + QCOMPARE(child.testAttribute(Qt::WA_WState_Hidden), false); + + // For some reason we reset WA_WState_ExplicitShowHide, and it's + // not clear whether this is correct or not See QWidget::setParent() + // for a comment with more details. + QEXPECT_FAIL("", "We reset WA_WState_ExplicitShowHide on widget re-parent", Continue); + QCOMPARE(child.testAttribute(Qt::WA_WState_ExplicitShowHide), true); + + // The fact that the child doesn't have Qt::WA_WState_Hidden means + // it's sufficient to show the parent widget. We don't need to + // explicitly show the child. + parent.show(); + QCOMPARE(child.isVisible(), true); + QCOMPARE(child.testAttribute(Qt::WA_WState_Hidden), false); + } + + { + QWidget parent; + parent.setObjectName("Parent"); + QWidget child(&parent); + child.setObjectName("Child"); + + parent.show(); + + // If a non-native child ends up being closed, we will hide the + // widget, but do so via QWidget::hide(), which marks the widget + // as explicitly hidden. + + child.setAttribute(Qt::WA_WState_ExplicitShowHide, false); + QCOMPARE(child.close(), true); + QCOMPARE(child.testAttribute(Qt::WA_WState_ExplicitShowHide), true); + QCOMPARE(child.testAttribute(Qt::WA_WState_Hidden), true); + + child.show(); + child.setAttribute(Qt::WA_NativeWindow); + child.setAttribute(Qt::WA_WState_ExplicitShowHide, false); + QCOMPARE(child.close(), true); + QCOMPARE(child.testAttribute(Qt::WA_WState_ExplicitShowHide), true); + QCOMPARE(child.testAttribute(Qt::WA_WState_Hidden), true); + + child.show(); + child.setAttribute(Qt::WA_WState_ExplicitShowHide, false); + QCOMPARE(child.windowHandle()->close(), false); // Can't close non-top level QWindows + QCOMPARE(child.testAttribute(Qt::WA_WState_ExplicitShowHide), false); + QCOMPARE(child.testAttribute(Qt::WA_WState_Hidden), false); + + // If we end up in QWidgetPrivate::handleClose via QWidgetWindow::closeEvent, + // either through QWindow::close(), or via QWSI::handleCloseEvent, we'll still + // do the explicit hide. + + child.show(); + child.setAttribute(Qt::WA_WState_ExplicitShowHide, false); + QCOMPARE(QWindowSystemInterface::handleCloseEvent<QWindowSystemInterface::SynchronousDelivery>( + child.windowHandle()), true); + QEXPECT_FAIL("", "Closing a native child via QWSI is treated as an explicit hide", Continue); + QCOMPARE(child.testAttribute(Qt::WA_WState_ExplicitShowHide), false); + QCOMPARE(child.testAttribute(Qt::WA_WState_Hidden), true); + } + + { + QWidget widget; + widget.show(); + widget.hide(); + + QCOMPARE(widget.testAttribute(Qt::WA_WState_Visible), false); + QCOMPARE(widget.testAttribute(Qt::WA_WState_ExplicitShowHide), true); + QCOMPARE(widget.testAttribute(Qt::WA_WState_Hidden), true); + + // The widget is now explicitly hidden. Showing it again, via QWindow, + // should make the widget visible, and it should not stay hidden, as + // that's an invalid state for a widget. + + widget.windowHandle()->setVisible(true); + QCOMPARE(widget.testAttribute(Qt::WA_WState_Visible), true); + QCOMPARE(widget.testAttribute(Qt::WA_WState_ExplicitShowHide), true); + QCOMPARE(widget.testAttribute(Qt::WA_WState_Hidden), false); + } +} + +/*! + Verify that we deliver DragEnter/Leave events symmetrically, even if the + widget entered didn't accept the DragEnter event. +*/ +void tst_QWidget::dragEnterLeaveSymmetry() +{ + QWidget widget; + widget.setAcceptDrops(true); + QLineEdit lineEdit; + QLabel label("Hello world"); + label.setAcceptDrops(true); + + struct EventFilter : QObject + { + bool eventFilter(QObject *receiver, QEvent *event) override + { + switch (event->type()) { + case QEvent::DragEnter: + case QEvent::DragLeave: + receivers[event->type()] << receiver; + break; + + default: + break; + } + + return false; + } + + QMap<QEvent::Type, QList<QObject *>> receivers; + + void clear() { receivers.clear(); } + bool hasEntered(QWidget *widget) const + { + return receivers.value(QEvent::DragEnter).contains(widget); + } + bool hasLeft(QWidget *widget) const + { + return receivers.value(QEvent::DragLeave).contains(widget); + } + } filter; + + widget.installEventFilter(&filter); + lineEdit.installEventFilter(&filter); + label.installEventFilter(&filter); + + QVBoxLayout vbox; + vbox.setContentsMargins(10, 10, 10, 10); + vbox.addWidget(&lineEdit); + vbox.addWidget(&label); + widget.setLayout(&vbox); + + widget.show(); + QVERIFY(QTest::qWaitForWindowExposed(&widget)); + + QMimeData data; + data.setColorData(QVariant::fromValue(Qt::red)); + QWindowSystemInterface::handleDrag(widget.windowHandle(), &data, QPoint(1, 1), + Qt::ActionMask, Qt::LeftButton, {}); + QVERIFY(filter.hasEntered(&widget)); + QVERIFY(!filter.hasEntered(&lineEdit)); + QVERIFY(!filter.hasEntered(&label)); + QVERIFY(widget.underMouse()); + QVERIFY(!lineEdit.underMouse()); + filter.clear(); + + QWindowSystemInterface::handleDrag(widget.windowHandle(), &data, lineEdit.geometry().center(), + Qt::ActionMask, Qt::LeftButton, {}); + // DragEnter propagates as the lineEdit doesn't want it, so the widget + // sees both a Leave and an Enter event + QVERIFY(filter.hasLeft(&widget)); + QVERIFY(filter.hasEntered(&widget)); + QVERIFY(filter.hasEntered(&widget)); + // both have the UnderMouse attribute set + QVERIFY(lineEdit.underMouse()); + QVERIFY(widget.underMouse()); + + // The lineEdit didn't accept the DragEnter, but it should still has to + // get the DragLeave so that UnderMouse is cleared; the widget gets both + // Leave and Enter through propagation. + QWindowSystemInterface::handleDrag(widget.windowHandle(), &data, label.geometry().center(), + Qt::ActionMask, Qt::LeftButton, {}); + QVERIFY(filter.hasLeft(&lineEdit)); + QVERIFY(filter.hasLeft(&widget)); + QVERIFY(filter.hasEntered(&label)); + QVERIFY(filter.hasEntered(&widget)); + + QVERIFY(!lineEdit.underMouse()); + QVERIFY(label.underMouse()); + QVERIFY(widget.underMouse()); +} + +void tst_QWidget::reparentWindowHandles_data() +{ + QTest::addColumn<int>("stage"); + QTest::addRow("reparent child") << 1; + QTest::addRow("top level to child") << 2; + QTest::addRow("transient parent") << 3; + QTest::addRow("window container") << 4; +} + +void tst_QWidget::reparentWindowHandles() +{ + const bool nativeSiblingsOriginal = qApp->testAttribute(Qt::AA_DontCreateNativeWidgetSiblings); + qApp->setAttribute(Qt::AA_DontCreateNativeWidgetSiblings, true); + auto nativeSiblingGuard = qScopeGuard([&]{ + qApp->setAttribute(Qt::AA_DontCreateNativeWidgetSiblings, nativeSiblingsOriginal); + }); + + QFETCH(int, stage); + + switch (stage) { + case 1: { + // Reparent child widget + + QWidget topLevel; + topLevel.setAttribute(Qt::WA_NativeWindow); + QVERIFY(topLevel.windowHandle()); + QPointer<QWidget> child = new QWidget(&topLevel); + child->setAttribute(Qt::WA_DontCreateNativeAncestors); + child->setAttribute(Qt::WA_NativeWindow); + QVERIFY(child->windowHandle()); + + QWidget anotherTopLevel; + anotherTopLevel.setAttribute(Qt::WA_NativeWindow); + QVERIFY(anotherTopLevel.windowHandle()); + QPointer<QWidget> intermediate = new QWidget(&anotherTopLevel); + QPointer<QWidget> leaf = new QWidget(intermediate); + leaf->setAttribute(Qt::WA_DontCreateNativeAncestors); + leaf->setAttribute(Qt::WA_NativeWindow); + QVERIFY(leaf->windowHandle()); + QVERIFY(!intermediate->windowHandle()); + + // Reparenting a native widget should reparent the QWindow + child->setParent(leaf); + QCOMPARE(child->windowHandle()->parent(), leaf->windowHandle()); + QCOMPARE(child->windowHandle()->transientParent(), nullptr); + QVERIFY(!intermediate->windowHandle()); + + // So should reparenting a non-native widget with native children + intermediate->setParent(&topLevel); + QVERIFY(!intermediate->windowHandle()); + QCOMPARE(leaf->windowHandle()->parent(), topLevel.windowHandle()); + QCOMPARE(leaf->windowHandle()->transientParent(), nullptr); + QCOMPARE(child->windowHandle()->parent(), leaf->windowHandle()); + QCOMPARE(child->windowHandle()->transientParent(), nullptr); + } + break; + case 2: { + // Top level to child + + QWidget topLevel; + topLevel.setAttribute(Qt::WA_NativeWindow); + + // A regular top level loses its nativeness + QPointer<QWidget> regularToplevel = new QWidget; + regularToplevel->show(); + QVERIFY(QTest::qWaitForWindowExposed(regularToplevel)); + QVERIFY(regularToplevel->windowHandle()); + regularToplevel->setParent(&topLevel); + QVERIFY(!regularToplevel->windowHandle()); + + // A regular top level loses its nativeness + QPointer<QWidget> regularToplevelWithNativeChildren = new QWidget; + QPointer<QWidget> nativeChild = new QWidget(regularToplevelWithNativeChildren); + nativeChild->setAttribute(Qt::WA_DontCreateNativeAncestors); + nativeChild->setAttribute(Qt::WA_NativeWindow); + QVERIFY(nativeChild->windowHandle()); + regularToplevelWithNativeChildren->show(); + QVERIFY(QTest::qWaitForWindowExposed(regularToplevelWithNativeChildren)); + QVERIFY(regularToplevelWithNativeChildren->windowHandle()); + regularToplevelWithNativeChildren->setParent(&topLevel); + QVERIFY(!regularToplevelWithNativeChildren->windowHandle()); + // But the native child does not + QVERIFY(nativeChild->windowHandle()); + QCOMPARE(nativeChild->windowHandle()->parent(), topLevel.windowHandle()); + + // An explicitly native top level keeps its nativeness, and the window handle moves + QPointer<QWidget> nativeTopLevel = new QWidget; + nativeTopLevel->setAttribute(Qt::WA_NativeWindow); + QVERIFY(nativeTopLevel->windowHandle()); + nativeTopLevel->setParent(&topLevel); + QVERIFY(nativeTopLevel->windowHandle()); + QCOMPARE(nativeTopLevel->windowHandle()->parent(), topLevel.windowHandle()); + } + break; + case 3: { + // Transient parent + + QWidget topLevel; + topLevel.setAttribute(Qt::WA_NativeWindow); + QVERIFY(topLevel.windowHandle()); + QPointer<QWidget> child = new QWidget(&topLevel); + child->setAttribute(Qt::WA_NativeWindow); + QVERIFY(child->windowHandle()); + + QWidget anotherTopLevel; + anotherTopLevel.setAttribute(Qt::WA_NativeWindow); + QVERIFY(anotherTopLevel.windowHandle()); + + // Make transient child of top level + anotherTopLevel.setParent(&topLevel, Qt::Window); + QCOMPARE(anotherTopLevel.windowHandle()->parent(), nullptr); + QCOMPARE(anotherTopLevel.windowHandle()->transientParent(), topLevel.windowHandle()); + + // Make transient child of child + anotherTopLevel.setParent(child, Qt::Window); + QCOMPARE(anotherTopLevel.windowHandle()->parent(), nullptr); + QCOMPARE(anotherTopLevel.windowHandle()->transientParent(), topLevel.windowHandle()); + } + break; + case 4: { + // Window container + + QWidget topLevel; + topLevel.setAttribute(Qt::WA_NativeWindow); + QVERIFY(topLevel.windowHandle()); + + QPointer<QWidget> child = new QWidget(&topLevel); + QVERIFY(!child->windowHandle()); + + QWindow *window = new QWindow; + QWidget *container = QWidget::createWindowContainer(window); + container->setParent(child); + topLevel.show(); + QVERIFY(QTest::qWaitForWindowExposed(&topLevel)); + QCOMPARE(window->parent(), topLevel.windowHandle()); + + QWidget anotherTopLevel; + anotherTopLevel.setAttribute(Qt::WA_NativeWindow); + QVERIFY(anotherTopLevel.windowHandle()); + + child->setParent(&anotherTopLevel); + QCOMPARE(window->parent(), anotherTopLevel.windowHandle()); + } + break; + default: + Q_UNREACHABLE(); + } } QTEST_MAIN(tst_QWidget) |