diff options
39 files changed, 950 insertions, 26 deletions
diff --git a/.qmake.conf b/.qmake.conf index 8a589b4e..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.6 +MODULE_VERSION = 5.15.13 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/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/qquickninepatchimage.cpp b/src/imports/controls/imagine/qquickninepatchimage.cpp index 71243988..ed388ec1 100644 --- a/src/imports/controls/imagine/qquickninepatchimage.cpp +++ b/src/imports/controls/imagine/qquickninepatchimage.cpp @@ -439,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; } 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/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 81b851a5..af5475fa 100644 --- a/src/imports/platform/qquickplatformfiledialog.cpp +++ b/src/imports/platform/qquickplatformfiledialog.cpp @@ -552,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/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/qquickabstractbutton.cpp b/src/quicktemplates2/qquickabstractbutton.cpp index 950e4e09..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); @@ -1086,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 2041e774..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 diff --git a/src/quicktemplates2/qquickapplicationwindow.cpp b/src/quicktemplates2/qquickapplicationwindow.cpp index bb1b9b15..a9a4ad72 100644 --- a/src/quicktemplates2/qquickapplicationwindow.cpp +++ b/src/quicktemplates2/qquickapplicationwindow.cpp @@ -653,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/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 8e979079..a6e624c9 100644 --- a/src/quicktemplates2/qquickcontrol_p_p.h +++ b/src/quicktemplates2/qquickcontrol_p_p.h @@ -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/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/qquickpopup.cpp b/src/quicktemplates2/qquickpopup.cpp index bdc6dff2..7df80a04 100644 --- a/src/quicktemplates2/qquickpopup.cpp +++ b/src/quicktemplates2/qquickpopup.cpp @@ -456,6 +456,9 @@ bool QQuickPopupPrivate::prepareEnterTransition() popupItem->setVisible(true); getPositioner()->setParentItem(parentItem); emit q->visibleChanged(); + + if (focus) + popupItem->setFocus(true); } return true; } @@ -489,8 +492,6 @@ bool QQuickPopupPrivate::prepareExitTransition() void QQuickPopupPrivate::finalizeEnterTransition() { Q_Q(QQuickPopup); - if (focus) - popupItem->setFocus(true); transitionState = NoTransition; getPositioner()->reposition(); emit q->openedChanged(); @@ -513,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); diff --git a/src/quicktemplates2/qquickrangeslider.cpp b/src/quicktemplates2/qquickrangeslider.cpp index eb9c1bb1..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(); } } diff --git a/src/quicktemplates2/qquickscrollbar.cpp b/src/quicktemplates2/qquickscrollbar.cpp index 4e2f509d..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()); 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/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 d09d9c3b..64fc631d 100644 --- a/src/quicktemplates2/qquicktextarea.cpp +++ b/src/quicktemplates2/qquicktextarea.cpp @@ -1086,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/quicktemplates2.pro b/src/quicktemplates2/quicktemplates2.pro index 5138b610..a3f778b2 100644 --- a/src/quicktemplates2/quicktemplates2.pro +++ b/src/quicktemplates2/quicktemplates2.pro @@ -14,5 +14,7 @@ SOURCES += \ $$PWD/qtquicktemplates2global.cpp include(quicktemplates2.pri) -include(accessible/accessible.pri) +qtConfig(accessibility) { + include(accessible/accessible.pri) +} load(qt_module) 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_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 d645ec70..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) diff --git a/tests/auto/controls/data/tst_scrollview.qml b/tests/auto/controls/data/tst_scrollview.qml index 0e8b0835..cd493118 100644 --- a/tests/auto/controls/data/tst_scrollview.qml +++ b/tests/auto/controls/data/tst_scrollview.qml @@ -576,4 +576,51 @@ TestCase { 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 e1e1ed97..bf65e486 100644 --- a/tests/auto/controls/data/tst_slider.qml +++ b/tests/auto/controls/data/tst_slider.qml @@ -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_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/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/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/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 ede3640c..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(); @@ -728,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"); @@ -915,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() { |