diff options
84 files changed, 2117 insertions, 138 deletions
diff --git a/.qmake.conf b/.qmake.conf index 4d40e427..722a8b53 100644 --- a/.qmake.conf +++ b/.qmake.conf @@ -5,4 +5,4 @@ DEFINES += QT_NO_FOREACH QT_NO_JAVA_STYLE_ITERATORS QT_NO_LINKED_LIST QQC2_SOURCE_TREE = $$PWD -MODULE_VERSION = 5.15.2 +MODULE_VERSION = 5.15.13 @@ -39,7 +39,7 @@ If you have problems or questions, don't hesitate to: ## Installation The MINIMUM REQUIREMENT for building this project is to use the same branch -of Qt 5. The dependencies are *qtbase*, *qtxmlpatterns* and *qtdeclarative*. +of Qt 5. The dependencies are *qtbase* and *qtdeclarative*. To install the controls into your Qt directory (```QTDIR/qml```): diff --git a/dist/changes-5.15.2 b/dist/changes-5.15.2 new file mode 100644 index 00000000..e71e2bf3 --- /dev/null +++ b/dist/changes-5.15.2 @@ -0,0 +1,42 @@ +Qt 5.15.2 is a bug-fix release. It maintains both forward and backward +compatibility (source and binary) with Qt 5.15.1. + +For more details, refer to the online documentation included in this +distribution. The documentation is also available online: + + https://doc.qt.io/qt-5.15/index.html + +The Qt version 5.15 series is binary compatible with the 5.14.x series. +Applications compiled for 5.14 will continue to run with 5.15. + +Some of the changes listed in this file include issue tracking numbers +corresponding to tasks in the Qt Bug Tracker: + + https://bugreports.qt.io/ + +Each of these identifiers can be entered in the bug tracker to obtain more +information about a particular change. + +**************************************************************************** +* Controls * +**************************************************************************** + + - [QTBUG-83698] Fixed issue where a menu opened by pressing enter (handled + via attached Keys property) would instantly trigger the first menu + item. + - [QTBUG-86851] QQuickMenu: fixed memory leak due to contentModel not being + deleted. Also fixed a heap-use-after-free that was exposed by the + previously mentioned fix. + - [QTBUG-85884] Fixed expected item not getting focus when a dialog closes. + - [QTBUG-85719] Fixed SpinBox failing to validate input correctly after + previous input was out of range. + +**************************************************************************** +* Styles * +**************************************************************************** + +Material +-------- + + - [QTBUG-85699] Fixed binding loops when binding between attached + properties. diff --git a/src/imports/controls/DialogButtonBox.qml b/src/imports/controls/DialogButtonBox.qml index 3c9d5b48..cc148ac4 100644 --- a/src/imports/controls/DialogButtonBox.qml +++ b/src/imports/controls/DialogButtonBox.qml @@ -55,6 +55,7 @@ T.DialogButtonBox { } contentItem: ListView { + implicitWidth: contentWidth model: control.contentModel spacing: control.spacing orientation: ListView.Horizontal diff --git a/src/imports/controls/designer/AbstractButtonSection.qml b/src/imports/controls/designer/AbstractButtonSection.qml index e8aa39c2..35fad2ab 100644 --- a/src/imports/controls/designer/AbstractButtonSection.qml +++ b/src/imports/controls/designer/AbstractButtonSection.qml @@ -104,8 +104,8 @@ Section { } Label { - text: qsTr("Repeat") - tooltip: qsTr("Whether the button repeats while pressed and held down.") + text: qsTr("Auto-Repeat") + tooltip: qsTr("Whether the button repeats pressed(), released() and clicked() signals while the button is pressed and held down.") } SecondColumnLayout { CheckBox { diff --git a/src/imports/controls/designer/ButtonSection.qml b/src/imports/controls/designer/ButtonSection.qml index fef46071..951c8cf4 100644 --- a/src/imports/controls/designer/ButtonSection.qml +++ b/src/imports/controls/designer/ButtonSection.qml @@ -43,17 +43,7 @@ Section { caption: qsTr("Button") SectionLayout { - Label { - text: qsTr("AutoRepeat") - tooltip: qsTr("Whether the button repeats pressed(), released() and clicked() signals while the button is pressed and held down.") - } - SecondColumnLayout { - CheckBox { - text: backendValues.autoRepeat.valueToString - backendValue: backendValues.autoRepeat - Layout.fillWidth: true - } - } + Label { text: qsTr("Flat") tooltip: qsTr("Whether the button is flat.") diff --git a/src/imports/controls/designer/RangeSliderSpecifics.qml b/src/imports/controls/designer/RangeSliderSpecifics.qml index 2324a66f..9372a4ff 100644 --- a/src/imports/controls/designer/RangeSliderSpecifics.qml +++ b/src/imports/controls/designer/RangeSliderSpecifics.qml @@ -127,7 +127,7 @@ Column { } SecondColumnLayout { ComboBox { - backendValue: backendValues.orientation + backendValue: backendValues.snapMode model: [ "NoSnap", "SnapOnRelease", "SnapAlways" ] scope: "RangeSlider" Layout.fillWidth: true diff --git a/src/imports/controls/fusion/DialogButtonBox.qml b/src/imports/controls/fusion/DialogButtonBox.qml index a0b0f243..4673e421 100644 --- a/src/imports/controls/fusion/DialogButtonBox.qml +++ b/src/imports/controls/fusion/DialogButtonBox.qml @@ -56,6 +56,7 @@ T.DialogButtonBox { delegate: Button { } contentItem: ListView { + implicitWidth: contentWidth model: control.contentModel spacing: control.spacing orientation: ListView.Horizontal diff --git a/src/imports/controls/imagine/DialogButtonBox.qml b/src/imports/controls/imagine/DialogButtonBox.qml index c24b29fc..fd27a876 100644 --- a/src/imports/controls/imagine/DialogButtonBox.qml +++ b/src/imports/controls/imagine/DialogButtonBox.qml @@ -66,6 +66,7 @@ T.DialogButtonBox { } contentItem: ListView { + implicitWidth: contentWidth model: control.contentModel spacing: control.spacing orientation: ListView.Horizontal diff --git a/src/imports/controls/imagine/GroupBox.qml b/src/imports/controls/imagine/GroupBox.qml index 7abdb6f0..46f9c98a 100644 --- a/src/imports/controls/imagine/GroupBox.qml +++ b/src/imports/controls/imagine/GroupBox.qml @@ -53,7 +53,6 @@ T.GroupBox { leftPadding: background ? background.leftPadding : 0 rightPadding: background ? background.rightPadding : 0 bottomPadding: background ? background.bottomPadding : 0 - padding: 12 label: Label { width: control.width @@ -88,7 +87,7 @@ T.GroupBox { x: -leftInset y: control.topPadding - control.bottomPadding - topInset width: control.width + leftInset + rightInset - height: control.height + topInset + bottomInset - control.topPadding + control.padding + height: control.height + topInset + bottomInset - control.topPadding + control.bottomPadding source: Imagine.url + "groupbox-background" NinePatchImageSelector on source { diff --git a/src/imports/controls/imagine/qquickninepatchimage.cpp b/src/imports/controls/imagine/qquickninepatchimage.cpp index 7d5e4f71..ed388ec1 100644 --- a/src/imports/controls/imagine/qquickninepatchimage.cpp +++ b/src/imports/controls/imagine/qquickninepatchimage.cpp @@ -386,7 +386,12 @@ void QQuickNinePatchImage::pixmapChange() { Q_D(QQuickNinePatchImage); if (QFileInfo(d->url.fileName()).completeSuffix().toLower() == QLatin1String("9.png")) { - d->resetNode = d->ninePatch.isNull(); + // Keep resetNode if it is already set, we do not want to miss an + // ImageNode->NinePatchNode change. Without this there's a chance one gets + // an incorrect cast on oldNode every once in a while with source changes. + if (!d->resetNode) + d->resetNode = d->ninePatch.isNull(); + d->ninePatch = d->pix.image(); if (d->ninePatch.depth() != 32) d->ninePatch = d->ninePatch.convertToFormat(QImage::Format_ARGB32); @@ -434,6 +439,8 @@ QSGNode *QQuickNinePatchImage::updatePaintNode(QSGNode *oldNode, UpdatePaintNode QSizeF sz = size(); QImage image = d->pix.image(); if (!sz.isValid() || image.isNull()) { + if (d->provider) + d->provider->updateTexture(nullptr); delete oldNode; return nullptr; } @@ -449,6 +456,13 @@ QSGNode *QQuickNinePatchImage::updatePaintNode(QSGNode *oldNode, UpdatePaintNode qsgnode_set_description(patchNode, QString::fromLatin1("QQuickNinePatchImage: '%1'").arg(d->url.toString())); #endif + // The image may wrap non-owned data (due to pixmapChange). Ensure we never + // pass such an image to the scenegraph, because with a separate render + // thread the data may become invalid (in a subsequent pixmapChange on the + // gui thread) by the time the renderer gets to do something with the QImage + // passed in here. + image.detach(); + QSGTexture *texture = window()->createTextureFromImage(image); patchNode->initialize(texture, sz * d->devicePixelRatio, image.size(), d->xDivs, d->yDivs, d->devicePixelRatio); return patchNode; diff --git a/src/imports/controls/material/ComboBox.qml b/src/imports/controls/material/ComboBox.qml index a9bdd934..6aada8c5 100644 --- a/src/imports/controls/material/ComboBox.qml +++ b/src/imports/controls/material/ComboBox.qml @@ -147,14 +147,14 @@ T.ComboBox { enter: Transition { // grow_fade_in - NumberAnimation { property: "scale"; from: 0.9; to: 1.0; easing.type: Easing.OutQuint; duration: 220 } - NumberAnimation { property: "opacity"; from: 0.0; to: 1.0; easing.type: Easing.OutCubic; duration: 150 } + NumberAnimation { property: "scale"; from: 0.9; easing.type: Easing.OutQuint; duration: 220 } + NumberAnimation { property: "opacity"; from: 0.0; easing.type: Easing.OutCubic; duration: 150 } } exit: Transition { // shrink_fade_out - NumberAnimation { property: "scale"; from: 1.0; to: 0.9; easing.type: Easing.OutQuint; duration: 220 } - NumberAnimation { property: "opacity"; from: 1.0; to: 0.0; easing.type: Easing.OutCubic; duration: 150 } + NumberAnimation { property: "scale"; to: 0.9; easing.type: Easing.OutQuint; duration: 220 } + NumberAnimation { property: "opacity"; to: 0.0; easing.type: Easing.OutCubic; duration: 150 } } contentItem: ListView { diff --git a/src/imports/controls/material/DialogButtonBox.qml b/src/imports/controls/material/DialogButtonBox.qml index c53b8210..d148fb24 100644 --- a/src/imports/controls/material/DialogButtonBox.qml +++ b/src/imports/controls/material/DialogButtonBox.qml @@ -60,6 +60,7 @@ T.DialogButtonBox { delegate: Button { flat: true } contentItem: ListView { + implicitWidth: contentWidth model: control.contentModel spacing: control.spacing orientation: ListView.Horizontal diff --git a/src/imports/controls/material/material.pro b/src/imports/controls/material/material.pro index cf08b925..ea74d277 100644 --- a/src/imports/controls/material/material.pro +++ b/src/imports/controls/material/material.pro @@ -1,4 +1,4 @@ -TARGET = qtquickcontrols2materialstyleplugin +TARGET = qqc2materialstyleplugin TARGETPATH = QtQuick/Controls.2/Material IMPORT_NAME = QtQuick.Controls.Material diff --git a/src/imports/controls/material/qmldir b/src/imports/controls/material/qmldir index 870a0382..d48b7b12 100644 --- a/src/imports/controls/material/qmldir +++ b/src/imports/controls/material/qmldir @@ -1,4 +1,4 @@ module QtQuick.Controls.Material -plugin qtquickcontrols2materialstyleplugin +plugin qqc2materialstyleplugin classname QtQuickControls2MaterialStylePlugin depends QtQuick.Controls 2.5 diff --git a/src/imports/controls/universal/DialogButtonBox.qml b/src/imports/controls/universal/DialogButtonBox.qml index 0458c39d..103b46c2 100644 --- a/src/imports/controls/universal/DialogButtonBox.qml +++ b/src/imports/controls/universal/DialogButtonBox.qml @@ -59,6 +59,7 @@ T.DialogButtonBox { } contentItem: ListView { + implicitWidth: contentWidth model: control.contentModel spacing: control.spacing orientation: ListView.Horizontal diff --git a/src/imports/platform/qquickplatformfiledialog.cpp b/src/imports/platform/qquickplatformfiledialog.cpp index 2ef08ef6..af5475fa 100644 --- a/src/imports/platform/qquickplatformfiledialog.cpp +++ b/src/imports/platform/qquickplatformfiledialog.cpp @@ -353,7 +353,6 @@ void QQuickPlatformFileDialog::resetNameFilters() } /*! - \qmlpropertygroup Qt.labs.platform::FileDialog::selectedNameFilter \qmlproperty int Qt.labs.platform::FileDialog::selectedNameFilter.index \qmlproperty string Qt.labs.platform::FileDialog::selectedNameFilter.name \qmlproperty list<string> Qt.labs.platform::FileDialog::selectedNameFilter.extensions @@ -553,8 +552,14 @@ QUrl QQuickPlatformFileDialog::addDefaultSuffix(const QUrl &file) const QUrl url = file; const QString path = url.path(); const QString suffix = m_options->defaultSuffix(); - if (!suffix.isEmpty() && !path.endsWith(QLatin1Char('/')) && path.lastIndexOf(QLatin1Char('.')) == -1) + // Urls with "content" scheme do not require suffixes. Such schemes are + // used on Android. + const bool isContentScheme = url.scheme() == QStringLiteral("content"); + if (!isContentScheme && !suffix.isEmpty() && !path.endsWith(QLatin1Char('/')) + && path.lastIndexOf(QLatin1Char('.')) == -1) { url.setPath(path + QLatin1Char('.') + suffix); + } + return url; } diff --git a/src/imports/platform/qquickplatformmenu.cpp b/src/imports/platform/qquickplatformmenu.cpp index 1f5f52d7..b5289f33 100644 --- a/src/imports/platform/qquickplatformmenu.cpp +++ b/src/imports/platform/qquickplatformmenu.cpp @@ -610,10 +610,9 @@ void QQuickPlatformMenu::setFont(const QFont& font) /*! \since Qt.labs.platform 1.1 (Qt 5.12) - \qmlpropertygroup Qt.labs.platform::MenuItem::icon - \qmlproperty url Qt.labs.platform::MenuItem::icon.source - \qmlproperty string Qt.labs.platform::MenuItem::icon.name - \qmlproperty bool Qt.labs.platform::MenuItem::icon.mask + \qmlproperty url Qt.labs.platform::Menu::icon.source + \qmlproperty string Qt.labs.platform::Menu::icon.name + \qmlproperty bool Qt.labs.platform::Menu::icon.mask This property holds the menu item's icon. */ diff --git a/src/imports/platform/qquickplatformmenuitem.cpp b/src/imports/platform/qquickplatformmenuitem.cpp index 72ad7d5d..5a8b3798 100644 --- a/src/imports/platform/qquickplatformmenuitem.cpp +++ b/src/imports/platform/qquickplatformmenuitem.cpp @@ -592,7 +592,6 @@ void QQuickPlatformMenuItem::setFont(const QFont& font) /*! \since Qt.labs.platform 1.1 (Qt 5.12) - \qmlpropertygroup Qt.labs.platform::MenuItem::icon \qmlproperty url Qt.labs.platform::MenuItem::icon.source \qmlproperty string Qt.labs.platform::MenuItem::icon.name \qmlproperty bool Qt.labs.platform::MenuItem::icon.mask diff --git a/src/imports/platform/qquickplatformsystemtrayicon.cpp b/src/imports/platform/qquickplatformsystemtrayicon.cpp index da483822..442da037 100644 --- a/src/imports/platform/qquickplatformsystemtrayicon.cpp +++ b/src/imports/platform/qquickplatformsystemtrayicon.cpp @@ -356,7 +356,6 @@ QRect QQuickPlatformSystemTrayIcon::geometry() const /*! \since Qt.labs.platform 1.1 (Qt 5.12) - \qmlpropertygroup Qt.labs.platform::SystemTrayIcon::icon \qmlproperty url Qt.labs.platform::SystemTrayIcon::icon.source \qmlproperty string Qt.labs.platform::SystemTrayIcon::icon.name \qmlproperty bool Qt.labs.platform::SystemTrayIcon::icon.mask diff --git a/src/imports/templates/qtquicktemplates2plugin.cpp b/src/imports/templates/qtquicktemplates2plugin.cpp index 3b085c56..cee8e53d 100644 --- a/src/imports/templates/qtquicktemplates2plugin.cpp +++ b/src/imports/templates/qtquicktemplates2plugin.cpp @@ -142,6 +142,8 @@ private: QtQuickTemplates2Plugin::QtQuickTemplates2Plugin(QObject *parent) : QQmlExtensionPlugin(parent), registered(false) { + volatile auto initialization = &QQuickTemplates_initializeModule; + Q_UNUSED(initialization) #if QT_CONFIG(shortcut) originalContextMatcher = qt_quick_shortcut_context_matcher(); qt_quick_set_shortcut_context_matcher(QQuickShortcutContext::matcher); diff --git a/src/quickcontrols2/qquickiconimage.cpp b/src/quickcontrols2/qquickiconimage.cpp index d86afd7f..11bb3bca 100644 --- a/src/quickcontrols2/qquickiconimage.cpp +++ b/src/quickcontrols2/qquickiconimage.cpp @@ -42,6 +42,12 @@ QT_BEGIN_NAMESPACE +QQuickIconImagePrivate::~QQuickIconImagePrivate() +{ + qDeleteAll(icon.entries); + icon.entries.clear(); +} + bool QQuickIconImagePrivate::updateDevicePixelRatio(qreal targetDevicePixelRatio) { if (isThemeIcon) { @@ -132,6 +138,7 @@ void QQuickIconImage::setName(const QString &name) if (d->icon.iconName == name) return; + qDeleteAll(d->icon.entries); d->icon = QIconLoader::instance()->loadIcon(name); if (isComponentComplete()) d->updateIcon(); diff --git a/src/quickcontrols2/qquickiconimage_p_p.h b/src/quickcontrols2/qquickiconimage_p_p.h index 0c755ff6..11bf5e92 100644 --- a/src/quickcontrols2/qquickiconimage_p_p.h +++ b/src/quickcontrols2/qquickiconimage_p_p.h @@ -59,6 +59,7 @@ class Q_QUICKCONTROLS2_PRIVATE_EXPORT QQuickIconImagePrivate : public QQuickImag Q_DECLARE_PUBLIC(QQuickIconImage) public: + ~QQuickIconImagePrivate() override; void updateIcon(); void updateFillMode(); qreal calculateDevicePixelRatio() const; diff --git a/src/quicktemplates2/accessible/accessible.pri b/src/quicktemplates2/accessible/accessible.pri new file mode 100644 index 00000000..93660b9f --- /dev/null +++ b/src/quicktemplates2/accessible/accessible.pri @@ -0,0 +1,5 @@ +HEADERS += \ + $$PWD/qaccessiblequickpage_p.h + +SOURCES += \ + $$PWD/qaccessiblequickpage.cpp diff --git a/src/quicktemplates2/accessible/qaccessiblequickpage.cpp b/src/quicktemplates2/accessible/qaccessiblequickpage.cpp new file mode 100644 index 00000000..90ac49f9 --- /dev/null +++ b/src/quicktemplates2/accessible/qaccessiblequickpage.cpp @@ -0,0 +1,81 @@ +/**************************************************************************** +** +** Copyright (C) 2021 The Qt Company Ltd. +** Contact: http://www.qt.io/licensing/ +** +** This file is part of the Qt Quick Templates 2 module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL3$ +** 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 http://www.qt.io/terms-conditions. For further +** information use the contact form at http://www.qt.io/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 3 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPLv3 included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 3 requirements +** will be met: https://www.gnu.org/licenses/lgpl.html. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 2.0 or later as published by the Free +** Software Foundation and appearing in the file LICENSE.GPL included in +** the packaging of this file. Please review the following information to +** ensure the GNU General Public License version 2.0 requirements will be +** met: http://www.gnu.org/licenses/gpl-2.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include "qaccessiblequickpage_p.h" +#include "qquickpage_p.h" + +QT_BEGIN_NAMESPACE + +QAccessibleQuickPage::QAccessibleQuickPage(QQuickPage *page) + : QAccessibleQuickItem(page) +{ +} + +QAccessibleInterface *QAccessibleQuickPage::child(int index) const +{ + const QList<QQuickItem*> kids = orderedChildItems(); + if (QQuickItem *item = kids.value(index)) + return QAccessible::queryAccessibleInterface(item); + return nullptr; +} + +int QAccessibleQuickPage::indexOfChild(const QAccessibleInterface *iface) const +{ + const QList<QQuickItem*> kids = orderedChildItems(); + return (int)kids.indexOf(static_cast<QQuickItem*>(iface->object())); +} + +QList<QQuickItem *> QAccessibleQuickPage::orderedChildItems() const +{ + // Just ensures that the header is first, and footer is last. Other existing order is kept. + const QQuickPage *p = page(); + QList<QQuickItem*> kids = childItems(); + const qsizetype hidx = kids.indexOf(p->header()); + if (hidx != -1) + kids.move(hidx, 0); + const qsizetype fidx = kids.indexOf(p->footer()); + if (fidx != -1) + kids.move(fidx, kids.count() - 1); + return kids; +} + +QQuickPage *QAccessibleQuickPage::page() const +{ + return static_cast<QQuickPage*>(object()); +} + +QT_END_NAMESPACE + diff --git a/src/quicktemplates2/accessible/qaccessiblequickpage_p.h b/src/quicktemplates2/accessible/qaccessiblequickpage_p.h new file mode 100644 index 00000000..9b208c14 --- /dev/null +++ b/src/quicktemplates2/accessible/qaccessiblequickpage_p.h @@ -0,0 +1,70 @@ +/**************************************************************************** +** +** Copyright (C) 2021 The Qt Company Ltd. +** Contact: http://www.qt.io/licensing/ +** +** This file is part of the Qt Quick Templates 2 module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL3$ +** 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 http://www.qt.io/terms-conditions. For further +** information use the contact form at http://www.qt.io/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 3 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPLv3 included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 3 requirements +** will be met: https://www.gnu.org/licenses/lgpl.html. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 2.0 or later as published by the Free +** Software Foundation and appearing in the file LICENSE.GPL included in +** the packaging of this file. Please review the following information to +** ensure the GNU General Public License version 2.0 requirements will be +** met: http://www.gnu.org/licenses/gpl-2.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#ifndef QACCESSIBLEQUICKPAGE_H +#define QACCESSIBLEQUICKPAGE_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include <QtQuick/private/qaccessiblequickitem_p.h> + +QT_BEGIN_NAMESPACE + +class QQuickPage; + +class QAccessibleQuickPage : public QAccessibleQuickItem +{ +public: + QAccessibleQuickPage(QQuickPage *page); + QAccessibleInterface *child(int index) const override; + int indexOfChild(const QAccessibleInterface *iface) const override; +private: + QQuickPage *page() const; + QList<QQuickItem *> orderedChildItems() const; +}; + +QT_END_NAMESPACE + +#endif // QACCESSIBLEQUICKPAGE_H diff --git a/src/quicktemplates2/qquickabstractbutton.cpp b/src/quicktemplates2/qquickabstractbutton.cpp index fc37b6f6..20cf59c1 100644 --- a/src/quicktemplates2/qquickabstractbutton.cpp +++ b/src/quicktemplates2/qquickabstractbutton.cpp @@ -215,8 +215,20 @@ bool QQuickAbstractButtonPrivate::acceptKeyClick(Qt::Key key) const bool QQuickAbstractButtonPrivate::isPressAndHoldConnected() { Q_Q(QQuickAbstractButton); - const auto signal = &QQuickAbstractButton::pressAndHold; - const QMetaMethod method = QMetaMethod::fromSignal(signal); + static const QMetaMethod method = [&]() { + const auto signal = &QQuickAbstractButton::pressAndHold; + return QMetaMethod::fromSignal(signal); + }(); + return q->isSignalConnected(method); +} + +bool QQuickAbstractButtonPrivate::isDoubleClickConnected() +{ + Q_Q(QQuickAbstractButton); + static const QMetaMethod method = [&]() { + const auto signal = &QQuickAbstractButton::doubleClicked; + return QMetaMethod::fromSignal(signal); + }(); return q->isSignalConnected(method); } @@ -387,6 +399,17 @@ void QQuickAbstractButtonPrivate::itemImplicitHeightChanged(QQuickItem *item) emit q->implicitIndicatorHeightChanged(); } +void QQuickAbstractButtonPrivate::itemDestroyed(QQuickItem *item) +{ + Q_Q(QQuickAbstractButton); + QQuickControlPrivate::itemDestroyed(item); + if (item == indicator) { + indicator = nullptr; + emit q->implicitIndicatorWidthChanged(); + emit q->implicitIndicatorHeightChanged(); + } +} + QQuickAbstractButton *QQuickAbstractButtonPrivate::findCheckedButton() const { Q_Q(const QQuickAbstractButton); @@ -736,7 +759,6 @@ void QQuickAbstractButton::setIndicator(QQuickItem *indicator) } /*! - \qmlpropertygroup QtQuick.Controls::AbstractButton::icon \qmlproperty string QtQuick.Controls::AbstractButton::icon.name \qmlproperty url QtQuick.Controls::AbstractButton::icon.source \qmlproperty int QtQuick.Controls::AbstractButton::icon.width @@ -1087,9 +1109,11 @@ void QQuickAbstractButton::mousePressEvent(QMouseEvent *event) void QQuickAbstractButton::mouseDoubleClickEvent(QMouseEvent *event) { Q_D(QQuickAbstractButton); - QQuickControl::mouseDoubleClickEvent(event); - emit doubleClicked(); - d->wasDoubleClick = true; + if (d->isDoubleClickConnected()) { + QQuickControl::mouseDoubleClickEvent(event); + emit doubleClicked(); + d->wasDoubleClick = true; + } } void QQuickAbstractButton::timerEvent(QTimerEvent *event) diff --git a/src/quicktemplates2/qquickabstractbutton_p_p.h b/src/quicktemplates2/qquickabstractbutton_p_p.h index 9291c1a8..907790dc 100644 --- a/src/quicktemplates2/qquickabstractbutton_p_p.h +++ b/src/quicktemplates2/qquickabstractbutton_p_p.h @@ -80,6 +80,7 @@ public: virtual bool acceptKeyClick(Qt::Key key) const; bool isPressAndHoldConnected(); + bool isDoubleClickConnected(); void startPressAndHold(); void stopPressAndHold(); @@ -109,6 +110,7 @@ public: void itemImplicitWidthChanged(QQuickItem *item) override; void itemImplicitHeightChanged(QQuickItem *item) override; + void itemDestroyed(QQuickItem *item) override; // copied from qabstractbutton.cpp static const int AUTO_REPEAT_DELAY = 300; diff --git a/src/quicktemplates2/qquickaction.cpp b/src/quicktemplates2/qquickaction.cpp index 0dab3b97..8610cdfa 100644 --- a/src/quicktemplates2/qquickaction.cpp +++ b/src/quicktemplates2/qquickaction.cpp @@ -145,7 +145,7 @@ int QQuickActionPrivate::ShortcutEntry::shortcutId() const void QQuickActionPrivate::ShortcutEntry::grab(const QKeySequence &shortcut, bool enabled) { - if (shortcut.isEmpty()) + if (shortcut.isEmpty() || m_shortcutId) return; Qt::ShortcutContext context = Qt::WindowShortcut; // TODO @@ -381,7 +381,6 @@ void QQuickAction::setText(const QString &text) } /*! - \qmlpropertygroup QtQuick.Controls::Action::icon \qmlproperty string QtQuick.Controls::Action::icon.name \qmlproperty url QtQuick.Controls::Action::icon.source \qmlproperty int QtQuick.Controls::Action::icon.width diff --git a/src/quicktemplates2/qquickapplicationwindow.cpp b/src/quicktemplates2/qquickapplicationwindow.cpp index 903de676..a9a4ad72 100644 --- a/src/quicktemplates2/qquickapplicationwindow.cpp +++ b/src/quicktemplates2/qquickapplicationwindow.cpp @@ -48,6 +48,7 @@ #include "qquickdeferredpointer_p_p.h" #include <QtCore/private/qobject_p.h> +#include <QtCore/qscopedvaluerollback.h> #include <QtQuick/private/qquickitem_p.h> #include <QtQuick/private/qquickitemchangelistener_p.h> @@ -174,6 +175,7 @@ public: QPalette palette; QQuickItem *activeFocusControl = nullptr; QQuickApplicationWindow *q_ptr = nullptr; + bool insideRelayout = false; }; static void layoutItem(QQuickItem *item, qreal y, qreal width) @@ -192,9 +194,10 @@ static void layoutItem(QQuickItem *item, qreal y, qreal width) void QQuickApplicationWindowPrivate::relayout() { Q_Q(QQuickApplicationWindow); - if (!complete) + if (!complete || insideRelayout) return; + QScopedValueRollback<bool> guard(insideRelayout, true); QQuickItem *content = q->contentItem(); qreal hh = header && header->isVisible() ? header->height() : 0; qreal fh = footer && footer->isVisible() ? footer->height() : 0; @@ -650,6 +653,8 @@ QQuickOverlay *QQuickApplicationWindow::overlay() const if (!d->overlay) { d->overlay = new QQuickOverlay(QQuickWindow::contentItem()); + // make the overlay discoverable by the virtual keyboard + d->q_ptr->setProperty("_q_QQuickOverlay", QVariant::fromValue<QQuickItem*>(d->overlay)); d->overlay->stackAfter(QQuickApplicationWindow::contentItem()); } return d->overlay; diff --git a/src/quicktemplates2/qquickcombobox.cpp b/src/quicktemplates2/qquickcombobox.cpp index 3b87e489..e7d99a5d 100644 --- a/src/quicktemplates2/qquickcombobox.cpp +++ b/src/quicktemplates2/qquickcombobox.cpp @@ -234,6 +234,7 @@ public: void updateCurrentText(); void updateCurrentValue(); void updateCurrentTextAndValue(); + void updateAcceptableInput(); bool isValidIndex(int index) const; @@ -265,6 +266,8 @@ public: void itemImplicitWidthChanged(QQuickItem *item) override; void itemImplicitHeightChanged(QQuickItem *item) override; + void setInputMethodHints(Qt::InputMethodHints hints, bool force = false); + static void hideOldPopup(QQuickPopup *popup); bool flat = false; @@ -288,6 +291,7 @@ public: QQmlComponent *delegate = nullptr; QQuickDeferredPointer<QQuickItem> indicator; QQuickDeferredPointer<QQuickPopup> popup; + bool m_acceptableInput = true; struct ExtraData { bool editable = false; @@ -476,6 +480,26 @@ void QQuickComboBoxPrivate::updateCurrentTextAndValue() updateCurrentValue(); } +void QQuickComboBoxPrivate::updateAcceptableInput() +{ + Q_Q(QQuickComboBox); + + if (!contentItem) + return; + + const QQuickTextInput *textInputContentItem = qobject_cast<QQuickTextInput *>(contentItem); + + if (!textInputContentItem) + return; + + const bool newValue = textInputContentItem->hasAcceptableInput(); + + if (m_acceptableInput != newValue) { + m_acceptableInput = newValue; + emit q->acceptableInputChanged(); + } +} + bool QQuickComboBoxPrivate::isValidIndex(int index) const { return delegateModel && index >= 0 && index < delegateModel->count(); @@ -770,6 +794,16 @@ void QQuickComboBoxPrivate::itemImplicitWidthChanged(QQuickItem *item) emit q->implicitIndicatorWidthChanged(); } +void QQuickComboBoxPrivate::setInputMethodHints(Qt::InputMethodHints hints, bool force) +{ + Q_Q(QQuickComboBox); + if (!force && hints == q->inputMethodHints()) + return; + + extra.value().inputMethodHints = hints; + emit q->inputMethodHintsChanged(); +} + void QQuickComboBoxPrivate::itemImplicitHeightChanged(QQuickItem *item) { Q_Q(QQuickComboBox); @@ -804,7 +838,8 @@ QQuickComboBox::QQuickComboBox(QQuickItem *parent) #if QT_CONFIG(cursor) setCursor(Qt::ArrowCursor); #endif - setInputMethodHints(Qt::ImhNoPredictiveText); + Q_D(QQuickComboBox); + d->setInputMethodHints(Qt::ImhNoPredictiveText, true); } QQuickComboBox::~QQuickComboBox() @@ -1437,11 +1472,7 @@ Qt::InputMethodHints QQuickComboBox::inputMethodHints() const void QQuickComboBox::setInputMethodHints(Qt::InputMethodHints hints) { Q_D(QQuickComboBox); - if (hints == inputMethodHints()) - return; - - d->extra.value().inputMethodHints = hints; - emit inputMethodHintsChanged(); + d->setInputMethodHints(hints); } /*! @@ -1476,7 +1507,7 @@ bool QQuickComboBox::isInputMethodComposing() const bool QQuickComboBox::hasAcceptableInput() const { Q_D(const QQuickComboBox); - return d->contentItem && d->contentItem->property("acceptableInput").toBool(); + return d->m_acceptableInput; } /*! @@ -1731,7 +1762,11 @@ void QQuickComboBox::focusInEvent(QFocusEvent *event) { Q_D(QQuickComboBox); QQuickControl::focusInEvent(event); - if (d->contentItem && isEditable()) + // Setting focus on TextField should not be done when drop down indicator was clicked + // That is why, if focus is not set with key reason, it should not be passed to textEdit by default. + // Focus on Edit Text should be set only intentionally by user. + if ((event->reason() == Qt::TabFocusReason || event->reason() == Qt::BacktabFocusReason || + event->reason() == Qt::ShortcutFocusReason) && d->contentItem && isEditable()) d->contentItem->forceActiveFocus(event->reason()); } @@ -1913,7 +1948,7 @@ void QQuickComboBox::contentItemChange(QQuickItem *newItem, QQuickItem *oldItem) QObjectPrivate::disconnect(oldInput, &QQuickTextInput::accepted, d, &QQuickComboBoxPrivate::acceptInput); QObjectPrivate::disconnect(oldInput, &QQuickTextInput::textChanged, d, &QQuickComboBoxPrivate::updateEditText); disconnect(oldInput, &QQuickTextInput::inputMethodComposingChanged, this, &QQuickComboBox::inputMethodComposingChanged); - disconnect(oldInput, &QQuickTextInput::acceptableInputChanged, this, &QQuickComboBox::acceptableInputChanged); + QObjectPrivate::disconnect(oldInput, &QQuickTextInput::acceptableInputChanged, d, &QQuickComboBoxPrivate::updateAcceptableInput); } } if (newItem && isEditable()) { @@ -1922,12 +1957,14 @@ void QQuickComboBox::contentItemChange(QQuickItem *newItem, QQuickItem *oldItem) QObjectPrivate::connect(newInput, &QQuickTextInput::accepted, d, &QQuickComboBoxPrivate::acceptInput); QObjectPrivate::connect(newInput, &QQuickTextInput::textChanged, d, &QQuickComboBoxPrivate::updateEditText); connect(newInput, &QQuickTextInput::inputMethodComposingChanged, this, &QQuickComboBox::inputMethodComposingChanged); - connect(newInput, &QQuickTextInput::acceptableInputChanged, this, &QQuickComboBox::acceptableInputChanged); + QObjectPrivate::connect(newInput, &QQuickTextInput::acceptableInputChanged, d, &QQuickComboBoxPrivate::updateAcceptableInput); } #if QT_CONFIG(cursor) newItem->setCursor(Qt::IBeamCursor); #endif } + + d->updateAcceptableInput(); } void QQuickComboBox::localeChange(const QLocale &newLocale, const QLocale &oldLocale) diff --git a/src/quicktemplates2/qquickcontrol.cpp b/src/quicktemplates2/qquickcontrol.cpp index bbbd0e62..a719efd3 100644 --- a/src/quicktemplates2/qquickcontrol.cpp +++ b/src/quicktemplates2/qquickcontrol.cpp @@ -177,11 +177,19 @@ bool QQuickControlPrivate::acceptTouch(const QTouchEvent::TouchPoint &point) return true; } - // If the control is on a Flickable that has a pressDelay, then the press is never - // sent as a touch event, therefore we need to check for this case. - if (touchId == -1 && pressWasTouch && point.state() == Qt::TouchPointReleased && - point.pos() == previousPressPos) { - return true; + // If the control is on a Flickable that has a pressDelay, the press is sent + // as a mouse event rather than touch; so it detect and deal with it. + if (touchId == -1 && pressWasTouch) { + const auto delta = QVector2D(point.pos() - previousPressPos); + const bool overThreshold = QQuickWindowPrivate::dragOverThreshold(delta); + if (point.state() == Qt::TouchPointReleased && !overThreshold) { + // touchpoint was released near the press position: don't expect any more events, but just handle the release + return true; + } else if (point.state() == Qt::TouchPointMoved && overThreshold) { + // touchpoint was dragged over the drag threshold: accept it, and remember to handle all moves from now on + touchId = point.id(); + return true; + } } return false; } @@ -845,6 +853,13 @@ void QQuickControlPrivate::executeBackground(bool complete) quickCompleteDeferred(q, backgroundName(), background); } +/* + \internal + + Hides an item that was replaced by a newer one, rather than + deleting it, as the item is typically created in QML and hence + we don't own it. +*/ void QQuickControlPrivate::hideOldItem(QQuickItem *item) { if (!item) @@ -863,6 +878,29 @@ void QQuickControlPrivate::hideOldItem(QQuickItem *item) #endif } +/* + \internal + + Named "unhide" because it's used for cases where an item + that was previously hidden by \l hideOldItem() wants to be + shown by a control again, such as a ScrollBar in ScrollView. +*/ +void QQuickControlPrivate::unhideOldItem(QQuickControl *control, QQuickItem *item) +{ + Q_ASSERT(item); + qCDebug(lcItemManagement) << "unhiding old item" << item; + + item->setVisible(true); + item->setParentItem(control); + +#if QT_CONFIG(accessibility) + // Add the item back in to the accessibility tree. + QQuickAccessibleAttached *accessible = accessibleAttached(item); + if (accessible) + accessible->setIgnored(false); +#endif +} + void QQuickControlPrivate::updateBaselineOffset() { Q_Q(QQuickControl); diff --git a/src/quicktemplates2/qquickcontrol_p_p.h b/src/quicktemplates2/qquickcontrol_p_p.h index fa06c97f..a6e624c9 100644 --- a/src/quicktemplates2/qquickcontrol_p_p.h +++ b/src/quicktemplates2/qquickcontrol_p_p.h @@ -122,7 +122,7 @@ public: void setRightInset(qreal value, bool reset = false); void setBottomInset(qreal value, bool reset = false); - void resizeBackground(); + virtual void resizeBackground(); virtual void resizeContent(); virtual QQuickItem *getContentItem(); @@ -173,6 +173,7 @@ public: virtual void executeBackground(bool complete = false); static void hideOldItem(QQuickItem *item); + static void unhideOldItem(QQuickControl *control, QQuickItem *item); void updateBaselineOffset(); diff --git a/src/quicktemplates2/qquickdial.cpp b/src/quicktemplates2/qquickdial.cpp index b64d8d6e..906f952a 100644 --- a/src/quicktemplates2/qquickdial.cpp +++ b/src/quicktemplates2/qquickdial.cpp @@ -41,6 +41,8 @@ #include <QtQuick/private/qquickflickable_p.h> #include <QtQuickTemplates2/private/qquickcontrol_p_p.h> +#include <cmath> + QT_BEGIN_NAMESPACE /*! @@ -116,25 +118,37 @@ public: void cancelHandle(); void executeHandle(bool complete = false); + void updateAllValuesAreInteger(); + qreal from = 0; qreal to = 1; qreal value = 0; qreal position = 0; qreal angle = startAngle; qreal stepSize = 0; - bool pressed = false; QPointF pressPoint; qreal positionBeforePress = 0; QQuickDial::SnapMode snapMode = QQuickDial::NoSnap; QQuickDial::InputMode inputMode = QQuickDial::Circular; + QQuickDeferredPointer<QQuickItem> handle; bool wrap = false; bool live = true; - QQuickDeferredPointer<QQuickItem> handle; + bool pressed = false; + bool allValuesAreInteger = false; }; qreal QQuickDialPrivate::valueAt(qreal position) const { - return from + (to - from) * position; + qreal value = from + (to - from) * position; + + /* play nice with users expecting that integer from, to and stepSize leads to + integer values - given that we are using floating point internally (and in + the API of value), this does not hold, but it is easy enough to handle + */ + if (allValuesAreInteger) + value = qRound(value); + + return value; } qreal QQuickDialPrivate::snapPosition(qreal position) const @@ -308,6 +322,16 @@ void QQuickDialPrivate::executeHandle(bool complete) quickCompleteDeferred(q, handleName(), handle); } +static bool areRepresentableAsInteger(qreal num1, qreal num2, qreal num3) { + auto check = [](qreal number) -> bool { return std::nearbyint(number) == number; }; + return check(num1) && check(num2) && check(num3); +} + +void QQuickDialPrivate::updateAllValuesAreInteger() +{ + allValuesAreInteger = areRepresentableAsInteger(to, from, stepSize) && stepSize != 0.0; +} + QQuickDial::QQuickDial(QQuickItem *parent) : QQuickControl(*(new QQuickDialPrivate), parent) { @@ -342,6 +366,7 @@ void QQuickDial::setFrom(qreal from) d->from = from; emit fromChanged(); + d->updateAllValuesAreInteger(); if (isComponentComplete()) { setValue(d->value); d->updatePosition(); @@ -369,6 +394,7 @@ void QQuickDial::setTo(qreal to) return; d->to = to; + d->updateAllValuesAreInteger(); emit toChanged(); if (isComponentComplete()) { setValue(d->value); @@ -468,6 +494,7 @@ void QQuickDial::setStepSize(qreal step) return; d->stepSize = step; + d->updateAllValuesAreInteger(); emit stepSizeChanged(); } diff --git a/src/quicktemplates2/qquickheaderview.cpp b/src/quicktemplates2/qquickheaderview.cpp index 85291bcb..d99c09eb 100644 --- a/src/quicktemplates2/qquickheaderview.cpp +++ b/src/quicktemplates2/qquickheaderview.cpp @@ -364,18 +364,22 @@ QModelIndex QHeaderDataProxyModel::parent(const QModelIndex &child) const return QModelIndex(); } -QModelIndex QHeaderDataProxyModel::sibling(int row, int column, const QModelIndex &idx) const +QModelIndex QHeaderDataProxyModel::sibling(int row, int column, const QModelIndex &) const { - return index(row, column, idx); + return index(row, column); } int QHeaderDataProxyModel::rowCount(const QModelIndex &parent) const { + if (parent.isValid()) + return 0; return m_model.isNull() ? -1 : (m_orientation == Qt::Horizontal ? 1 : m_model->rowCount(parent)); } int QHeaderDataProxyModel::columnCount(const QModelIndex &parent) const { + if (parent.isValid()) + return 0; return m_model.isNull() ? -1 : (m_orientation == Qt::Vertical ? 1 : m_model->columnCount(parent)); } @@ -401,7 +405,8 @@ bool QHeaderDataProxyModel::setData(const QModelIndex &index, const QVariant &va bool QHeaderDataProxyModel::hasChildren(const QModelIndex &parent) const { - Q_UNUSED(parent) + if (!parent.isValid()) + return rowCount(parent) > 0 && columnCount(parent) > 0; return false; } diff --git a/src/quicktemplates2/qquickmenu.cpp b/src/quicktemplates2/qquickmenu.cpp index 9fd63587..0043dddd 100644 --- a/src/quicktemplates2/qquickmenu.cpp +++ b/src/quicktemplates2/qquickmenu.cpp @@ -295,8 +295,8 @@ QQuickItem *QQuickMenuPrivate::beginCreateItem() QQuickItem *item = qobject_cast<QQuickItem *>(object); if (!item) delete object; - - QQml_setParent_noEvent(item, q); + else + QQml_setParent_noEvent(item, q); return item; } diff --git a/src/quicktemplates2/qquickoverlay.cpp b/src/quicktemplates2/qquickoverlay.cpp index b8d417f2..91bd5918 100644 --- a/src/quicktemplates2/qquickoverlay.cpp +++ b/src/quicktemplates2/qquickoverlay.cpp @@ -471,22 +471,27 @@ bool QQuickOverlay::childMouseEventFilter(QQuickItem *item, QEvent *event) // background dimming OR over another popup underneath, in case the popup // does not have background dimming. if (item == p->dimmer || !p->popupItem->isAncestorOf(item)) { + bool handled = false; switch (event->type()) { #if QT_CONFIG(quicktemplates2_multitouch) case QEvent::TouchBegin: case QEvent::TouchUpdate: case QEvent::TouchEnd: - return d->handleTouchEvent(item, static_cast<QTouchEvent *>(event), popup); + handled = d->handleTouchEvent(item, static_cast<QTouchEvent *>(event), popup); + break; #endif case QEvent::MouseButtonPress: case QEvent::MouseMove: case QEvent::MouseButtonRelease: - return d->handleMouseEvent(item, static_cast<QMouseEvent *>(event), popup); + handled = d->handleMouseEvent(item, static_cast<QMouseEvent *>(event), popup); + break; default: break; } + if (handled) + return true; } } return false; diff --git a/src/quicktemplates2/qquickpage.cpp b/src/quicktemplates2/qquickpage.cpp index 0a72bad7..bc27740b 100644 --- a/src/quicktemplates2/qquickpage.cpp +++ b/src/quicktemplates2/qquickpage.cpp @@ -244,6 +244,9 @@ QQuickPage::~QQuickPage() The title is often displayed at the top of a page to give the user context about the page they are viewing. + Page does not render the title itself, but instead relies + on the application to do so. For example: + \code ApplicationWindow { visible: true diff --git a/src/quicktemplates2/qquickpopup.cpp b/src/quicktemplates2/qquickpopup.cpp index 1a7731f4..7df80a04 100644 --- a/src/quicktemplates2/qquickpopup.cpp +++ b/src/quicktemplates2/qquickpopup.cpp @@ -51,6 +51,8 @@ QT_BEGIN_NAMESPACE +Q_LOGGING_CATEGORY(lcDimmer, "qt.quick.controls.popup.dimmer") + /*! \qmltype Popup \inherits QtObject @@ -215,6 +217,19 @@ QT_BEGIN_NAMESPACE To ensure that the popup is positioned within the bounds of the enclosing window, the \l margins property can be set to a non-negative value. + \section1 Popup Transitions + + Since Qt 5.15.3 the following properties are restored to their original values from before + the enter transition after the exit transition is completed. + + \list + \li \l opacity + \li \l scale + \endlist + + This allows the built-in styles to animate on these properties without losing any explicitly + defined value. + \sa {Popup Controls}, {Customizing Popup}, ApplicationWindow */ @@ -441,6 +456,9 @@ bool QQuickPopupPrivate::prepareEnterTransition() popupItem->setVisible(true); getPositioner()->setParentItem(parentItem); emit q->visibleChanged(); + + if (focus) + popupItem->setFocus(true); } return true; } @@ -451,6 +469,11 @@ bool QQuickPopupPrivate::prepareExitTransition() if (transitionState == ExitTransition && transitionManager.isRunning()) return false; + // We need to cache the original scale and opacity values so we can reset it after + // the exit transition is done so they have the original values again + prevScale = popupItem->scale(); + prevOpacity = popupItem->opacity(); + if (transitionState != ExitTransition) { // The setFocus(false) call below removes any active focus before we're // able to check it in finalizeExitTransition. @@ -469,8 +492,6 @@ bool QQuickPopupPrivate::prepareExitTransition() void QQuickPopupPrivate::finalizeEnterTransition() { Q_Q(QQuickPopup); - if (focus) - popupItem->setFocus(true); transitionState = NoTransition; getPositioner()->reposition(); emit q->openedChanged(); @@ -481,8 +502,10 @@ void QQuickPopupPrivate::finalizeExitTransition() { Q_Q(QQuickPopup); getPositioner()->setParentItem(nullptr); - popupItem->setParentItem(nullptr); - popupItem->setVisible(false); + if (popupItem) { + popupItem->setParentItem(nullptr); + popupItem->setVisible(false); + } destroyOverlay(); if (hadActiveFocusBeforeExitTransition && window) { @@ -491,13 +514,14 @@ void QQuickPopupPrivate::finalizeExitTransition() if (QQuickOverlay *overlay = QQuickOverlay::overlay(window)) { const auto stackingOrderPopups = QQuickOverlayPrivate::get(overlay)->stackingOrderPopups(); for (auto popup : stackingOrderPopups) { - if (QQuickPopupPrivate::get(popup)->transitionState != ExitTransition) { + if (QQuickPopupPrivate::get(popup)->transitionState != ExitTransition + && popup->hasFocus()) { nextFocusPopup = popup; break; } } } - if (nextFocusPopup && nextFocusPopup->hasFocus()) { + if (nextFocusPopup) { nextFocusPopup->forceActiveFocus(); } else { QQuickApplicationWindow *applicationWindow = qobject_cast<QQuickApplicationWindow*>(window); @@ -513,6 +537,10 @@ void QQuickPopupPrivate::finalizeExitTransition() hadActiveFocusBeforeExitTransition = false; emit q->visibleChanged(); emit q->closed(); + if (popupItem) { + popupItem->setScale(prevScale); + popupItem->setOpacity(prevOpacity); + } } QMarginsF QQuickPopupPrivate::getMargins() const @@ -575,7 +603,6 @@ void QQuickPopupPrivate::setBottomMargin(qreal value, bool reset) /*! \since QtQuick.Controls 2.5 (Qt 5.12) - \qmlpropertygroup QtQuick.Controls::Popup::anchors \qmlproperty Object QtQuick.Controls::Popup::anchors.centerIn Anchors provide a way to position an item by specifying its @@ -703,6 +730,8 @@ static QQuickItem *createDimmer(QQmlComponent *component, QQuickPopup *popup, QQ if (component) component->completeCreate(); } + qCDebug(lcDimmer) << "finished creating dimmer from component" << component + << "for popup" << popup << "with parent" << parent << "- item is:" << item; return item; } @@ -729,6 +758,7 @@ void QQuickPopupPrivate::createOverlay() void QQuickPopupPrivate::destroyOverlay() { if (dimmer) { + qCDebug(lcDimmer) << "destroying dimmer" << dimmer; dimmer->setParentItem(nullptr); dimmer->deleteLater(); dimmer = nullptr; @@ -827,6 +857,12 @@ QQuickPopup::~QQuickPopup() d->popupItem = nullptr; delete d->positioner; d->positioner = nullptr; + + // If the popup is destroyed before the exit transition finishes, + // the necessary cleanup (removing modal dimmers that block mouse events, + // emitting closed signal, etc.) won't happen. That's why we do it manually here. + if (d->transitionState == QQuickPopupPrivate::ExitTransition && d->transitionManager.isRunning()) + d->finalizeExitTransition(); } /*! diff --git a/src/quicktemplates2/qquickpopup_p_p.h b/src/quicktemplates2/qquickpopup_p_p.h index 8a85f914..ef4b112e 100644 --- a/src/quicktemplates2/qquickpopup_p_p.h +++ b/src/quicktemplates2/qquickpopup_p_p.h @@ -196,6 +196,8 @@ public: QList<QQuickStateAction> exitActions; QQuickPopupTransitionManager transitionManager; QQuickPopupAnchors *anchors = nullptr; + qreal prevOpacity = 0; + qreal prevScale = 0; friend class QQuickPopupTransitionManager; }; diff --git a/src/quicktemplates2/qquickrangeslider.cpp b/src/quicktemplates2/qquickrangeslider.cpp index 3702c971..f1e5c805 100644 --- a/src/quicktemplates2/qquickrangeslider.cpp +++ b/src/quicktemplates2/qquickrangeslider.cpp @@ -665,6 +665,10 @@ void QQuickRangeSlider::setFrom(qreal from) if (isComponentComplete()) { d->first->setValue(d->first->value()); d->second->setValue(d->second->value()); + auto *firstPrivate = QQuickRangeSliderNodePrivate::get(d->first); + auto *secondPrivate = QQuickRangeSliderNodePrivate::get(d->second); + firstPrivate->updatePosition(true); + secondPrivate->updatePosition(); } } @@ -693,6 +697,10 @@ void QQuickRangeSlider::setTo(qreal to) if (isComponentComplete()) { d->first->setValue(d->first->value()); d->second->setValue(d->second->value()); + auto *firstPrivate = QQuickRangeSliderNodePrivate::get(d->first); + auto *secondPrivate = QQuickRangeSliderNodePrivate::get(d->second); + firstPrivate->updatePosition(true); + secondPrivate->updatePosition(); } } @@ -746,7 +754,6 @@ qreal QQuickRangeSlider::valueAt(qreal position) const } /*! - \qmlpropertygroup QtQuick.Controls::RangeSlider::first \qmlproperty real QtQuick.Controls::RangeSlider::first.value \qmlproperty real QtQuick.Controls::RangeSlider::first.position \qmlproperty real QtQuick.Controls::RangeSlider::first.visualPosition @@ -825,7 +832,6 @@ QQuickRangeSliderNode *QQuickRangeSlider::first() const */ /*! - \qmlpropertygroup QtQuick.Controls::RangeSlider::second \qmlproperty real QtQuick.Controls::RangeSlider::second.value \qmlproperty real QtQuick.Controls::RangeSlider::second.position \qmlproperty real QtQuick.Controls::RangeSlider::second.visualPosition diff --git a/src/quicktemplates2/qquickscrollbar.cpp b/src/quicktemplates2/qquickscrollbar.cpp index 4733d665..f0783708 100644 --- a/src/quicktemplates2/qquickscrollbar.cpp +++ b/src/quicktemplates2/qquickscrollbar.cpp @@ -77,7 +77,7 @@ QT_BEGIN_NAMESPACE \list \li \l orientation \li \l position - \li \l size + \li \l {ScrollBar::} {size} \li \l active \endlist @@ -797,6 +797,14 @@ void QQuickScrollBarAttachedPrivate::initHorizontal() if (parent && parent == flickable->parentItem()) horizontal->stackAfter(flickable); + // If a scroll bar was previously hidden (due to e.g. setting a new contentItem + // on a ScrollView), we need to make sure that we un-hide it. + // We don't bother checking if the item is actually the old one, because + // if it's not, all of the things the function does (setting parent, visibility, etc.) + // should be no-ops anyway. + if (auto control = qobject_cast<QQuickControl*>(q_func()->parent())) + QQuickControlPrivate::unhideOldItem(control, horizontal); + layoutHorizontal(); horizontal->setSize(area->property("widthRatio").toReal()); horizontal->setPosition(area->property("xPosition").toReal()); @@ -818,6 +826,9 @@ void QQuickScrollBarAttachedPrivate::initVertical() if (parent && parent == flickable->parentItem()) vertical->stackAfter(flickable); + if (auto control = qobject_cast<QQuickControl*>(q_func()->parent())) + QQuickControlPrivate::unhideOldItem(control, vertical); + layoutVertical(); vertical->setSize(area->property("heightRatio").toReal()); vertical->setPosition(area->property("yPosition").toReal()); @@ -827,6 +838,16 @@ void QQuickScrollBarAttachedPrivate::cleanupHorizontal() { Q_ASSERT(flickable && horizontal); + QQuickControlPrivate::hideOldItem(horizontal); + // ScrollBar.qml has a binding to visible and ScrollView.qml has a binding to parent. + // If we just set visible to false and parent to null, these bindings will overwrite + // them upon component completion as part of the binding evaluation. + // That's why we remove the binding completely. + const QQmlProperty visibleProperty(horizontal, QStringLiteral("visible")); + const QQmlProperty parentProperty(horizontal, QStringLiteral("parent")); + QQmlPropertyPrivate::removeBinding(visibleProperty); + QQmlPropertyPrivate::removeBinding(parentProperty); + disconnect(flickable, &QQuickFlickable::movingHorizontallyChanged, this, &QQuickScrollBarAttachedPrivate::activateHorizontal); // TODO: export QQuickFlickableVisibleArea @@ -839,6 +860,12 @@ void QQuickScrollBarAttachedPrivate::cleanupVertical() { Q_ASSERT(flickable && vertical); + QQuickControlPrivate::hideOldItem(vertical); + const QQmlProperty visibleProperty(vertical, QStringLiteral("visible")); + const QQmlProperty parentProperty(vertical, QStringLiteral("parent")); + QQmlPropertyPrivate::removeBinding(visibleProperty); + QQmlPropertyPrivate::removeBinding(parentProperty); + disconnect(flickable, &QQuickFlickable::movingVerticallyChanged, this, &QQuickScrollBarAttachedPrivate::activateVertical); // TODO: export QQuickFlickableVisibleArea @@ -869,6 +896,9 @@ class QQuickFriendlyFlickable : public QQuickFlickable void QQuickScrollBarAttachedPrivate::scrollHorizontal() { + if (!flickable) + return; + QQuickFriendlyFlickable *f = reinterpret_cast<QQuickFriendlyFlickable *>(flickable); const qreal viewwidth = f->width(); @@ -881,6 +911,9 @@ void QQuickScrollBarAttachedPrivate::scrollHorizontal() void QQuickScrollBarAttachedPrivate::scrollVertical() { + if (!flickable) + return; + QQuickFriendlyFlickable *f = reinterpret_cast<QQuickFriendlyFlickable *>(flickable); const qreal viewheight = f->height(); diff --git a/src/quicktemplates2/qquickscrollbar_p_p.h b/src/quicktemplates2/qquickscrollbar_p_p.h index 5c7628b9..c58c2ed1 100644 --- a/src/quicktemplates2/qquickscrollbar_p_p.h +++ b/src/quicktemplates2/qquickscrollbar_p_p.h @@ -107,6 +107,8 @@ public: class QQuickScrollBarAttachedPrivate : public QObjectPrivate, public QQuickItemChangeListener { + Q_DECLARE_PUBLIC(QQuickScrollBar) + public: static QQuickScrollBarAttachedPrivate *get(QQuickScrollBarAttached *attached) { diff --git a/src/quicktemplates2/qquickscrollview.cpp b/src/quicktemplates2/qquickscrollview.cpp index f385778d..01e19b16 100644 --- a/src/quicktemplates2/qquickscrollview.cpp +++ b/src/quicktemplates2/qquickscrollview.cpp @@ -38,6 +38,7 @@ #include "qquickpane_p_p.h" #include "qquickscrollbar_p_p.h" +#include <QtQml/qqmlinfo.h> #include <QtQuick/private/qquickflickable_p.h> QT_BEGIN_NAMESPACE @@ -575,7 +576,10 @@ void QQuickScrollView::contentItemChange(QQuickItem *newItem, QQuickItem *oldIte // assume/require that it has an explicit content size assigned. d->flickableHasExplicitContentWidth = true; d->flickableHasExplicitContentHeight = true; - d->setFlickable(qobject_cast<QQuickFlickable *>(newItem), false); + auto newItemAsFlickable = qobject_cast<QQuickFlickable *>(newItem); + if (newItem && !newItemAsFlickable) + qmlWarning(this) << "ScrollView only supports Flickable types as its contentItem"; + d->setFlickable(newItemAsFlickable, false); } QQuickPane::contentItemChange(newItem, oldItem); } diff --git a/src/quicktemplates2/qquickspinbox.cpp b/src/quicktemplates2/qquickspinbox.cpp index f04ceff8..a3a73472 100644 --- a/src/quicktemplates2/qquickspinbox.cpp +++ b/src/quicktemplates2/qquickspinbox.cpp @@ -726,7 +726,6 @@ void QQuickSpinBox::setValueFromText(const QJSValue &callback) } /*! - \qmlpropertygroup QtQuick.Controls::SpinBox::up \qmlproperty bool QtQuick.Controls::SpinBox::up.pressed \qmlproperty Item QtQuick.Controls::SpinBox::up.indicator \qmlproperty bool QtQuick.Controls::SpinBox::up.hovered @@ -747,7 +746,6 @@ QQuickSpinButton *QQuickSpinBox::up() const } /*! - \qmlpropertygroup QtQuick.Controls::SpinBox::down \qmlproperty bool QtQuick.Controls::SpinBox::down.pressed \qmlproperty Item QtQuick.Controls::SpinBox::down.indicator \qmlproperty bool QtQuick.Controls::SpinBox::down.hovered diff --git a/src/quicktemplates2/qquicksplitview.cpp b/src/quicktemplates2/qquicksplitview.cpp index a3566c56..a06c8d31 100644 --- a/src/quicktemplates2/qquicksplitview.cpp +++ b/src/quicktemplates2/qquicksplitview.cpp @@ -49,7 +49,7 @@ QT_BEGIN_NAMESPACE /*! \qmltype SplitView - \inherits Control + \inherits Container //! \instantiates QQuickSplitView \inqmlmodule QtQuick.Controls \since 5.13 diff --git a/src/quicktemplates2/qquickswipedelegate.cpp b/src/quicktemplates2/qquickswipedelegate.cpp index c6214243..629bf23d 100644 --- a/src/quicktemplates2/qquickswipedelegate.cpp +++ b/src/quicktemplates2/qquickswipedelegate.cpp @@ -713,6 +713,29 @@ QQuickSwipeDelegatePrivate::QQuickSwipeDelegatePrivate(QQuickSwipeDelegate *cont { } +void QQuickSwipeDelegatePrivate::resizeBackground() +{ + if (!background) + return; + + resizingBackground = true; + + QQuickItemPrivate *p = QQuickItemPrivate::get(background); + const bool extraAllocated = extra.isAllocated(); + // Don't check for or set the x here since it will just be overwritten by reposition(). + if (((!p->widthValid || !extraAllocated || !extra->hasBackgroundWidth)) + || (extraAllocated && (extra->hasLeftInset || extra->hasRightInset))) { + background->setWidth(width - getLeftInset() - getRightInset()); + } + if (((!p->heightValid || !extraAllocated || !extra->hasBackgroundHeight) && qFuzzyIsNull(background->y())) + || (extraAllocated && (extra->hasTopInset || extra->hasBottomInset))) { + background->setY(getTopInset()); + background->setHeight(height - getTopInset() - getBottomInset()); + } + + resizingBackground = false; +} + bool QQuickSwipeDelegatePrivate::handleMousePressEvent(QQuickItem *item, QMouseEvent *event) { Q_Q(QQuickSwipeDelegate); @@ -778,9 +801,10 @@ bool QQuickSwipeDelegatePrivate::handleMouseMoveEvent(QQuickItem *item, QMouseEv const QPointF mappedEventPos = item->mapToItem(q, event->pos()); const qreal distance = (mappedEventPos - pressPoint).x(); if (!q->keepMouseGrab()) { - // Taken from QQuickDrawerPrivate::grabMouse; see comments there. - int threshold = qMax(20, QGuiApplication::styleHints()->startDragDistance() + 5); - const bool overThreshold = QQuickWindowPrivate::dragOverThreshold(distance, Qt::XAxis, event, threshold); + // We used to use the custom threshold that QQuickDrawerPrivate::grabMouse used, + // but since it's larger than what Flickable uses, it results in Flickable + // stealing events from us (QTBUG-50045), so now we use the default. + const bool overThreshold = QQuickWindowPrivate::dragOverThreshold(distance, Qt::XAxis, event); if (window && overThreshold) { QQuickItem *grabber = q->window()->mouseGrabberItem(); if (!grabber || !grabber->keepMouseGrab()) { @@ -935,13 +959,15 @@ void QQuickSwipeDelegatePrivate::resizeContent() // If the background and contentItem are repositioned due to a swipe, // we don't want to call QQuickControlPrivate's implementation of this function, // as it repositions the contentItem to be visible. - // However, we still want to resize the control vertically. + // However, we still want to position the contentItem vertically + // and resize it (in case the control was resized while open). QQuickSwipePrivate *swipePrivate = QQuickSwipePrivate::get(&swipe); if (!swipePrivate->complete) { QQuickItemDelegatePrivate::resizeContent(); } else if (contentItem) { Q_Q(QQuickSwipeDelegate); contentItem->setY(q->topPadding()); + contentItem->setWidth(q->availableWidth()); contentItem->setHeight(q->availableHeight()); } } @@ -1025,7 +1051,6 @@ QQuickSwipeDelegate::QQuickSwipeDelegate(QQuickItem *parent) */ /*! - \qmlpropertygroup QtQuick.Controls::SwipeDelegate::swipe \qmlproperty real QtQuick.Controls::SwipeDelegate::swipe.position \qmlproperty bool QtQuick.Controls::SwipeDelegate::swipe.complete \qmlproperty bool QtQuick.Controls::SwipeDelegate::swipe.enabled diff --git a/src/quicktemplates2/qquickswipedelegate_p_p.h b/src/quicktemplates2/qquickswipedelegate_p_p.h index 78c72ec8..95a999a0 100644 --- a/src/quicktemplates2/qquickswipedelegate_p_p.h +++ b/src/quicktemplates2/qquickswipedelegate_p_p.h @@ -67,6 +67,7 @@ public: bool handleMouseReleaseEvent(QQuickItem *item, QMouseEvent *event); void resizeContent() override; + void resizeBackground() override; QQuickSwipe swipe; }; diff --git a/src/quicktemplates2/qquickswipeview.cpp b/src/quicktemplates2/qquickswipeview.cpp index dab20b93..98d4d22b 100644 --- a/src/quicktemplates2/qquickswipeview.cpp +++ b/src/quicktemplates2/qquickswipeview.cpp @@ -310,7 +310,6 @@ void QQuickSwipeView::geometryChanged(const QRectF &newGeometry, const QRectF &o void QQuickSwipeView::itemAdded(int index, QQuickItem *item) { Q_D(QQuickSwipeView); - QQuickItemPrivate::get(item)->setCulled(true); // QTBUG-51078, QTBUG-51669 if (isComponentComplete()) item->setSize(QSizeF(d->contentItem->width(), d->contentItem->height())); QQuickSwipeViewAttached *attached = qobject_cast<QQuickSwipeViewAttached *>(qmlAttachedPropertiesObject<QQuickSwipeView>(item)); diff --git a/src/quicktemplates2/qquicktextarea.cpp b/src/quicktemplates2/qquicktextarea.cpp index d0a08c47..64fc631d 100644 --- a/src/quicktemplates2/qquicktextarea.cpp +++ b/src/quicktemplates2/qquicktextarea.cpp @@ -358,6 +358,7 @@ void QQuickTextAreaPrivate::attachFlickable(QQuickFlickable *item) QObject::connect(flickable, &QQuickFlickable::contentYChanged, q, &QQuickItem::update); QQuickItemPrivate::get(flickable)->updateOrAddGeometryChangeListener(this, QQuickGeometryChange::Size); + QQuickItemPrivate::get(flickable)->addItemChangeListener(this, QQuickItemPrivate::Destroyed); QObjectPrivate::connect(flickable, &QQuickFlickable::contentWidthChanged, this, &QQuickTextAreaPrivate::resizeFlickableControl); QObjectPrivate::connect(flickable, &QQuickFlickable::contentHeightChanged, this, &QQuickTextAreaPrivate::resizeFlickableControl); @@ -378,6 +379,7 @@ void QQuickTextAreaPrivate::detachFlickable() QObject::disconnect(flickable, &QQuickFlickable::contentYChanged, q, &QQuickItem::update); QQuickItemPrivate::get(flickable)->updateOrRemoveGeometryChangeListener(this, QQuickGeometryChange::Nothing); + QQuickItemPrivate::get(flickable)->removeItemChangeListener(this, QQuickItemPrivate::Destroyed); QObjectPrivate::disconnect(flickable, &QQuickFlickable::contentWidthChanged, this, &QQuickTextAreaPrivate::resizeFlickableControl); QObjectPrivate::disconnect(flickable, &QQuickFlickable::contentHeightChanged, this, &QQuickTextAreaPrivate::resizeFlickableControl); @@ -562,6 +564,8 @@ void QQuickTextAreaPrivate::itemDestroyed(QQuickItem *item) background = nullptr; emit q->implicitBackgroundWidthChanged(); emit q->implicitBackgroundHeightChanged(); + } else if (item == flickable) { + detachFlickable(); } } @@ -1082,7 +1086,11 @@ QSGNode *QQuickTextArea::updatePaintNode(QSGNode *oldNode, UpdatePaintNodeData * if (d->flickable) clipper = d->flickable; - const QRectF cr = clipper->clipRect().adjusted(leftPadding(), topPadding(), -rightPadding(), -bottomPadding()); + const QRectF cr = clipper->clipRect().adjusted( + leftPadding(), topPadding(), + (!d->cursorItem && effectiveHAlign() == HAlignment::AlignRight ? 1 : 0) - rightPadding(), + -bottomPadding()); + clipNode->setRect(!d->flickable ? cr : cr.translated(d->flickable->contentX(), d->flickable->contentY())); clipNode->update(); diff --git a/src/quicktemplates2/qquicktumbler.cpp b/src/quicktemplates2/qquicktumbler.cpp index c1d1c00f..35ed9680 100644 --- a/src/quicktemplates2/qquicktumbler.cpp +++ b/src/quicktemplates2/qquicktumbler.cpp @@ -662,8 +662,9 @@ void QQuickTumblerPrivate::syncCurrentIndex() return; } - // PathView likes to use 0 as currentIndex for empty models, but we use -1 for that. - if (q->count() == 0 && actualViewIndex == 0) + // actualViewIndex might be 0 or -1 for PathView and ListView respectively, + // but we always use -1 for that. + if (q->count() == 0 && actualViewIndex <= 0) return; ignoreCurrentIndexChanges = true; diff --git a/src/quicktemplates2/qtquicktemplates2global.cpp b/src/quicktemplates2/qtquicktemplates2global.cpp new file mode 100644 index 00000000..5d7816b4 --- /dev/null +++ b/src/quicktemplates2/qtquicktemplates2global.cpp @@ -0,0 +1,63 @@ +/**************************************************************************** +** +** Copyright (C) 2021 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the QtQuick module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** 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 Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 3 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL3 included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 3 requirements +** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 2.0 or (at your option) the GNU General +** Public license version 3 or any later version approved by the KDE Free +** Qt Foundation. The licenses are as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 +** 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-2.0.html and +** https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ +#include "qquickpage_p.h" +#include "accessible/qaccessiblequickpage_p.h" + +QT_BEGIN_NAMESPACE + +#if QT_CONFIG(accessibility) +static QAccessibleInterface *qQuickAccessibleFactory(const QString &classname, QObject *object) +{ + if (classname == u"QQuickPage") { + return new QAccessibleQuickPage(qobject_cast<QQuickPage *>(object)); + } + return nullptr; +} +#endif + +void QQuickTemplates_initializeModule() +{ +#if QT_CONFIG(accessibility) + QAccessible::installFactory(&qQuickAccessibleFactory); +#endif +} + +Q_CONSTRUCTOR_FUNCTION(QQuickTemplates_initializeModule) + +QT_END_NAMESPACE diff --git a/src/quicktemplates2/qtquicktemplates2global_p.h b/src/quicktemplates2/qtquicktemplates2global_p.h index e5ee3f2e..9b6610dd 100644 --- a/src/quicktemplates2/qtquicktemplates2global_p.h +++ b/src/quicktemplates2/qtquicktemplates2global_p.h @@ -64,6 +64,8 @@ QT_BEGIN_NAMESPACE # define Q_QUICKTEMPLATES2_PRIVATE_EXPORT #endif +Q_QUICKTEMPLATES2_PRIVATE_EXPORT void QQuickTemplates_initializeModule(); + QT_END_NAMESPACE #endif // QTQUICKTEMPLATES2GLOBAL_P_H diff --git a/src/quicktemplates2/quicktemplates2.pro b/src/quicktemplates2/quicktemplates2.pro index 8ed0151a..a3f778b2 100644 --- a/src/quicktemplates2/quicktemplates2.pro +++ b/src/quicktemplates2/quicktemplates2.pro @@ -10,5 +10,11 @@ DEFINES += QT_NO_CAST_TO_ASCII QT_NO_CAST_FROM_ASCII HEADERS += \ $$PWD/qtquicktemplates2global_p.h +SOURCES += \ + $$PWD/qtquicktemplates2global.cpp + include(quicktemplates2.pri) +qtConfig(accessibility) { + include(accessible/accessible.pri) +} load(qt_module) diff --git a/tests/auto/accessibility/accessibility.pro b/tests/auto/accessibility/accessibility.pro index d8d5bb95..4cc101fb 100644 --- a/tests/auto/accessibility/accessibility.pro +++ b/tests/auto/accessibility/accessibility.pro @@ -12,5 +12,7 @@ include (../shared/util.pri) TESTDATA = data/* OTHER_FILES += \ - data/*.qml + data/defaults\*.qml \ + data/ordering\*.qml \ + data/override*.qml diff --git a/tests/auto/accessibility/data/ordering/page.qml b/tests/auto/accessibility/data/ordering/page.qml new file mode 100644 index 00000000..5eeb1530 --- /dev/null +++ b/tests/auto/accessibility/data/ordering/page.qml @@ -0,0 +1,23 @@ +import QtQuick 2.15 +import QtQuick.Controls 2.15 + +Page { + title: "Page" + Accessible.role: Accessible.Pane + + header: Label { + text: "Header" + } + + footer: Label { + text: "Footer" + } + + Label { + text: "Content item 1" + } + + Label { + text: "Content item 2" + } +} diff --git a/tests/auto/accessibility/tst_accessibility.cpp b/tests/auto/accessibility/tst_accessibility.cpp index 6e5a37df..30bd4757 100644 --- a/tests/auto/accessibility/tst_accessibility.cpp +++ b/tests/auto/accessibility/tst_accessibility.cpp @@ -60,6 +60,7 @@ private slots: void override_data(); void override(); + void ordering(); private: QQmlEngine engine; }; @@ -284,6 +285,35 @@ void tst_accessibility::override() Q_UNUSED(text) #endif } +template <typename Predicate> +void a11yDescendants(QAccessibleInterface *iface, Predicate pred) +{ + for (int i = 0; i < iface->childCount(); ++i) { + if (QAccessibleInterface *child = iface->child(i)) { + pred(child); + a11yDescendants(child, pred); + } + } +} + +void tst_accessibility::ordering() +{ + QQmlComponent component(&engine); + component.loadUrl(testFileUrl("ordering/page.qml")); + + QScopedPointer<QObject> object(component.create()); + QVERIFY2(!object.isNull(), qPrintable(component.errorString())); + +#if QT_CONFIG(accessibility) + QQuickItem *item = findItem(object.data()); + QVERIFY(item); + QAccessibleInterface *iface = QAccessible::queryAccessibleInterface(item); + QVERIFY(iface); + QStringList strings; + a11yDescendants(iface, [&](QAccessibleInterface *iface) {strings << iface->text(QAccessible::Name);}); + QCOMPARE(strings.join(QLatin1String(", ")), "Header, Content item 1, Content item 2, Footer"); +#endif +} QTEST_MAIN(tst_accessibility) diff --git a/tests/auto/controls/data/tst_abstractbutton.qml b/tests/auto/controls/data/tst_abstractbutton.qml index da5642cc..6181e526 100644 --- a/tests/auto/controls/data/tst_abstractbutton.qml +++ b/tests/auto/controls/data/tst_abstractbutton.qml @@ -910,4 +910,53 @@ TestCase { compare(clickedSpy.count, 1) compare(doubleClickedSpy.count, 1) } + + // It should be possible to quickly click a button whose doubleClicked signal + // is not connected to anything. + function test_fastClick() { + let control = createTemporaryObject(button, testCase, { text: "Hello" }) + verify(control) + + let pressedSpy = signalSpy.createObject(control, { target: control, signalName: "pressed" }) + verify(pressedSpy.valid) + + let releasedSpy = signalSpy.createObject(control, { target: control, signalName: "released" }) + verify(releasedSpy.valid) + + let clickedSpy = signalSpy.createObject(control, { target: control, signalName: "clicked" }) + verify(clickedSpy.valid) + + // Can't listen to doubleClicked because it would cause it to be emitted. + // We instead just check that clicked is emitted twice. + + mouseDoubleClickSequence(control) + compare(pressedSpy.count, 2) + compare(releasedSpy.count, 2) + compare(clickedSpy.count, 2) + + let touch = touchEvent(control) + touch.press(0, control) + touch.commit() + compare(pressedSpy.count, 3) + compare(releasedSpy.count, 2) + compare(clickedSpy.count, 2) + + touch.release(0, control) + touch.commit() + compare(pressedSpy.count, 3) + compare(releasedSpy.count, 3) + compare(clickedSpy.count, 3) + + touch.press(0, control) + touch.commit() + compare(pressedSpy.count, 4) + compare(releasedSpy.count, 3) + compare(clickedSpy.count, 3) + + touch.release(0, control) + touch.commit() + compare(pressedSpy.count, 4) + compare(releasedSpy.count, 4) + compare(clickedSpy.count, 4) + } } diff --git a/tests/auto/controls/data/tst_action.qml b/tests/auto/controls/data/tst_action.qml index 0e41b7f3..7ed4aa11 100644 --- a/tests/auto/controls/data/tst_action.qml +++ b/tests/auto/controls/data/tst_action.qml @@ -194,4 +194,37 @@ TestCase { verify(container) compare(container.indirect.nativeText, container.direct.nativeText); } + + Component { + id: shortcutCleanup + Item { + property alias page: page + property alias action: action + property alias menu: menu + Item { + id: page + Action { + id: action + text: "action" + shortcut: "Insert" + } + Menu { + id: menu + MenuItem { action: action } + } + } + } + } + + function test_shortcutCleanup() { + { + var container = createTemporaryObject(shortcutCleanup, testCase); + verify(container) + container.action.shortcut = "Delete" + container.menu.open() + container.page.destroy() + tryVerify(function() { return !container.page }) + } + keyClick(Qt.Key_Delete, Qt.NoModifier) + } } diff --git a/tests/auto/controls/data/tst_combobox.qml b/tests/auto/controls/data/tst_combobox.qml index 1c58372f..9bbea26d 100644 --- a/tests/auto/controls/data/tst_combobox.qml +++ b/tests/auto/controls/data/tst_combobox.qml @@ -157,6 +157,8 @@ TestCase { verify(control.delegate) verify(control.indicator) verify(control.popup) + verify(control.acceptableInput) + compare(control.inputMethodHints, Qt.ImhNoPredictiveText) } function test_array() { @@ -948,6 +950,56 @@ TestCase { tryCompare(control.popup, "visible", false) } + Component { + id: reopenCombo + Window { + property alias innerCombo: innerCombo + visible: true + width: 300 + height: 300 + ComboBox { + id: innerCombo + model: 10 + anchors.verticalCenter: parent.verticalCenter + } + } + } + + // This test checks that when reopening the combobox that it is still appears at the same y position as + // previously + function test_reopen_popup() { + var control = createTemporaryObject(reopenCombo, testCase) + verify(control) + var y = 0; + for (var i = 0; i < 2; ++i) { + tryCompare(control.innerCombo.popup, "visible", false) + control.innerCombo.y = control.height - (control.innerCombo.popup.contentItem.height * 0.99) + var popupYSpy = createTemporaryObject(signalSpy, testCase, {target: control.innerCombo.popup, signalName: "yChanged"}) + verify(popupYSpy.valid) + mousePress(control.innerCombo) + compare(control.innerCombo.pressed, true) + compare(control.innerCombo.popup.visible, false) + mouseRelease(control.innerCombo) + compare(control.innerCombo.pressed, false) + compare(control.innerCombo.popup.visible, true) + if (control.innerCombo.popup.enter) + tryCompare(control.innerCombo.popup.enter, "running", false) + // Check on the second opening that it has the same y position as before + if (i !== 0) { + // y should not have changed again + verify(popupYSpy.count === 0) + verify(y === control.innerCombo.popup.y) + } else { + // In some cases on the initial show, y changes more than once + verify(popupYSpy.count >= 1) + y = control.innerCombo.popup.y + mouseClick(control.innerCombo) + compare(control.innerCombo.pressed, false) + tryCompare(control.innerCombo.popup, "visible", false) + } + } + } + function test_mouse() { var control = createTemporaryObject(comboBox, testCase, {model: 3, hoverEnabled: false}) verify(control) @@ -1496,7 +1548,7 @@ TestCase { control.editText = "" compare(control.acceptableInput, true) control.editText = "" - control.forceActiveFocus() + control.contentItem.forceActiveFocus() keyPress(Qt.Key_A) compare(control.editText, "") keyPress(Qt.Key_A) @@ -1533,7 +1585,7 @@ TestCase { compare(control.currentIndex, 0) compare(control.currentText, "first") - control.forceActiveFocus() + control.contentItem.forceActiveFocus() compare(control.activeFocus, true) control.selectAll() @@ -1556,7 +1608,7 @@ TestCase { var control = createTemporaryObject(comboBox, testCase, {editable: true, model: ["Banana", "Coco", "Coconut", "Apple", "Cocomuffin"]}) verify(control) - control.forceActiveFocus() + control.contentItem.forceActiveFocus() verify(control.activeFocus) var acceptCount = 0 @@ -1707,7 +1759,7 @@ TestCase { var control = createTemporaryObject(keysAttachedBox, testCase) verify(control) - control.forceActiveFocus() + control.contentItem.forceActiveFocus() verify(control.activeFocus) verify(!control.gotit) @@ -1962,7 +2014,7 @@ TestCase { // Give the first ComboBox focus and type in 0 to select "Item 10" (default is "Item 1"). waitForRendering(comboBox1) - comboBox1.forceActiveFocus() + comboBox1.contentItem.forceActiveFocus() verify(comboBox1.activeFocus) keyClick(Qt.Key_0) compare(comboBox1.editText, "Item 10") @@ -1980,7 +2032,7 @@ TestCase { // Give focus back to the first ComboBox, and try the same thing except // with non-existing text; the currentIndex should not change. - comboBox1.forceActiveFocus() + comboBox1.contentItem.forceActiveFocus() verify(comboBox1.activeFocus) keySequence(StandardKey.SelectAll) compare(comboBox1.contentItem.selectedText, "Item 10") @@ -1992,4 +2044,62 @@ TestCase { compare(comboBox1.currentIndex, 9) compare(currentIndexSpy.count, 1) } + + // QTBUG-61021: text line should not be focused by default + // It causes (e.g. on Android) showing virtual keyboard when it is not needed + function test_doNotFocusTextLineByDefault() { + var control = createTemporaryObject(comboBox, testCase) + // Focus not set after creating combobox + verify(!control.activeFocus) + verify(!control.contentItem.focus) + + // After setting focus on combobox, text line should not be focused + control.forceActiveFocus() + verify(control.activeFocus) + verify(!control.contentItem.focus) + + // Text line is focused after intentional setting focus on it + control.contentItem.forceActiveFocus() + verify(control.activeFocus) + verify(control.contentItem.focus) + } + + Component { + id: intValidatorComponent + IntValidator { + bottom: 0 + top: 255 + } + } + + function test_acceptableInput_QTBUG_94307() { + let items = [ + { text: "A" }, + { text: "2" }, + { text: "3" } + ] + let control = createTemporaryObject(comboBox, testCase, {model: items, editable: true}) + verify(control) + + verify(control.acceptableInput) + compare(control.displayText, "A") + + let acceptableInputSpy = signalSpy.createObject(control, {target: control, signalName: "acceptableInputChanged"}) + verify(acceptableInputSpy.valid) + + let intValidator = intValidatorComponent.createObject(testCase) + verify(intValidator) + + control.validator = intValidator + + compare(acceptableInputSpy.count, 1) + compare(control.displayText, "A") + compare(control.acceptableInput, false) + + control.currentIndex = 1 + + compare(acceptableInputSpy.count, 2) + compare(control.displayText, "2") + compare(control.acceptableInput, true) + } } diff --git a/tests/auto/controls/data/tst_dial.qml b/tests/auto/controls/data/tst_dial.qml index 26f30c33..e3ad2b2f 100644 --- a/tests/auto/controls/data/tst_dial.qml +++ b/tests/auto/controls/data/tst_dial.qml @@ -691,4 +691,19 @@ TestCase { compare(control.pressed, false); compare(control.position, data.expectedPosition); } + + function test_integerStepping() { + var dial = createTemporaryObject(dialComponent, testCase) + verify(dial) + + dial.from = 1 + dial.to = 8 + dial.stepSize = 1 + + for (let i = 1; i < 8; ++i) { + // compare as strings to avoid a fuzzy compare; we want an exact match + compare(""+dial.value, ""+1) + keyClick(Qt.Key_Right) + } + } } diff --git a/tests/auto/controls/data/tst_dialogbuttonbox.qml b/tests/auto/controls/data/tst_dialogbuttonbox.qml index a651713a..63cefa46 100644 --- a/tests/auto/controls/data/tst_dialogbuttonbox.qml +++ b/tests/auto/controls/data/tst_dialogbuttonbox.qml @@ -398,7 +398,7 @@ TestCase { // QTBUG-72886 function test_changeCustomButtonText(data) { - var control = createTemporaryObject(customButtonBox, testCase, {}) + var control = createTemporaryObject(data.component, testCase, {}) verify(control) var listView = control.contentItem @@ -418,6 +418,88 @@ TestCase { } Component { + id: customButtonBoxInDialog + + Dialog { + width: 300 + visible: true + + footer: DialogButtonBox { + objectName: "customButtonBoxInDialog" + alignment: Qt.AlignRight + + property alias okButton: okButton + + Button { + id: okButton + text: "OK" + + DialogButtonBox.buttonRole: DialogButtonBox.AcceptRole + } + } + } + } + + Component { + id: customButtonBoxTwoButtonsInDialog + + Dialog { + width: 300 + visible: true + + footer: DialogButtonBox { + objectName: "customButtonBoxTwoButtonsInDialog" + alignment: Qt.AlignRight + + property alias okButton: okButton + + Button { + id: okButton + text: "OK" + + DialogButtonBox.buttonRole: DialogButtonBox.AcceptRole + } + Button { + text: "Cancel" + + DialogButtonBox.buttonRole: DialogButtonBox.RejectRole + } + } + } + } + + function test_changeCustomButtonImplicitWidth_data() { + return [ + { tag: "oneButton", component: customButtonBoxInDialog }, + { tag: "twoButtons", component: customButtonBoxTwoButtonsInDialog }, + ] + } + + // QTBUG-102558 + function test_changeCustomButtonImplicitWidth(data) { + let dialog = createTemporaryObject(data.component, testCase, {}) + verify(dialog) + + let control = dialog.footer + verify(control) + + let listView = control.contentItem + waitForRendering(listView) + + let button = control.okButton + verify(button) + button.implicitWidth *= 1.5 + + // The button should never go outside of the box. + tryVerify(function() { return button.mapToItem(control, 0, 0).x >= 0 }, + 1000, "Expected left edge of button to be within left edge of DialogButtonBox (i.e. greater than or equal to 0)" + + ", but it's " + button.mapToItem(control, 0, 0).x) + tryVerify(function() { return button.mapToItem(control, 0, 0).x + button.width <= control.width }, + 1000, "Expected right edge of button to be within right edge of DialogButtonBox (i.e. less than or equal to " + + control.width + "), but it's " + (button.mapToItem(control, 0, 0).x + button.width)) + } + + Component { id: noRolesDialog Dialog { diff --git a/tests/auto/controls/data/tst_rangeslider.qml b/tests/auto/controls/data/tst_rangeslider.qml index 24f8a207..42f5bad5 100644 --- a/tests/auto/controls/data/tst_rangeslider.qml +++ b/tests/auto/controls/data/tst_rangeslider.qml @@ -159,6 +159,43 @@ TestCase { compare(control.first.position, 0.5) } + function test_setToFromUpdatesHandles() { + var control = createTemporaryObject(sliderComponent, testCase, { from: 0, to: 100, "first.value": 50, "second.value": 75 }) + verify(control) + + let firstPos = control.first.position + let secondPos = control.second.position + + var firstPosChangesSpy = signalSpy.createObject(control, {target: control.first, signalName: "positionChanged"}) + verify(firstPosChangesSpy.valid) + + var secondPosChangesSpy = signalSpy.createObject(control, {target: control.second, signalName: "positionChanged"}) + verify(secondPosChangesSpy.valid) + + // Increasing the 'to' value, so the positions of the handles should be + // moved to the left (become smaller) + control.to = 200; + compare(firstPosChangesSpy.count, 1) + compare(secondPosChangesSpy.count, 1) + verify(control.first.position < firstPos) + verify(control.second.position < secondPos) + + // resetting the values + control.to = 100 + firstPosChangesSpy.clear() + secondPosChangesSpy.clear() + firstPos = control.first.position + secondPos = control.second.position + + // Decreasing the 'from' value, so the positions of the handles should + // be moved to the right (become larger) + control.from = -100 + compare(firstPosChangesSpy.count, 1) + compare(secondPosChangesSpy.count, 1) + verify(control.first.position > firstPos) + verify(control.second.position > secondPos) + } + function test_setValues() { var control = createTemporaryObject(sliderComponent, testCase) verify(control) @@ -612,7 +649,7 @@ TestCase { } function test_overlappingHandles() { - var control = createTemporaryObject(sliderComponent, testCase, { orientation: data.orientation }) + var control = createTemporaryObject(sliderComponent, testCase) verify(control) // By default, we force the second handle to be after the first in @@ -1064,17 +1101,16 @@ TestCase { function test_valueAt_data() { return [ - { tag: "0.0..1.0", from: 0.0, to: 1.0, values: [0.0, 0.2, 0.5, 1.0] }, - { tag: "0..100", from: 0, to: 100, values: [0, 20, 50, 100] }, - { tag: "100..-100", from: 100, to: -100, values: [100, 60, 0, -100] }, - { tag: "-7..7", from: -7, to: 7, stepSize: 1.0, values: [-7.0, -4.0, 0.0, 7.0] }, - { tag: "-3..7", from: -3, to: 7, stepSize: 5.0, values: [-3.0, -3.0, 2.0, 7.0] }, + { tag: "0.0..1.0", properties: { from: 0.0, to: 1.0 }, values: [0.0, 0.2, 0.5, 1.0] }, + { tag: "0..100", properties: { from: 0, to: 100 }, values: [0, 20, 50, 100] }, + { tag: "100..-100", properties: { from: 100, to: -100 }, values: [100, 60, 0, -100] }, + { tag: "-7..7", properties: { from: -7, to: 7, stepSize: 1.0 }, values: [-7.0, -4.0, 0.0, 7.0] }, + { tag: "-3..7", properties: { from: -3, to: 7, stepSize: 5.0 }, values: [-3.0, -3.0, 2.0, 7.0] }, ] } function test_valueAt(data) { - var control = createTemporaryObject(sliderComponent, testCase, - { from: data.from, to: data.to, stepSize: data.stepSize }) + var control = createTemporaryObject(sliderComponent, testCase, data.properties) verify(control) compare(control.valueAt(0.0), data.values[0]) diff --git a/tests/auto/controls/data/tst_scrollbar.qml b/tests/auto/controls/data/tst_scrollbar.qml index 9d21fa8b..b018899e 100644 --- a/tests/auto/controls/data/tst_scrollbar.qml +++ b/tests/auto/controls/data/tst_scrollbar.qml @@ -189,6 +189,36 @@ TestCase { compare(horizontal.width, oldWidth) } + function test_attachTwice() { + let container = createTemporaryObject(flickable, testCase) + verify(container) + waitForRendering(container) + + container.ScrollBar.vertical = scrollBar.createObject(container, { objectName: "oldVerticalScrollBar" }) + verify(container.ScrollBar.vertical) + let oldVerticalScrollBar = findChild(container, "oldVerticalScrollBar") + verify(oldVerticalScrollBar) + verify(oldVerticalScrollBar.visible) + + container.ScrollBar.horizontal = scrollBar.createObject(container, { objectName: "oldHorizontalScrollBar" }) + verify(container.ScrollBar.horizontal) + let oldHorizontalScrollBar = findChild(container, "oldHorizontalScrollBar") + verify(oldHorizontalScrollBar) + verify(oldHorizontalScrollBar.visible) + + container.ScrollBar.vertical = scrollBar.createObject(container, { objectName: "newVerticalScrollBar" }) + let newVerticalScrollBar = findChild(container, "newVerticalScrollBar") + verify(newVerticalScrollBar) + verify(newVerticalScrollBar.visible) + verify(!oldVerticalScrollBar.visible) + + container.ScrollBar.horizontal = scrollBar.createObject(container, { objectName: "newHorizontalScrollBar" }) + let newHorizontalScrollBar = findChild(container, "newHorizontalScrollBar") + verify(newHorizontalScrollBar) + verify(newHorizontalScrollBar.visible) + verify(!oldHorizontalScrollBar.visible) + } + function test_mouse_data() { return [ { tag: "horizontal", properties: { visible: true, orientation: Qt.Horizontal, width: testCase.width } }, diff --git a/tests/auto/controls/data/tst_scrollview.qml b/tests/auto/controls/data/tst_scrollview.qml index 87c39509..cd493118 100644 --- a/tests/auto/controls/data/tst_scrollview.qml +++ b/tests/auto/controls/data/tst_scrollview.qml @@ -71,6 +71,11 @@ TestCase { } Component { + id: scrollBarComponent + ScrollBar {} + } + + Component { id: scrollableLabel ScrollView { Label { @@ -188,6 +193,15 @@ TestCase { } } } + Component { + id: scrollableTextAreaWithSibling + ScrollView { + Item { + } + TextArea { + } + } + } function test_scrollBars() { var control = createTemporaryObject(scrollView, testCase, {width: 200, height: 200}) @@ -502,4 +516,111 @@ TestCase { compare(control.contentWidth, flickable.contentWidth) compare(control.contentHeight, flickable.contentHeight) } + + function test_textAreaWithSibling() { + // Checks that it does not crash when the ScrollView is deleted + var control = createTemporaryObject(scrollableTextAreaWithSibling, testCase) + verify(control) + } + + Component { + id: zeroSizedContentItemComponent + ScrollView { + width: 100 + height: 100 + contentItem: Item {} + } + } + + function test_zeroSizedContentItem() { + ignoreWarning(/ScrollView only supports Flickable types as its contentItem/) + let control = createTemporaryObject(zeroSizedContentItemComponent, testCase) + verify(control) + + let verticalScrollBar = control.ScrollBar.vertical + verify(verticalScrollBar) + // Scrolling a ScrollView with a zero-sized contentItem shouldn't crash. + mouseDrag(verticalScrollBar, verticalScrollBar.width / 2, verticalScrollBar.height / 2, 0, 50) + + let horizontalScrollBar = control.ScrollBar.horizontal + verify(verticalScrollBar) + mouseDrag(horizontalScrollBar, horizontalScrollBar.width / 2, horizontalScrollBar.height / 2, 50, 0) + } + + function test_customScrollBars() { + let control = createTemporaryObject(scrollView, testCase) + verify(control) + control.ScrollBar.vertical.objectName = "oldVerticalScrollBar" + control.ScrollBar.horizontal.objectName = "oldHorizontalScrollBar" + + let oldVerticalScrollBar = control.ScrollBar.vertical + verify(oldVerticalScrollBar) + compare(oldVerticalScrollBar.objectName, "oldVerticalScrollBar") + + let oldHorizontalScrollBar = control.ScrollBar.horizontal + verify(oldHorizontalScrollBar) + compare(oldHorizontalScrollBar.objectName, "oldHorizontalScrollBar") + + // Create the new scroll bars imperatively so that we can easily access the old ones. + control.ScrollBar.vertical = scrollBarComponent.createObject(control, { objectName: "newVerticalScrollBar" }) + verify(control.ScrollBar.vertical) + let newVerticalScrollBar = findChild(control, "newVerticalScrollBar") + verify(newVerticalScrollBar) + verify(newVerticalScrollBar.visible) + verify(!oldVerticalScrollBar.visible) + + control.ScrollBar.horizontal = scrollBarComponent.createObject(control, { objectName: "newHorizontalScrollBar" }) + verify(control.ScrollBar.horizontal) + let newHorizontalScrollBar = findChild(control, "newHorizontalScrollBar") + verify(newHorizontalScrollBar) + verify(newHorizontalScrollBar.visible) + verify(!oldHorizontalScrollBar.visible) + } + + Component { + id: bindingToContentItemAndStandaloneFlickable + + Item { + width: 200 + height: 200 + + property alias scrollView: scrollView + + ScrollView { + id: scrollView + anchors.fill: parent + contentItem: listView + + property Item someBinding: contentItem + } + ListView { + id: listView + model: 10 + delegate: ItemDelegate { + text: modelData + width: listView.width + } + } + } + } + + // Tests that scroll bars show up for a ScrollView where + // - its contentItem is declared as a standalone, separate item + // - there is a binding to contentItem (which causes a default Flickable to be created) + function test_bindingToContentItemAndStandaloneFlickable() { + let root = createTemporaryObject(bindingToContentItemAndStandaloneFlickable, testCase) + verify(root) + + let control = root.scrollView + let verticalScrollBar = control.ScrollBar.vertical + let horizontalScrollBar = control.ScrollBar.horizontal + compare(verticalScrollBar.parent, control) + compare(horizontalScrollBar.parent, control) + verify(verticalScrollBar.visible) + verify(horizontalScrollBar.visible) + + mouseDrag(verticalScrollBar, verticalScrollBar.width / 2, verticalScrollBar.height / 2, 0, 50) + verify(verticalScrollBar.active) + verify(horizontalScrollBar.active) + } } diff --git a/tests/auto/controls/data/tst_slider.qml b/tests/auto/controls/data/tst_slider.qml index 280138c5..bf65e486 100644 --- a/tests/auto/controls/data/tst_slider.qml +++ b/tests/auto/controls/data/tst_slider.qml @@ -831,16 +831,16 @@ TestCase { function test_valueAt_data() { return [ - { tag: "0.0..1.0", from: 0.0, to: 1.0, values: [0.0, 0.2, 0.5, 1.0] }, - { tag: "0..100", from: 0, to: 100, values: [0, 20, 50, 100] }, - { tag: "100..-100", from: 100, to: -100, values: [100, 60, 0, -100] }, - { tag: "-7..7", from: -7, to: 7, stepSize: 1.0, values: [-7.0, -4.0, 0.0, 7.0] }, - { tag: "-3..7", from: -3, to: 7, stepSize: 5.0, values: [-3.0, -3.0, 2.0, 7.0] }, + { tag: "0.0..1.0", properties: { from: 0.0, to: 1.0 }, values: [0.0, 0.2, 0.5, 1.0] }, + { tag: "0..100", properties: { from: 0, to: 100 }, values: [0, 20, 50, 100] }, + { tag: "100..-100", properties: { from: 100, to: -100 }, values: [100, 60, 0, -100] }, + { tag: "-7..7", properties: { from: -7, to: 7, stepSize: 1.0 }, values: [-7.0, -4.0, 0.0, 7.0] }, + { tag: "-3..7", properties: { from: -3, to: 7, stepSize: 5.0 }, values: [-3.0, -3.0, 2.0, 7.0] }, ] } function test_valueAt(data) { - var control = createTemporaryObject(slider, testCase, {from: data.from, to: data.to, stepSize: data.stepSize}) + let control = createTemporaryObject(slider, testCase, data.properties) verify(control) compare(control.valueAt(0.0), data.values[0]) @@ -923,4 +923,39 @@ TestCase { touch.release(0, control, x0 + data.dx2, y0 + data.dy2).commit() } + + Component { + id: listViewWithPressDelayAndSliders + ListView { + width: 300 + height: 500 + model: 3 + pressDelay: 150 + delegate: Slider { + width: 300 + height: 150 + } + } + } + + function test_listViewWithPressDelay() { + var listView = createTemporaryObject(listViewWithPressDelayAndSliders, testCase, { width: parent.width, height: parent.height }) + verify(listView) + var control = listView.itemAtIndex(0) + verify(control) + var movedSpy = signalSpy.createObject(control, {target: control, signalName: "moved"}) + verify(movedSpy.valid) + + var touch = touchEvent(control) + var x0 = control.handle.x + control.handle.width * 0.5 + var y0 = control.handle.y + control.handle.height * 0.5 + touch.press(0, control, x0, y0).commit() + tryCompare(control, "pressed", true) + fuzzyCompare(control.value, 0, 0.01) + + touch.move(0, control, x0 + 100, y0).commit() + tryVerify(function() { return (control.value > 0.3); }) // around 0.35, depending on style + tryVerify(function() { return (movedSpy.count > 0); }) // ideally == 1, but in Material and Fusion it's 2 + touch.release(0) + } } diff --git a/tests/auto/controls/data/tst_spinbox.qml b/tests/auto/controls/data/tst_spinbox.qml index adb70a30..14ebba21 100644 --- a/tests/auto/controls/data/tst_spinbox.qml +++ b/tests/auto/controls/data/tst_spinbox.qml @@ -244,17 +244,17 @@ TestCase { function test_keys_data() { return [ - { tag: "1", from: 1, to: 10, value: 1, stepSize: 1, upSteps: [2,3,4], downSteps: [3,2,1,1] }, - { tag: "2", from: 1, to: 10, value: 10, stepSize: 2, upSteps: [10,10], downSteps: [8,6,4] }, - { tag: "25", from: 0, to: 100, value: 50, stepSize: 25, upSteps: [75,100,100], downSteps: [75,50,25,0,0] }, - { tag: "wrap1", wrap: true, from: 1, to: 10, value: 1, stepSize: 1, upSteps: [2,3], downSteps: [2,1,10,9] }, - { tag: "wrap2", wrap: true, from: 1, to: 10, value: 10, stepSize: 2, upSteps: [1,3,5], downSteps: [3,1,10,8,6] }, - { tag: "wrap25", wrap: true, from: 0, to: 100, value: 50, stepSize: 25, upSteps: [75,100,0,25], downSteps: [0,100,75] } + { tag: "1", properties: { from: 1, to: 10, value: 1, stepSize: 1 }, upSteps: [2,3,4], downSteps: [3,2,1,1] }, + { tag: "2", properties: { from: 1, to: 10, value: 10, stepSize: 2 }, upSteps: [10,10], downSteps: [8,6,4] }, + { tag: "25", properties: { from: 0, to: 100, value: 50, stepSize: 25 }, upSteps: [75,100,100], downSteps: [75,50,25,0,0] }, + { tag: "wrap1", properties: { wrap: true, from: 1, to: 10, value: 1, stepSize: 1 }, upSteps: [2,3], downSteps: [2,1,10,9] }, + { tag: "wrap2", properties: { wrap: true, from: 1, to: 10, value: 10, stepSize: 2 }, upSteps: [1,3,5], downSteps: [3,1,10,8,6] }, + { tag: "wrap25", properties: { wrap: true, from: 0, to: 100, value: 50, stepSize: 25 }, upSteps: [75,100,0,25], downSteps: [0,100,75] } ] } function test_keys(data) { - var control = createTemporaryObject(spinBox, testCase, {wrap: data.wrap, from: data.from, to: data.to, value: data.value, stepSize: data.stepSize}) + var control = createTemporaryObject(spinBox, testCase, data.properties) verify(control) var upPressedCount = 0 @@ -396,12 +396,12 @@ TestCase { function test_wheel_data() { return [ - { tag: "1", from: 1, to: 10, value: 1, stepSize: 1, upSteps: [2,3,4], downSteps: [3,2,1,1] }, - { tag: "2", from: 1, to: 10, value: 10, stepSize: 2, upSteps: [10,10], downSteps: [8,6,4] }, - { tag: "25", from: 0, to: 100, value: 50, stepSize: 25, upSteps: [75,100,100], downSteps: [75,50,25,0,0] }, - { tag: "wrap1", wrap: true, from: 1, to: 10, value: 1, stepSize: 1, upSteps: [2,3], downSteps: [2,1,10,9] }, - { tag: "wrap2", wrap: true, from: 1, to: 10, value: 10, stepSize: 2, upSteps: [1,3,5], downSteps: [3,1,10,8,6] }, - { tag: "wrap25", wrap: true, from: 0, to: 100, value: 50, stepSize: 25, upSteps: [75,100,0,25], downSteps: [0,100,75] } + { tag: "1", properties: { from: 1, to: 10, value: 1, stepSize: 1 }, upSteps: [2,3,4], downSteps: [3,2,1,1] }, + { tag: "2", properties: { from: 1, to: 10, value: 10, stepSize: 2 }, upSteps: [10,10], downSteps: [8,6,4] }, + { tag: "25", properties: { from: 0, to: 100, value: 50, stepSize: 25 }, upSteps: [75,100,100], downSteps: [75,50,25,0,0] }, + { tag: "wrap1", properties: { wrap: true, from: 1, to: 10, value: 1, stepSize: 1 }, upSteps: [2,3], downSteps: [2,1,10,9] }, + { tag: "wrap2", properties: { wrap: true, from: 1, to: 10, value: 10, stepSize: 2 }, upSteps: [1,3,5], downSteps: [3,1,10,8,6] }, + { tag: "wrap25", properties: { wrap: true, from: 0, to: 100, value: 50, stepSize: 25 }, upSteps: [75,100,0,25], downSteps: [0,100,75] } ] } @@ -409,7 +409,8 @@ TestCase { var ma = createTemporaryObject(mouseArea, testCase, {width: 100, height: 100}) verify(ma) - var control = spinBox.createObject(ma, {wrap: data.wrap, from: data.from, to: data.to, value: data.value, stepSize: data.stepSize, wheelEnabled: true}) + data.properties.wheelEnabled = true + var control = spinBox.createObject(ma, data.properties) verify(control) var valueModifiedCount = 0 diff --git a/tests/auto/controls/data/tst_splitview.qml b/tests/auto/controls/data/tst_splitview.qml index ae8179b2..aa167472 100644 --- a/tests/auto/controls/data/tst_splitview.qml +++ b/tests/auto/controls/data/tst_splitview.qml @@ -1838,8 +1838,7 @@ TestCase { var flickable = createTemporaryObject(flickableComponent, testCase) verify(flickable) - var control = threeSizedItemsComponent.createObject(flickable.contentItem, - { "orientation": data.orientation }) + var control = threeSizedItemsComponent.createObject(flickable.contentItem) verify(control) control.anchors.fill = undefined diff --git a/tests/auto/controls/data/tst_swipedelegate.qml b/tests/auto/controls/data/tst_swipedelegate.qml index d37ea42b..255dd881 100644 --- a/tests/auto/controls/data/tst_swipedelegate.qml +++ b/tests/auto/controls/data/tst_swipedelegate.qml @@ -702,7 +702,9 @@ TestCase { property alias removeAnimation: onRemoveAnimation - ListView.onRemove: SequentialAnimation { + ListView.onRemove: onRemoveAnimation.start() + + SequentialAnimation { id: onRemoveAnimation PropertyAction { @@ -1273,10 +1275,10 @@ TestCase { // When this happens, it will grab the mouse and hence we must clear // that action's pressed state so that it doesn't stay pressed after releasing. function test_dragSideAction() { - var listView = createTemporaryObject(removableDelegatesComponent, testCase); + let listView = createTemporaryObject(removableDelegatesComponent, testCase); verify(listView); - var control = listView.itemAt(0, 0); + let control = listView.itemAt(0, 0); verify(control); // Expose the side action. @@ -1284,15 +1286,43 @@ TestCase { verify(control.swipe.leftItem); tryCompare(control.swipe, "complete", true); - var pressedSpy = signalSpyComponent.createObject(control, + let pressedSpy = signalSpyComponent.createObject(control, { target: control.swipe.leftItem.SwipeDelegate, signalName: "pressedChanged" }); verify(pressedSpy); verify(pressedSpy.valid); + let movingHorizontallySpy = createTemporaryObject(signalSpyComponent, testCase, + { target: listView, signalName: "movingHorizontallyChanged" }) + verify(movingHorizontallySpy) + verify(movingHorizontallySpy.valid) + + let movingVerticallySpy = createTemporaryObject(signalSpyComponent, testCase, + { target: listView, signalName: "movingVerticallyChanged" }) + verify(movingVerticallySpy) + verify(movingVerticallySpy.valid) + + let flickingHorizontallySpy = createTemporaryObject(signalSpyComponent, testCase, + { target: listView, signalName: "flickingHorizontallyChanged" }) + verify(flickingHorizontallySpy) + verify(flickingHorizontallySpy.valid) + + let flickingVerticallySpy = createTemporaryObject(signalSpyComponent, testCase, + { target: listView, signalName: "flickingVerticallyChanged" }) + verify(flickingVerticallySpy) + verify(flickingVerticallySpy.valid) + + // Drag the ListView vertically; its contentY should change. mouseDrag(listView, 20, 20, 0, listView.height); compare(pressedSpy.count, 2); - verify(listView.contentY !== 0); + // Wait for it to stop moving. + tryCompare(listView, "flickingVertically", false) + + // 2 because it should change to true then false. + compare(movingHorizontallySpy.count, 0) + compare(movingVerticallySpy.count, 2) + compare(flickingHorizontallySpy.count, 0) + compare(flickingVerticallySpy.count, 2) compare(control.swipe.leftItem.SwipeDelegate.pressed, false); } @@ -1713,4 +1743,39 @@ TestCase { break; } } + + function test_resizeParent() { + let container = createTemporaryObject(itemComponent, testCase, { objectName: "container", width: 100, height: 200 }) + verify(container) + + let control = swipeDelegateComponent.createObject(container, { width: Qt.binding(function() { return container.width }) }) + verify(control) + + // Resize while closed. + container.width = 200 + compare(container.width, 200) + compare(control.width, 200) + compare(control.background.width, 200) + compare(control.contentItem.width, 200 - control.leftPadding - control.rightPadding) + + // Return to original size. + container.width = 100 + compare(control.width, 100) + compare(control.background.width, 100) + compare(control.contentItem.width, 100 - control.leftPadding - control.rightPadding) + + // Swipe to the left to open. + swipe(control, 0, -1.0) + // Nothing should have changed except positions. + compare(control.width, 100) + compare(control.background.width, 100) + compare(control.contentItem.width, 100 - control.leftPadding - control.rightPadding) + + // Resize while open. + container.width = 200 + // The items should fill the width as usual. + compare(control.width, 200) + compare(control.background.width, 200) + compare(control.contentItem.width, 200 - control.leftPadding - control.rightPadding) + } } diff --git a/tests/auto/controls/data/tst_swipeview.qml b/tests/auto/controls/data/tst_swipeview.qml index 5775491c..07d0cc0c 100644 --- a/tests/auto/controls/data/tst_swipeview.qml +++ b/tests/auto/controls/data/tst_swipeview.qml @@ -623,4 +623,67 @@ TestCase { compare(control.rectanglePressCount, 1) compare(control.rectangleReleaseCount, 1) } + + // We have a particular customer who came up with this hack to make SwipeView wrap around at the end. + // It's not advisible, and perhaps the test can be removed when we add a bool wrap property. + Component { + id: pathViewWorkaroundComponent + + SwipeView { + id: swipeView + anchors.left: parent.left + width: 100 + height: 100 + clip: true + Repeater { + id: repeater + objectName: "peter" + delegate: Rectangle { + id: rect + color: "#ffff00" + border.color: "black" + width: 100 + height: 100 + Text { + anchors.verticalCenter: parent.verticalCenter + anchors.horizontalCenter: parent.horizontalCenter + font.pixelSize: 24 + text: model.index + } + } + } + contentItem: PathView { + id: pathview + + preferredHighlightBegin: 0.5 + preferredHighlightEnd: 0.5 + model: swipeView.contentModel + + path: Path { + id: path + startX: (swipeView.width / 2 * -1) * (swipeView.count - 1) + startY: swipeView.height / 2 + PathLine { + relativeX: swipeView.width * swipeView.count + relativeY: 0 + } + } + } + } + } + + function test_child_pathview() { + const control = createTemporaryObject(pathViewWorkaroundComponent, testCase) + verify(control) + const repeater = control.children[0].children[0] + const spy = signalSpy.createObject(repeater, {target: repeater, signalName: "itemAdded"}) + repeater.model = 1 + tryCompare(spy, "count", 1) + const rect = repeater.itemAt(0) + tryCompare(rect, "visible", true) + if (Qt.platform.pluginName === "offscreen") + skip("grabImage() is not functional on the offscreen platform (QTBUG-63185)") + var image = grabImage(control) + compare(image.pixel(3, 3), "#ffff00") + } } diff --git a/tests/auto/controls/data/tst_switch.qml b/tests/auto/controls/data/tst_switch.qml index b3fab41c..10b6baa0 100644 --- a/tests/auto/controls/data/tst_switch.qml +++ b/tests/auto/controls/data/tst_switch.qml @@ -608,4 +608,27 @@ TestCase { mouseClick(control.indicator) verify(control.activeFocus) } + + Component { + id: deletionOrder1 + Item { + Image { id: innerImage } + Switch { indicator: innerImage } + } + } + + Component { + id: deletionOrder2 + Item { + Switch { indicator: innerImage } + Image { id: innerImage } + } + } + + function test_deletionOrder() { + var control1 = createTemporaryObject(deletionOrder1, testCase) + verify(control1) + var control2 = createTemporaryObject(deletionOrder2, testCase) + verify(control2) + } } diff --git a/tests/auto/controls/data/tst_tumbler.qml b/tests/auto/controls/data/tst_tumbler.qml index 5b3ef6e3..fd8b7d92 100644 --- a/tests/auto/controls/data/tst_tumbler.qml +++ b/tests/auto/controls/data/tst_tumbler.qml @@ -1257,4 +1257,25 @@ TestCase { tumbler.height *= 1.4 tryCompare(delegate, "displacement", 0) } + + //QTBUG-84426 + Component { + id: initialCurrentIndexTumbler + + Tumbler { + anchors.centerIn: parent + width: 60 + height: 200 + delegate: Text {text: modelData} + model: 10 + currentIndex: 4 + } + } + + function test_initialCurrentIndex() { + var tumbler = createTemporaryObject(initialCurrentIndexTumbler, testCase, {wrap: true}); + compare(tumbler.currentIndex, 4); + tumbler = createTemporaryObject(initialCurrentIndexTumbler, testCase, {wrap: false}); + compare(tumbler.currentIndex, 4); + } } diff --git a/tests/auto/qquickapplicationwindow/data/layoutLayout.qml b/tests/auto/qquickapplicationwindow/data/layoutLayout.qml new file mode 100644 index 00000000..24eeb57b --- /dev/null +++ b/tests/auto/qquickapplicationwindow/data/layoutLayout.qml @@ -0,0 +1,65 @@ +/**************************************************************************** +** +** Copyright (C) 2021 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:BSD$ +** 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. +** +** BSD License Usage +** Alternatively, you may use this file under the terms of the BSD license +** as follows: +** +** "Redistribution and use in source and binary forms, with or without +** modification, are permitted provided that the following conditions are +** met: +** * Redistributions of source code must retain the above copyright +** notice, this list of conditions and the following disclaimer. +** * Redistributions in binary form must reproduce the above copyright +** notice, this list of conditions and the following disclaimer in +** the documentation and/or other materials provided with the +** distribution. +** * Neither the name of The Qt Company Ltd nor the names of its +** contributors may be used to endorse or promote products derived +** from this software without specific prior written permission. +** +** +** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." +** +** $QT_END_LICENSE$ +** +****************************************************************************/ +import QtQuick 2.12 +import QtQuick.Controls 2.12 +import QtQuick.Layouts 1.15 + +ApplicationWindow { + width: 200 + height: 200 + visible: true + + header: RowLayout { + Rectangle { color: "red"; implicitWidth: 20; implicitHeight: 20; Layout.fillWidth: true} + } + footer: ColumnLayout { + Rectangle { color: "green"; implicitWidth: 20; implicitHeight: 20; Layout.fillWidth: true} + } +} diff --git a/tests/auto/qquickapplicationwindow/tst_qquickapplicationwindow.cpp b/tests/auto/qquickapplicationwindow/tst_qquickapplicationwindow.cpp index 80124230..ebdc763a 100644 --- a/tests/auto/qquickapplicationwindow/tst_qquickapplicationwindow.cpp +++ b/tests/auto/qquickapplicationwindow/tst_qquickapplicationwindow.cpp @@ -76,6 +76,7 @@ private slots: void focusAfterPopupClosed(); void clearFocusOnDestruction(); void layout(); + void layoutLayout(); void componentComplete(); }; @@ -862,6 +863,42 @@ void tst_QQuickApplicationWindow::layout() QCOMPARE(content->height(), qreal(window->height())); } +void tst_QQuickApplicationWindow::layoutLayout() +{ + QQmlEngine engine; + QQmlComponent component(&engine); + component.loadUrl(testFileUrl("layoutLayout.qml")); + QScopedPointer<QObject> object(component.create()); + QVERIFY2(!object.isNull(), qPrintable(component.errorString())); + + QQuickApplicationWindow* window = qobject_cast<QQuickApplicationWindow*>(object.data()); + QVERIFY(window); + QVERIFY(QTest::qWaitForWindowExposed(window)); + + QQuickItem *content = window->contentItem(); + QVERIFY(content); + QQuickItem *header = window->header(); + QVERIFY(header); + QQuickItem *footer = window->footer(); + QVERIFY(footer); + + QQuickItem *headerChild = header->findChild<QQuickItem*>(); + QVERIFY(headerChild); + QCOMPARE(header->x(), 0.0); + QCOMPARE(header->y(), -header->height()); + QCOMPARE(header->width(), qreal(window->width())); + QCOMPARE(headerChild->width(), qreal(window->width())); + QVERIFY(header->height() > 0); + + QQuickItem *footerChild = header->findChild<QQuickItem*>(); + QVERIFY(footerChild); + QCOMPARE(footer->x(), 0.0); + QCOMPARE(footer->y(), content->height()); + QCOMPARE(footer->width(), qreal(window->width())); + QCOMPARE(footerChild->width(), qreal(window->width())); + QVERIFY(footer->height() > 0.0); +} + class FriendlyApplicationWindow : public QQuickApplicationWindow { friend class tst_QQuickApplicationWindow; diff --git a/tests/auto/qquickcontrol/tst_qquickcontrol.cpp b/tests/auto/qquickcontrol/tst_qquickcontrol.cpp index c8d34756..df3b31c8 100644 --- a/tests/auto/qquickcontrol/tst_qquickcontrol.cpp +++ b/tests/auto/qquickcontrol/tst_qquickcontrol.cpp @@ -98,9 +98,12 @@ void tst_QQuickControl::flickable() QSignalSpy buttonClickedSpy(button, SIGNAL(clicked())); QVERIFY(buttonClickedSpy.isValid()); - QTest::touchEvent(window, touchDevice.data()).press(0, QPoint(button->width() / 2, button->height() / 2)); + QPoint p(button->width() / 2, button->height() / 2); + QTest::touchEvent(window, touchDevice.data()).press(0, p); QTRY_COMPARE(buttonPressedSpy.count(), 1); - QTest::touchEvent(window, touchDevice.data()).release(0, QPoint(button->width() / 2, button->height() / 2)); + p += QPoint(1, 1); // less than the drag threshold + QTest::touchEvent(window, touchDevice.data()).move(0, p); + QTest::touchEvent(window, touchDevice.data()).release(0, p); QTRY_COMPARE(buttonReleasedSpy.count(), 1); QTRY_COMPARE(buttonClickedSpy.count(), 1); } diff --git a/tests/auto/qquickdrawer/BLACKLIST b/tests/auto/qquickdrawer/BLACKLIST new file mode 100644 index 00000000..9f3f96be --- /dev/null +++ b/tests/auto/qquickdrawer/BLACKLIST @@ -0,0 +1,8 @@ +# See qtbase/src/testlib/qtestblacklist.cpp for format + +# QTBUG-77946 +[slider] +opensuse-leap + +[position] +opensuse-leap diff --git a/tests/auto/qquickheaderview/tst_qquickheaderview.cpp b/tests/auto/qquickheaderview/tst_qquickheaderview.cpp index 611e39cb..f335aa86 100644 --- a/tests/auto/qquickheaderview/tst_qquickheaderview.cpp +++ b/tests/auto/qquickheaderview/tst_qquickheaderview.cpp @@ -58,8 +58,10 @@ public: { } - int rowCount(const QModelIndex & = QModelIndex()) const override + int rowCount(const QModelIndex &index = QModelIndex()) const override { + if (index.isValid()) + return 0; return m_rows; } virtual void setRowCount(int count) @@ -70,8 +72,10 @@ public: endResetModel(); } - int columnCount(const QModelIndex & = QModelIndex()) const override + int columnCount(const QModelIndex &index = QModelIndex()) const override { + if (index.isValid()) + return 0; return m_cols; } virtual void setColumnCount(int count) diff --git a/tests/auto/qquickimaginestyle/data/tst_imagine.qml b/tests/auto/qquickimaginestyle/data/tst_imagine.qml index b9078d78..f4ba1c71 100644 --- a/tests/auto/qquickimaginestyle/data/tst_imagine.qml +++ b/tests/auto/qquickimaginestyle/data/tst_imagine.qml @@ -153,4 +153,60 @@ TestCase { // Shouldn't result in a crash. afterRenderingSpy.wait(1000) } + + Component { + id: invalidNinePatchImageProvider + Item { + width: 200 + height: 200 + property alias ninePatchImage: np + + NinePatchImage { + id: np + source : "qrc:/test-assets/button-background-1.png" + cache: false + visible: false + } + ShaderEffect { + width: 300 + height: 300 + property variant src: np + vertexShader: " + uniform highp mat4 qt_Matrix; + attribute highp vec4 qt_Vertex; + attribute highp vec2 qt_MultiTexCoord0; + varying highp vec2 coord; + void main() { + coord = qt_MultiTexCoord0; + gl_Position = qt_Matrix * qt_Vertex; + }" + fragmentShader: " + varying highp vec2 coord; + uniform sampler2D src; + uniform lowp float qt_Opacity; + void main() { + lowp vec4 tex = texture2D(src, coord); + gl_FragColor = vec4(vec3(dot(tex.rgb, + vec3(0.344, 0.5, 0.156))), + tex.a) * qt_Opacity; + }" + } + } + } + + function test_invalidNinePatchImageProvide() { + var container = createTemporaryObject(invalidNinePatchImageProvider, testCase) + verify(container); + + var afterRenderingSpy = signalSpyComponent.createObject(null, + { target: testCase.Window.window, signalName: "afterRendering" }) + verify(afterRenderingSpy.valid) + + afterRenderingSpy.wait(100) + container.ninePatchImage.source = "" + testCase.Window.window.update() + // Shouldn't result in a crash. + wait(10) + afterRenderingSpy.wait(100) + } } diff --git a/tests/auto/qquickpopup/BLACKLIST b/tests/auto/qquickpopup/BLACKLIST new file mode 100644 index 00000000..9246d301 --- /dev/null +++ b/tests/auto/qquickpopup/BLACKLIST @@ -0,0 +1,8 @@ +# See qtbase/src/testlib/qtestblacklist.cpp for format + +# QTBUG-94251 +[closePolicy] +opensuse-leap + +[cursorShape] +opensuse-leap diff --git a/tests/auto/qquickpopup/data/activeFocusAfterExit.qml b/tests/auto/qquickpopup/data/activeFocusAfterExit.qml new file mode 100644 index 00000000..06b0c068 --- /dev/null +++ b/tests/auto/qquickpopup/data/activeFocusAfterExit.qml @@ -0,0 +1,79 @@ +/**************************************************************************** +** +** Copyright (C) 2021 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:BSD$ +** 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. +** +** BSD License Usage +** Alternatively, you may use this file under the terms of the BSD license +** as follows: +** +** "Redistribution and use in source and binary forms, with or without +** modification, are permitted provided that the following conditions are +** met: +** * Redistributions of source code must retain the above copyright +** notice, this list of conditions and the following disclaimer. +** * Redistributions in binary form must reproduce the above copyright +** notice, this list of conditions and the following disclaimer in +** the documentation and/or other materials provided with the +** distribution. +** * Neither the name of The Qt Company Ltd nor the names of its +** contributors may be used to endorse or promote products derived +** from this software without specific prior written permission. +** +** +** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +import QtQuick 2.12 +import QtQuick.Controls 2.12 + +ApplicationWindow { + width: 400 + height: 400 + + property alias popup1: popup1 + property alias popup2: popup2 + property alias popup3: popup3 + + Popup { + id: popup1 + focus: true + z: 1 + } + + Popup { + id: popup2 + focus: false + z: 2 + } + + Popup { + id: popup3 + focus: true + z: 3 + } +} diff --git a/tests/auto/qquickpopup/data/activeFocusOnDelayedEnter.qml b/tests/auto/qquickpopup/data/activeFocusOnDelayedEnter.qml new file mode 100644 index 00000000..1ceea99c --- /dev/null +++ b/tests/auto/qquickpopup/data/activeFocusOnDelayedEnter.qml @@ -0,0 +1,73 @@ +/**************************************************************************** +** +** Copyright (C) 2021 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:BSD$ +** 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. +** +** BSD License Usage +** Alternatively, you may use this file under the terms of the BSD license +** as follows: +** +** "Redistribution and use in source and binary forms, with or without +** modification, are permitted provided that the following conditions are +** met: +** * Redistributions of source code must retain the above copyright +** notice, this list of conditions and the following disclaimer. +** * Redistributions in binary form must reproduce the above copyright +** notice, this list of conditions and the following disclaimer in +** the documentation and/or other materials provided with the +** distribution. +** * Neither the name of The Qt Company Ltd nor the names of its +** contributors may be used to endorse or promote products derived +** from this software without specific prior written permission. +** +** +** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +import QtQuick 2.12 +import QtQuick.Controls 2.12 + +ApplicationWindow { + width: 400 + height: 400 + + property alias popup1: popup1 + property alias popup2: popup2 + + Popup { + id: popup1 + focus: true + enter: Transition { + NumberAnimation { property: "opacity"; from: 0.0; to: 1.0; duration: 100 } + } + } + + Popup { + id: popup2 + focus: true + } +} diff --git a/tests/auto/qquickpopup/data/destroyDuringExitTransition.qml b/tests/auto/qquickpopup/data/destroyDuringExitTransition.qml new file mode 100644 index 00000000..ae72669f --- /dev/null +++ b/tests/auto/qquickpopup/data/destroyDuringExitTransition.qml @@ -0,0 +1,114 @@ +/**************************************************************************** +** +** Copyright (C) 2021 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:BSD$ +** 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. +** +** BSD License Usage +** Alternatively, you may use this file under the terms of the BSD license +** as follows: +** +** "Redistribution and use in source and binary forms, with or without +** modification, are permitted provided that the following conditions are +** met: +** * Redistributions of source code must retain the above copyright +** notice, this list of conditions and the following disclaimer. +** * Redistributions in binary form must reproduce the above copyright +** notice, this list of conditions and the following disclaimer in +** the documentation and/or other materials provided with the +** distribution. +** * Neither the name of The Qt Company Ltd nor the names of its +** contributors may be used to endorse or promote products derived +** from this software without specific prior written permission. +** +** +** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +import QtQuick 2.15 +import QtQuick.Controls 2.15 + +ApplicationWindow { + id: window + width: 400 + height: 400 + title: "destroyDuringExitTransition" + + property Dialog dialog1 + property Dialog dialog2 + + Component { + id: dlg + + Dialog { + dim: true + modal: true + closePolicy: Popup.CloseOnEscape + visible: true + + property alias button: button + + Column { + Text { + text: "button is " + (button.down ? "down" : "up") + } + + Button { + id: button + text: "Try to press this button" + } + } + } + } + + Component { + id: brokenDlg + Dialog { + dim: true + modal: true + closePolicy: Popup.CloseOnEscape + visible: true + + Text { + text: "Press Esc key to reject this dialog" + } + + exit: Transition { + NumberAnimation { property: "opacity"; from: 1.0; to: 0.0; duration: 100 } + } + } + } + + + Component.onCompleted: { + dialog1 = dlg.createObject(window) + dialog2 = brokenDlg.createObject(window) + + dialog2.onRejected.connect(function(){ + dialog2.destroy() + }) + } +} diff --git a/tests/auto/qquickpopup/data/modelessOnModalOnModeless.qml b/tests/auto/qquickpopup/data/modelessOnModalOnModeless.qml new file mode 100644 index 00000000..7f05cb67 --- /dev/null +++ b/tests/auto/qquickpopup/data/modelessOnModalOnModeless.qml @@ -0,0 +1,98 @@ +/**************************************************************************** +** +** Copyright (C) 2021 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:BSD$ +** 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. +** +** BSD License Usage +** Alternatively, you may use this file under the terms of the BSD license +** as follows: +** +** "Redistribution and use in source and binary forms, with or without +** modification, are permitted provided that the following conditions are +** met: +** * Redistributions of source code must retain the above copyright +** notice, this list of conditions and the following disclaimer. +** * Redistributions in binary form must reproduce the above copyright +** notice, this list of conditions and the following disclaimer in +** the documentation and/or other materials provided with the +** distribution. +** * Neither the name of The Qt Company Ltd nor the names of its +** contributors may be used to endorse or promote products derived +** from this software without specific prior written permission. +** +** +** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +import QtQuick 2.13 +import QtQuick.Controls 2.13 + +ApplicationWindow { + width: 400 + height: 400 + + property alias modelessPopup: modelessPopup + property alias button: button + property alias modalPopup: modalPopup + property alias tooltip: tooltip + + Popup { + id: modelessPopup + modal: false + closePolicy: Popup.NoAutoClose + width: 200 + height: 200 + anchors.centerIn: parent + + Button { + id: button + checkable: true + x: 0 + y: 0 + text: "Click me" + } + + Popup { + id: modalPopup + modal: true + closePolicy: Popup.NoAutoClose + width: 100 + height: 100 + anchors.centerIn: parent + + Popup { + id: tooltip + modal: false + closePolicy: Popup.NoAutoClose + width: 50 + height: 50 + anchors.centerIn: parent + } + } + } + +} diff --git a/tests/auto/qquickpopup/tst_qquickpopup.cpp b/tests/auto/qquickpopup/tst_qquickpopup.cpp index 41955d7d..54952d12 100644 --- a/tests/auto/qquickpopup/tst_qquickpopup.cpp +++ b/tests/auto/qquickpopup/tst_qquickpopup.cpp @@ -73,12 +73,15 @@ private slots: void activeFocusOnClose2(); void activeFocusOnClose3(); void activeFocusOnClosingSeveralPopups(); + void activeFocusAfterExit(); + void activeFocusOnDelayedEnter(); void hover_data(); void hover(); void wheel_data(); void wheel(); void parentDestroyed(); void nested(); + void modelessOnModalOnModeless(); void grabber(); void cursorShape(); void componentComplete(); @@ -96,6 +99,7 @@ private slots: void tabFence(); void invisibleToolTipOpen(); void centerInOverlayWithinStackViewItem(); + void destroyDuringExitTransition(); }; void tst_QQuickPopup::initTestCase() @@ -727,6 +731,79 @@ void tst_QQuickPopup::activeFocusOnClosingSeveralPopups() QTRY_COMPARE(button->hasActiveFocus(), true); } +void tst_QQuickPopup::activeFocusAfterExit() +{ + // Test that after closing a popup the highest one in z-order receives it instead. + QQuickApplicationHelper helper(this, QStringLiteral("activeFocusAfterExit.qml")); + QVERIFY2(helper.ready, helper.failureMessage()); + QQuickApplicationWindow *window = helper.appWindow; + window->show(); + window->requestActivate(); + QVERIFY(QTest::qWaitForWindowActive(window)); + + QQuickPopup *popup1 = window->property("popup1").value<QQuickPopup*>(); + QVERIFY(popup1); + + QQuickPopup *popup2 = window->property("popup2").value<QQuickPopup*>(); + QVERIFY(popup2); + QSignalSpy closedSpy2(popup2, SIGNAL(closed())); + QVERIFY(closedSpy2.isValid()); + + QQuickPopup *popup3 = window->property("popup3").value<QQuickPopup*>(); + QVERIFY(popup3); + QSignalSpy closedSpy3(popup3, SIGNAL(closed())); + QVERIFY(closedSpy3.isValid()); + + popup1->open(); + QVERIFY(popup1->isVisible()); + QTRY_VERIFY(popup1->hasActiveFocus()); + + popup2->open(); + QVERIFY(popup2->isVisible()); + QTRY_VERIFY(!popup2->hasActiveFocus()); + + popup3->open(); + QVERIFY(popup3->isVisible()); + QTRY_VERIFY(popup3->hasActiveFocus()); + + popup3->close(); + closedSpy3.wait(); + QVERIFY(!popup3->isVisible()); + QTRY_VERIFY(!popup3->hasActiveFocus()); + QTRY_VERIFY(!popup2->hasActiveFocus()); + QTRY_VERIFY(popup1->hasActiveFocus()); + + popup2->close(); + closedSpy2.wait(); + QVERIFY(!popup2->isVisible()); + QTRY_VERIFY(!popup2->hasActiveFocus()); + QTRY_VERIFY(popup1->hasActiveFocus()); +} + +void tst_QQuickPopup::activeFocusOnDelayedEnter() +{ + // Test that after opening two popups, first of which has an animation, does not cause + // the first one to receive focus after the animation stops. + QQuickApplicationHelper helper(this, QStringLiteral("activeFocusOnDelayedEnter.qml")); + QVERIFY2(helper.ready, helper.failureMessage()); + QQuickApplicationWindow *window = helper.appWindow; + window->show(); + window->requestActivate(); + QVERIFY(QTest::qWaitForWindowActive(window)); + + QQuickPopup *popup1 = window->property("popup1").value<QQuickPopup*>(); + QVERIFY(popup1); + QSignalSpy openedSpy(popup1, SIGNAL(opened())); + + QQuickPopup *popup2 = window->property("popup2").value<QQuickPopup*>(); + QVERIFY(popup2); + + popup1->open(); + popup2->open(); + openedSpy.wait(); + QTRY_VERIFY(popup2->hasActiveFocus()); +} + void tst_QQuickPopup::hover_data() { QTest::addColumn<QString>("source"); @@ -914,6 +991,46 @@ void tst_QQuickPopup::nested() QCOMPARE(modalPopup->isVisible(), true); } +void tst_QQuickPopup::modelessOnModalOnModeless() +{ + QQuickApplicationHelper helper(this, QStringLiteral("modelessOnModalOnModeless.qml")); + QVERIFY2(helper.ready, helper.failureMessage()); + QQuickWindow *window = helper.window; + window->show(); + QVERIFY(QTest::qWaitForWindowExposed(window)); + + QQuickPopup *modelessPopup = window->property("modelessPopup").value<QQuickPopup *>(); + QVERIFY(modelessPopup); + + QQuickButton *button = window->property("button").value<QQuickButton *>(); + QVERIFY(button); + QQuickPopup *modalPopup = window->property("modalPopup").value<QQuickPopup *>(); + QVERIFY(modalPopup); + QQuickPopup *tooltip = window->property("tooltip").value<QQuickPopup *>(); + QVERIFY(modalPopup); + + modelessPopup->open(); + QCOMPARE(modelessPopup->isVisible(), true); + QTRY_COMPARE(modelessPopup->isOpened(), true); + const auto buttonPoint = button->mapToScene(button->boundingRect().center()).toPoint(); + // click into the button, should not be blocked + QTest::mouseClick(window, Qt::LeftButton, Qt::NoModifier, buttonPoint); + QVERIFY(button->isChecked()); + modalPopup->open(); + QCOMPARE(modalPopup->isVisible(), true); + QTRY_COMPARE(modalPopup->isOpened(), true); + // click into the button, should be blocked + QTest::mouseClick(window, Qt::LeftButton, Qt::NoModifier, buttonPoint); + QVERIFY(button->isChecked()); + + tooltip->setVisible(true); + QCOMPARE(tooltip->isVisible(), true); + QTRY_COMPARE(tooltip->isOpened(), true); + // click into the button, should be blocked + QTest::mouseClick(window, Qt::LeftButton, Qt::NoModifier, buttonPoint); + QVERIFY(button->isChecked()); +} + // QTBUG-56697 void tst_QQuickPopup::grabber() { @@ -1429,6 +1546,35 @@ void tst_QQuickPopup::centerInOverlayWithinStackViewItem() // Shouldn't crash on exit. } +void tst_QQuickPopup::destroyDuringExitTransition() +{ + QQuickApplicationHelper helper(this, "destroyDuringExitTransition.qml"); + QVERIFY2(helper.ready, helper.failureMessage()); + + QQuickWindow *window = helper.window; + window->show(); + QVERIFY(QTest::qWaitForWindowActive(window)); + + QPointer<QQuickPopup> dialog2 = window->property("dialog2").value<QQuickPopup*>(); + QVERIFY(dialog2); + QTRY_COMPARE(dialog2->isVisible(), true); + + // Close the second dialog, destroying it before its exit transition can finish. + QTest::keyClick(window, Qt::Key_Escape); + QTRY_VERIFY(!dialog2); + + // Events should go through to the dialog underneath. + QQuickPopup *dialog1 = window->property("dialog1").value<QQuickPopup*>(); + QVERIFY(dialog1); + QQuickButton *button = dialog1->property("button").value<QQuickButton*>(); + QVERIFY(button); + const auto buttonClickPos = button->mapToScene(QPointF(button->width() / 2, button->height() / 2)).toPoint(); + QTest::mousePress(window, Qt::LeftButton, Qt::NoModifier, buttonClickPos); + QVERIFY(button->isDown()); + QTest::mouseRelease(window, Qt::LeftButton, Qt::NoModifier, buttonClickPos); + QVERIFY(!button->isDown()); +} + QTEST_QUICKCONTROLS_MAIN(tst_QQuickPopup) #include "tst_qquickpopup.moc" |