diff options
Diffstat (limited to 'tests/auto/widgets/kernel/qwidget')
14 files changed, 2096 insertions, 522 deletions
diff --git a/tests/auto/widgets/kernel/qwidget/BLACKLIST b/tests/auto/widgets/kernel/qwidget/BLACKLIST index 880ec6f9b4..4c17af245b 100644 --- a/tests/auto/widgets/kernel/qwidget/BLACKLIST +++ b/tests/auto/widgets/kernel/qwidget/BLACKLIST @@ -1,65 +1,49 @@ -# OSX QTBUG-25300 QTBUG-45502 -[normalGeometry] -ubuntu-16.04 -[restoreVersion1Geometry] -ubuntu-16.04 -[focusProxyAndInputMethods] -rhel-7.6 -centos -opensuse-leap -ubuntu [raise] opensuse-leap -# QTBUG-68175 -opensuse-42.3 -[renderInvisible] -macos -[optimizedResizeMove] -osx [optimizedResize_topLevel] osx [render_windowOpacity] macos arm [render_systemClip] osx -[maskedUpdate] -opensuse -opensuse-leap -[moveInResizeEvent] -ubuntu-16.04 [multipleToplevelFocusCheck] -rhel-7.6 centos opensuse-leap ubuntu sles-15 - # QTBUG-87668 -[reparent] -android -[windowState] -android [showMinimizedKeepsFocus] android +macos-13 ci +macos-14 ci [normalGeometry] android [saveRestoreGeometry] android [optimizedResizeMove] android -[resizeEvent] -android [update] android [scroll] android -[setWindowGeometry] +[moveChild] android -[windowMoveResize] +[multipleToplevelFocusCheck] android -[moveChild] +[renderInvisible] android -[showAndMoveChild] +[updateWhileMinimized] android -[multipleToplevelFocusCheck] +[doubleRepaint] +android +[setMaskInResizeEvent] +android +[activateWindow] +android +[optimizedResize_topLevel] +android +[hoverPosition] +macos-14 x86 +# QTBUG-124291 +[setParentChangesFocus:make dialog parentless, after] android diff --git a/tests/auto/widgets/kernel/qwidget/CMakeLists.txt b/tests/auto/widgets/kernel/qwidget/CMakeLists.txt index d04db40e87..f18bc98bb3 100644 --- a/tests/auto/widgets/kernel/qwidget/CMakeLists.txt +++ b/tests/auto/widgets/kernel/qwidget/CMakeLists.txt @@ -1,20 +1,15 @@ -# Generated from qwidget.pro. +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: BSD-3-Clause ##################################################################### ## tst_qwidget Test: ##################################################################### -qt_internal_add_test(tst_qwidget - SOURCES - tst_qwidget.cpp - PUBLIC_LIBRARIES - Qt::CorePrivate - Qt::Gui - Qt::GuiPrivate - Qt::TestPrivate - Qt::Widgets - Qt::WidgetsPrivate -) +if(NOT QT_BUILD_STANDALONE_TESTS AND NOT QT_BUILDING_QT) + cmake_minimum_required(VERSION 3.16) + project(tst_qwidget LANGUAGES CXX) + find_package(Qt6BuildInternals REQUIRED COMPONENTS STANDALONE_TEST) +endif() # Resources: set(qwidget_resource_files @@ -24,17 +19,20 @@ set(qwidget_resource_files "hellotr_la.qm" ) -qt_internal_add_resource(tst_qwidget "qwidget" - PREFIX - "/" - FILES - ${qwidget_resource_files} +qt_internal_add_test(tst_qwidget + SOURCES + tst_qwidget.cpp + LIBRARIES + Qt::CorePrivate + Qt::Gui + Qt::GuiPrivate + Qt::TestPrivate + Qt::Widgets + Qt::WidgetsPrivate + TESTDATA ${qwidget_resource_files} + BUILTIN_TESTDATA ) - -#### Keys ignored in scope 1:.:.:qwidget.pro:<TRUE>: -# testcase.timeout = "600" - ## Scopes: ##################################################################### @@ -43,16 +41,8 @@ qt_internal_extend_target(tst_qwidget CONDITION AIX -fpermissive ) -qt_internal_extend_target(tst_qwidget CONDITION APPLE - SOURCES - tst_qwidget_mac_helpers.mm - PUBLIC_LIBRARIES - ${FWAppKit} - ${FWSecurity} -) - qt_internal_extend_target(tst_qwidget CONDITION WIN32 - PUBLIC_LIBRARIES + LIBRARIES gdi32 user32 ) diff --git a/tests/auto/widgets/kernel/qwidget/qwidget.qrc b/tests/auto/widgets/kernel/qwidget/qwidget.qrc deleted file mode 100644 index 597ea872a5..0000000000 --- a/tests/auto/widgets/kernel/qwidget/qwidget.qrc +++ /dev/null @@ -1,8 +0,0 @@ -<!DOCTYPE RCC><RCC version="1.0"> - <qresource> - <file>geometry.dat</file> - <file>geometry-maximized.dat</file> - <file>geometry-fullscreen.dat</file> - <file>hellotr_la.qm</file> - </qresource> -</RCC> diff --git a/tests/auto/widgets/kernel/qwidget/testdata/paintEvent/res_Motif_data0.qsnap b/tests/auto/widgets/kernel/qwidget/testdata/paintEvent/res_Motif_data0.qsnap Binary files differdeleted file mode 100644 index b3473cdefe..0000000000 --- a/tests/auto/widgets/kernel/qwidget/testdata/paintEvent/res_Motif_data0.qsnap +++ /dev/null diff --git a/tests/auto/widgets/kernel/qwidget/testdata/paintEvent/res_Motif_data1.qsnap b/tests/auto/widgets/kernel/qwidget/testdata/paintEvent/res_Motif_data1.qsnap Binary files differdeleted file mode 100644 index 10007733ca..0000000000 --- a/tests/auto/widgets/kernel/qwidget/testdata/paintEvent/res_Motif_data1.qsnap +++ /dev/null diff --git a/tests/auto/widgets/kernel/qwidget/testdata/paintEvent/res_Motif_data2.qsnap b/tests/auto/widgets/kernel/qwidget/testdata/paintEvent/res_Motif_data2.qsnap Binary files differdeleted file mode 100644 index cde5964a30..0000000000 --- a/tests/auto/widgets/kernel/qwidget/testdata/paintEvent/res_Motif_data2.qsnap +++ /dev/null diff --git a/tests/auto/widgets/kernel/qwidget/testdata/paintEvent/res_Motif_data3.qsnap b/tests/auto/widgets/kernel/qwidget/testdata/paintEvent/res_Motif_data3.qsnap Binary files differdeleted file mode 100644 index 23ea1410e4..0000000000 --- a/tests/auto/widgets/kernel/qwidget/testdata/paintEvent/res_Motif_data3.qsnap +++ /dev/null diff --git a/tests/auto/widgets/kernel/qwidget/testdata/paintEvent/res_Windows_data0.qsnap b/tests/auto/widgets/kernel/qwidget/testdata/paintEvent/res_Windows_data0.qsnap Binary files differdeleted file mode 100644 index a8918c1d1b..0000000000 --- a/tests/auto/widgets/kernel/qwidget/testdata/paintEvent/res_Windows_data0.qsnap +++ /dev/null diff --git a/tests/auto/widgets/kernel/qwidget/testdata/paintEvent/res_Windows_data1.qsnap b/tests/auto/widgets/kernel/qwidget/testdata/paintEvent/res_Windows_data1.qsnap Binary files differdeleted file mode 100644 index 0981cf5dd1..0000000000 --- a/tests/auto/widgets/kernel/qwidget/testdata/paintEvent/res_Windows_data1.qsnap +++ /dev/null diff --git a/tests/auto/widgets/kernel/qwidget/testdata/paintEvent/res_Windows_data2.qsnap b/tests/auto/widgets/kernel/qwidget/testdata/paintEvent/res_Windows_data2.qsnap Binary files differdeleted file mode 100644 index 75d09136cf..0000000000 --- a/tests/auto/widgets/kernel/qwidget/testdata/paintEvent/res_Windows_data2.qsnap +++ /dev/null diff --git a/tests/auto/widgets/kernel/qwidget/testdata/paintEvent/res_Windows_data3.qsnap b/tests/auto/widgets/kernel/qwidget/testdata/paintEvent/res_Windows_data3.qsnap Binary files differdeleted file mode 100644 index f58f74d030..0000000000 --- a/tests/auto/widgets/kernel/qwidget/testdata/paintEvent/res_Windows_data3.qsnap +++ /dev/null diff --git a/tests/auto/widgets/kernel/qwidget/tst_qwidget.cpp b/tests/auto/widgets/kernel/qwidget/tst_qwidget.cpp index 27cf884fd5..666cb70da9 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> @@ -62,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> @@ -74,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> @@ -147,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 @@ -160,7 +163,9 @@ public: public slots: void initTestCase(); void cleanup(); + private slots: + void nativeWindowAttribute(); void addActionOverloads(); void getSetCheck(); void fontPropagation(); @@ -186,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(); @@ -197,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(); @@ -220,6 +234,8 @@ private slots: void saveRestoreGeometry(); void restoreVersion1Geometry_data(); void restoreVersion1Geometry(); + void restoreGeometryAfterScreenChange_data(); + void restoreGeometryAfterScreenChange(); void widgetAt(); #ifdef Q_OS_MACOS @@ -240,13 +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(); @@ -302,6 +319,7 @@ private slots: void renderTargetOffset(); void renderInvisible(); void renderWithPainter(); + void renderRTL(); void render_task188133(); void render_task211796(); void render_task217815(); @@ -334,6 +352,8 @@ private slots: void resizeInPaintEvent(); void opaqueChildren(); + void dumpObjectTree(); + void setMaskInResizeEvent(); void moveInResizeEvent(); @@ -365,6 +385,7 @@ private slots: void enterLeaveOnWindowShowHide_data(); void enterLeaveOnWindowShowHide(); void taskQTBUG_4055_sendSyntheticEnterLeave(); + void hoverPosition(); void underMouse(); void taskQTBUG_27643_enterEvents(); #endif @@ -376,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(); @@ -386,7 +409,8 @@ private slots: void openModal_taskQTBUG_5804(); void focusProxy(); - void focusProxyAndInputMethods(); + void imEnabledNotImplemented(); + #ifdef QT_BUILD_INTERNAL void scrollWithoutBackingStore(); #endif @@ -440,6 +464,23 @@ private slots: void activateWhileModalHidden(); +#ifdef Q_OS_ANDROID + void showFullscreenAndroid(); +#endif + + void setVisibleDuringDestruction(); + + void explicitShowHide(); + + void dragEnterLeaveSymmetry(); + + void reparentWindowHandles_data(); + void reparentWindowHandles(); + +#ifndef QT_NO_CONTEXTMENU + void contextMenuTrigger(); +#endif + private: const QString m_platform; QSize m_testWidgetSize; @@ -448,6 +489,16 @@ private: const bool m_windowsAnimationsEnabled; QPointingDevice *m_touchScreen; const int m_fuzz; + QPalette simplePalette(); + +private: + enum class ScreenPosition { + OffAbove, + OffLeft, + OffBelow, + OffRight, + Contained + }; }; // Testing get/set functions @@ -464,8 +515,8 @@ 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$"_qs); - const QRegularExpression largestAllowedSize(u"^.*The largest allowed size is \\(.*\\)$"_qs); + 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); @@ -606,7 +657,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 } @@ -643,6 +694,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; @@ -675,6 +730,24 @@ struct ImplicitlyConvertibleTo { 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: @@ -1829,6 +1902,7 @@ void tst_QWidget::focusChainOnReparent() } QWidget window2; + child22->setParent(child21); child2->setParent(&window2); QWidget *expectedNewChain[5] = {&window2, child2, child21, child22, &window2}; @@ -1869,8 +1943,6 @@ void tst_QWidget::focusChainOnHide() QWidget::setTabOrder(child, parent.data()); parent->show(); - QApplication::setActiveWindow(parent->window()); - child->activateWindow(); child->setFocus(); QTRY_VERIFY(child->hasFocus()); @@ -1911,8 +1983,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); @@ -1927,6 +2002,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)) @@ -1950,7 +2131,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()); @@ -1986,23 +2166,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 @@ -2011,7 +2197,6 @@ void tst_QWidget::reverseTabOrder() container.show(); container.activateWindow(); - QApplication::setActiveWindow(&container); QVERIFY(QTest::qWaitForWindowActive(&container)); QTRY_VERIFY(firstEdit->hasFocus()); @@ -2046,6 +2231,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)) @@ -2073,7 +2391,6 @@ void tst_QWidget::tabOrderWithProxy() container.show(); container.activateWindow(); - QApplication::setActiveWindow(&container); QVERIFY(QTest::qWaitForWindowActive(&container)); QTRY_VERIFY(firstEdit->hasFocus()); @@ -2107,6 +2424,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)) @@ -2147,7 +2539,6 @@ void tst_QWidget::tabOrderWithCompoundWidgets() container.show(); container.activateWindow(); - QApplication::setActiveWindow(&container); QVERIFY(QTest::qWaitForWindowActive(&container)); lastEdit->setFocus(); @@ -2198,34 +2589,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() @@ -2249,28 +2786,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() @@ -2355,7 +2891,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); @@ -2383,7 +2918,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()); @@ -2410,7 +2944,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); @@ -2439,7 +2972,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); @@ -2517,12 +3049,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); @@ -2637,6 +3171,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; @@ -2826,7 +3473,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(); @@ -2835,7 +3482,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); } @@ -2843,7 +3490,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(); @@ -2852,7 +3499,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); } @@ -2928,7 +3575,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(); @@ -2952,7 +3599,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); @@ -2971,7 +3617,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); @@ -2991,7 +3636,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); @@ -3012,7 +3656,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); @@ -3029,7 +3672,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()) @@ -3067,8 +3709,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); @@ -3078,7 +3720,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")) @@ -3094,7 +3736,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); @@ -3488,9 +4130,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); @@ -3504,9 +4146,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); @@ -3533,15 +4176,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); @@ -3549,7 +4194,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); @@ -3587,9 +4232,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); @@ -3606,7 +4251,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); @@ -3652,7 +4297,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) @@ -3670,7 +4315,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); @@ -3685,7 +4330,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 @@ -3807,6 +4452,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); @@ -3962,10 +4614,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; @@ -4019,6 +4670,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 @@ -4177,7 +4890,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; @@ -4216,7 +4930,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; @@ -4282,22 +4997,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()); @@ -4305,107 +5018,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(); @@ -4430,10 +5150,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); } @@ -4603,7 +5328,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 @@ -4658,6 +5383,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() { { @@ -4825,24 +5628,29 @@ void tst_QWidget::closeAndShowWithNativeChild() QApplication::setAttribute(Qt::AA_DontCreateNativeWidgetSiblings); QWidget topLevel; + topLevel.setObjectName("TopLevel"); QWidget *nativeChild = new QWidget; + nativeChild->setObjectName("NativeChild"); nativeChild->setFixedSize(200, 200); - QWidget *nativeHiddenChild = new QWidget; - nativeHiddenChild->setFixedSize(200, 200); QWidget *normalChild = new QWidget; + normalChild->setObjectName("NormalChild"); normalChild->setFixedSize(200, 200); QHBoxLayout *layout = new QHBoxLayout; layout->addWidget(nativeChild); - layout->addWidget(nativeHiddenChild); layout->addWidget(normalChild); topLevel.setLayout(layout); - nativeHiddenChild->hide(); + 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)); - nativeChild->winId(); const QSize originalSize = topLevel.size(); topLevel.close(); @@ -5010,6 +5818,7 @@ void tst_QWidget::update() Q_CHECK_PAINTEVENTS UpdateWidget w; + w.setPalette(simplePalette()); w.setWindowTitle(QLatin1String(QTest::currentTestFunction())); w.resize(100, 100); centerOnScreen(&w); @@ -5023,6 +5832,7 @@ void tst_QWidget::update() w.reset(); UpdateWidget child(&w); + child.setPalette(simplePalette()); child.setGeometry(10, 10, 80, 80); child.show(); @@ -5094,6 +5904,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(); @@ -5173,9 +5984,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); @@ -5257,11 +6070,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(m_availableTopLeft); updateWidget.showNormal(); - QApplication::setActiveWindow(&updateWidget); QVERIFY(QTest::qWaitForWindowActive(&updateWidget)); QVERIFY(updateWidget.numPaintEvents > 0); @@ -5336,34 +6149,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 @@ -5409,7 +6262,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(); @@ -5428,8 +6281,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); @@ -5446,7 +6299,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); @@ -5472,7 +6325,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); @@ -5487,7 +6340,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); @@ -5497,7 +6350,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); @@ -5523,7 +6376,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); @@ -5538,7 +6391,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); @@ -5548,7 +6401,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); @@ -5617,7 +6470,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(); @@ -5647,7 +6500,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) @@ -5676,7 +6529,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(); @@ -5696,7 +6549,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(); @@ -5727,7 +6580,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(); @@ -5747,7 +6600,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(); @@ -5767,7 +6620,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); @@ -5945,8 +6798,7 @@ void tst_QWidget::moveChild() QTRY_COMPARE(pos, child.pos()); QTRY_COMPARE(parent.r, QRegion(oldGeometry) - child.geometry()); - if (QGuiApplication::platformName() == "cocoa") - QEXPECT_FAIL("", "Cocoa backing store doesn't implement scroll", Abort); + // should be scrolled in backingstore QCOMPARE(child.r, QRegion()); VERIFY_COLOR(child, child.rect(), child.color); @@ -5955,6 +6807,9 @@ void tst_QWidget::moveChild() 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); @@ -5972,7 +6827,6 @@ void tst_QWidget::showAndMoveChild() parent.setGeometry(desktopDimensions); parent.setPalette(Qt::red); parent.show(); - QApplication::setActiveWindow(&parent); QVERIFY(QTest::qWaitForWindowActive(&parent)); QWidget child(&parent); @@ -6091,17 +6945,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); @@ -6109,16 +6963,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); } @@ -6181,7 +7035,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 @@ -6227,7 +7081,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 @@ -6403,34 +7257,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() { @@ -6634,7 +7460,7 @@ void tst_QWidget::testWindowIconChangeEventPropagation() QWidgetList widgets; widgets << &topLevelWidget << &topLevelChild << &dialog << &dialogChild; - QCOMPARE(widgets.count(), 4); + QCOMPARE(widgets.size(), 4); topLevelWidget.show(); dialog.show(); @@ -6648,13 +7474,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)); } @@ -6663,7 +7489,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(); @@ -6680,7 +7506,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); @@ -6698,7 +7524,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); @@ -6855,7 +7681,6 @@ 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()); @@ -6990,7 +7815,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, @@ -7079,7 +7906,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, @@ -7171,7 +8000,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, @@ -7226,6 +8057,9 @@ private: void tst_QWidget::render() { +#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 @@ -7279,7 +8113,12 @@ void tst_QWidget::renderChildFillsBackground() 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))); - QEXPECT_FAIL("", "This test fails on all platforms", Continue); +#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); } @@ -7313,15 +8152,12 @@ void tst_QWidget::renderTargetOffset() 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. +// 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; @@ -7342,6 +8178,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())); @@ -7362,7 +8201,7 @@ void tst_QWidget::renderInvisible() // 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"); @@ -7373,7 +8212,7 @@ void tst_QWidget::renderInvisible() const QSize calendarSizeResized = calendar->size() + QSize(50, 50); calendar->resize(calendarSizeResized); 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"); @@ -7386,7 +8225,7 @@ void tst_QWidget::renderInvisible() 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"); @@ -7402,7 +8241,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"); @@ -7414,7 +8253,7 @@ void tst_QWidget::renderInvisible() 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"); @@ -7428,7 +8267,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"); @@ -7437,7 +8276,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"); @@ -7451,7 +8290,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"); @@ -7489,7 +8328,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"); @@ -7501,7 +8340,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); @@ -7641,6 +8480,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; @@ -8305,6 +9185,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; @@ -8878,6 +9759,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); @@ -8894,9 +9776,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; @@ -8912,9 +9791,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); @@ -8968,6 +9848,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 { @@ -9002,6 +9922,7 @@ public slots: void tst_QWidget::setMaskInResizeEvent() { UpdateWidget w; + w.setPalette(simplePalette()); w.setWindowTitle(QLatin1String(QTest::currentTestFunction())); w.reset(); w.resize(200, 200); @@ -9081,6 +10002,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(); @@ -9474,6 +10396,10 @@ void tst_QWidget::translucentWidget() 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); @@ -9528,11 +10454,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(); @@ -9562,6 +10489,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); @@ -9955,6 +10883,9 @@ void tst_QWidget::enterLeaveOnWindowShowHide_data() */ void tst_QWidget::enterLeaveOnWindowShowHide() { + if (!QGuiApplicationPrivate::platformIntegration()->hasCapability(QPlatformIntegration::WindowActivation)) + QSKIP("QWindow::requestActivate() is not supported."); + QFETCH(Qt::WindowType, windowType); class Widget : public QWidget { @@ -10006,6 +10937,8 @@ void tst_QWidget::enterLeaveOnWindowShowHide() 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 } }; @@ -10022,25 +10955,25 @@ void tst_QWidget::enterLeaveOnWindowShowHide() if (!QTest::qWaitFor([&]{ return widget.geometry().contains(QCursor::pos()); })) QSKIP("We can't move the cursor"); widget.show(); - QApplication::setActiveWindow(&widget); QVERIFY(QTest::qWaitForWindowActive(&widget)); ++expectedEnter; - QTRY_COMPARE_WITH_TIMEOUT(widget.numEnterEvents, expectedEnter, 250); + 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, 500); + 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. - QVERIFY(QTest::qWaitFor([&]{ return widget.numEnterEvents >= expectedEnter; })); + 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") @@ -10122,7 +11055,7 @@ void tst_QWidget::taskQTBUG_4055_sendSyntheticEnterLeave() QTRY_COMPARE(child.numEnterEvents, 1); QCOMPARE(child.numMouseMoveEvents, 0); - // Sending synthetic enter/leave trough the parent's mousePressEvent handler. + // Sending synthetic enter/leave through the parent's mousePressEvent handler. parent.child = &child; child.hide(); @@ -10151,6 +11084,110 @@ void tst_QWidget::taskQTBUG_4055_sendSyntheticEnterLeave() 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); + // wait until we got the correct global pos + QPoint expectedGlobalPos = root.geometry().topLeft() + QPoint(100, 100) + middle; + if (!QTest::qWaitFor([&]{ return expectedGlobalPos == h.mapToGlobal(middle); })) + QSKIP("Can't move cursor"); + 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() @@ -10256,6 +11293,7 @@ void tst_QWidget::focusWidget_task254563() void tst_QWidget::destroyBackingStore() { UpdateWidget w; + w.setPalette(simplePalette()); w.setWindowTitle(QLatin1String(QTest::currentTestFunction())); centerOnScreen(&w); w.reset(); @@ -10380,6 +11418,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)) @@ -10425,6 +11614,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: @@ -10572,32 +11764,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 @@ -10956,7 +12158,6 @@ void tst_QWidget::grabMouse() layout->addWidget(grabber); centerOnScreen(&w); w.show(); - QApplication::setActiveWindow(&w); QVERIFY(QTest::qWaitForWindowActive(&w)); QStringList expectedLog; @@ -10993,7 +12194,6 @@ void tst_QWidget::grabKeyboard() layout->addWidget(nonGrabber); centerOnScreen(&w); w.show(); - QApplication::setActiveWindow(&w); QVERIFY(QTest::qWaitForWindowActive(&w)); nonGrabber->setFocus(); grabber->grabKeyboard(); @@ -11087,6 +12287,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; @@ -11795,6 +12998,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); @@ -11893,60 +13097,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); @@ -12253,6 +13451,9 @@ protected: void tst_QWidget::deleteWindowInCloseEvent() { +#ifdef Q_OS_ANDROID + QSKIP("This test crashes on Android"); +#endif QSignalSpy quitSpy(qApp, &QGuiApplication::lastWindowClosed); // Closing this widget should not cause a crash @@ -12265,7 +13466,7 @@ void tst_QWidget::deleteWindowInCloseEvent() QApplication::exec(); // It should still result in a single lastWindowClosed emit - QCOMPARE(quitSpy.count(), 1); + QCOMPARE(quitSpy.size(), 1); } /*! @@ -12286,7 +13487,7 @@ void tst_QWidget::quitOnClose() widget->close(); }); QApplication::exec(); - QCOMPARE(quitSpy.count(), 1); + QCOMPARE(quitSpy.size(), 1); widget->show(); QVERIFY(QTest::qWaitForWindowExposed(widget.get())); @@ -12294,7 +13495,7 @@ void tst_QWidget::quitOnClose() widget.reset(); }); QApplication::exec(); - QCOMPARE(quitSpy.count(), 2); + QCOMPARE(quitSpy.size(), 2); } void tst_QWidget::setParentChangesFocus_data() @@ -12354,20 +13555,24 @@ void tst_QWidget::setParentChangesFocus() QCOMPARE(secondary->focusWidget()->objectName(), focusWidget); } secondary->show(); - QApplication::setActiveWindow(secondary.get()); + QApplicationPrivate::setActiveWindow(secondary.get()); QVERIFY(QTest::qWaitForWindowActive(secondary.get())); if (!reparentBeforeShow) { secondary->setParent(targetParent ? &window : nullptr, targetType); secondary->show(); // reparenting hides, so show again - QApplication::setActiveWindow(secondary.get()); + 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(); @@ -12385,5 +13590,500 @@ void tst_QWidget::activateWhileModalHidden() 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(); + } +} + +#ifndef QT_NO_CONTEXTMENU +void tst_QWidget::contextMenuTrigger() +{ + class ContextMenuWidget : public QWidget + { + public: + int events = 0; + + protected: + void contextMenuEvent(QContextMenuEvent *) override { ++events; } + }; + + const Qt::ContextMenuTrigger wasTrigger = QGuiApplication::styleHints()->contextMenuTrigger(); + auto restoreTriggerGuard = qScopeGuard([wasTrigger]{ + QGuiApplication::styleHints()->setContextMenuTrigger(wasTrigger); + }); + + ContextMenuWidget widget; + widget.show(); + QVERIFY(!qApp->topLevelWindows().empty()); + auto *window = qApp->topLevelWindows()[0]; + QVERIFY(window); + QCOMPARE(widget.events, 0); + QGuiApplication::styleHints()->setContextMenuTrigger(Qt::ContextMenuTrigger::Press); + QTest::mousePress(window, Qt::RightButton); + QCOMPARE(widget.events, 1); + QTest::mouseRelease(window, Qt::RightButton); + QCOMPARE(widget.events, 1); + QGuiApplication::styleHints()->setContextMenuTrigger(Qt::ContextMenuTrigger::Release); + QTest::mousePress(window, Qt::RightButton); + QCOMPARE(widget.events, 1); + QTest::mouseRelease(window, Qt::RightButton); + QCOMPARE(widget.events, 2); +} +#endif + QTEST_MAIN(tst_QWidget) #include "tst_qwidget.moc" diff --git a/tests/auto/widgets/kernel/qwidget/tst_qwidget_mac_helpers.h b/tests/auto/widgets/kernel/qwidget/tst_qwidget_mac_helpers.h deleted file mode 100644 index 87de300da9..0000000000 --- a/tests/auto/widgets/kernel/qwidget/tst_qwidget_mac_helpers.h +++ /dev/null @@ -1,35 +0,0 @@ -/**************************************************************************** -** -** 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$ -** -****************************************************************************/ -#include <QtCore/QString> -#include <QtCore/QPair> -#include <QtWidgets/QWidget> - -#pragma once // Yeah, it's deprecated in general, but it's standard practice for Mac OS X. - -QString nativeWindowTitle(QWidget *widget, Qt::WindowState state); -bool nativeWindowModified(QWidget *widget); diff --git a/tests/auto/widgets/kernel/qwidget/tst_qwidget_mac_helpers.mm b/tests/auto/widgets/kernel/qwidget/tst_qwidget_mac_helpers.mm deleted file mode 100644 index e2d00aa25b..0000000000 --- a/tests/auto/widgets/kernel/qwidget/tst_qwidget_mac_helpers.mm +++ /dev/null @@ -1,57 +0,0 @@ -/**************************************************************************** -** -** Copyright (C) 2016 The Qt Company Ltd. -** Contact: https://www.qt.io/licensing/ -** -** This file is part of the documentation 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$ -** -****************************************************************************/ - -// some versions of CALayer.h use 'slots' as an identifier -#define QT_NO_KEYWORDS - -#include "tst_qwidget_mac_helpers.h" -#include <QApplication> -#include <qpa/qplatformnativeinterface.h> -#include <private/qcore_mac_p.h> - -#include <AppKit/AppKit.h> - -QString nativeWindowTitle(QWidget *window, Qt::WindowState state) -{ - QWindow *qwindow = window->windowHandle(); - NSWindow *nswindow = (NSWindow *) qApp->platformNativeInterface()->nativeResourceForWindow("nswindow", qwindow); - QCFString macTitle; - if (state == Qt::WindowMinimized) { - macTitle = reinterpret_cast<CFStringRef>([[nswindow miniwindowTitle] retain]); - } else { - macTitle = reinterpret_cast<CFStringRef>([[nswindow title] retain]); - } - return macTitle; -} - -bool nativeWindowModified(QWidget *widget) -{ - QWindow *qwindow = widget->windowHandle(); - NSWindow *nswindow = (NSWindow *) qApp->platformNativeInterface()->nativeResourceForWindow("nswindow", qwindow); - return [nswindow isDocumentEdited]; -} |