diff options
author | Mitch Curtis <[email protected]> | 2025-05-06 14:00:50 +0800 |
---|---|---|
committer | Mitch Curtis <[email protected]> | 2025-05-13 10:06:11 +0800 |
commit | 72b558bd3f6f5c23dbe9776b233ee78f08d37f56 (patch) | |
tree | bec5234dc8f18b76d46c49afaeb545c3e5a3e054 | |
parent | 722116b32a5c18dde24d15232edfbecf605a240a (diff) |
Menu: force contentItem ListView layout before starting enter transition
Enter transitions may want to animate the Menu's height based on its
implicitHeight. The Menu's implicitHeight is typically based on the
ListView's contentHeight, among other things. The docs for ListView's
forceLayout function say: "Responding to changes in the model is
usually batched to happen only once per frame." As e.g.
NumberAnimation's from and to values are set before any polishes
happen, any re-evaluation of their bindings happen too late, and the
starting height can be out-dated when menu items are added after
component completion (QQuickItemView::componentComplete does a layout,
so items declared as children aren't affected by this).
To account for this, this patch forces a layout before the transition
starts, if necessary.
Fixes: QTBUG-136256
Pick-to: 6.5 6.8 6.9
Change-Id: I1cc912347b774369b3542f0d32c654ae5615b97a
Reviewed-by: Richard Moe Gustavsen <[email protected]>
Reviewed-by: Shawn Rutledge <[email protected]>
-rw-r--r-- | src/quick/items/qquickitemview_p_p.h | 2 | ||||
-rw-r--r-- | src/quicktemplates/qquickmenu.cpp | 17 | ||||
-rw-r--r-- | tests/auto/quickcontrols/qquickmenu/data/animationOnHeight.qml | 71 | ||||
-rw-r--r-- | tests/auto/quickcontrols/qquickmenu/tst_qquickmenu.cpp | 23 |
4 files changed, 112 insertions, 1 deletions
diff --git a/src/quick/items/qquickitemview_p_p.h b/src/quick/items/qquickitemview_p_p.h index 9aca664ec6..459d348b9b 100644 --- a/src/quick/items/qquickitemview_p_p.h +++ b/src/quick/items/qquickitemview_p_p.h @@ -43,7 +43,7 @@ public: }; -class Q_AUTOTEST_EXPORT QQuickItemViewChangeSet +class Q_QUICK_EXPORT QQuickItemViewChangeSet { public: QQuickItemViewChangeSet(); diff --git a/src/quicktemplates/qquickmenu.cpp b/src/quicktemplates/qquickmenu.cpp index 7b49fb7ef3..3e90e2f041 100644 --- a/src/quicktemplates/qquickmenu.cpp +++ b/src/quicktemplates/qquickmenu.cpp @@ -36,6 +36,7 @@ #include <private/qqmlobjectmodel_p.h> #include <QtQuick/private/qquickitem_p.h> #include <QtQuick/private/qquickitemchangelistener_p.h> +#include <QtQuick/private/qquickitemview_p_p.h> #include <QtQuick/private/qquickevents_p_p.h> #include <QtQuick/private/qquicklistview_p.h> #include <QtQuick/private/qquickrendercontrol_p.h> @@ -979,6 +980,22 @@ bool QQuickMenuPrivate::prepareEnterTransition() // the right, it flips on the other side of the parent menu. allowHorizontalFlip = cascade && parentMenu; + // Enter transitions may want to animate the Menu's height based on its implicitHeight. + // The Menu's implicitHeight is typically based on the ListView's contentHeight, + // among other things. The docs for ListView's forceLayout function say: + // "Responding to changes in the model is usually batched to happen only once per frame." + // As e.g. NumberAnimation's from and to values are set before any polishes happen, + // any re-evaluation of their bindings happen too late, and the starting height can be + // out-dated when menu items are added after component completion + // (QQuickItemView::componentComplete does a layout, so items declared as children aren't + // affected by this). To account for this, we force a layout before the transition starts. + // We try to avoid unnecessary re-layouting if we can avoid it. + auto *contentItemAsListView = qobject_cast<QQuickListView *>(contentItem); + if (contentItemAsListView) { + if (QQuickItemViewPrivate::get(contentItemAsListView)->currentChanges.hasPendingChanges()) + contentItemAsListView->forceLayout(); + } + if (!QQuickPopupPrivate::prepareEnterTransition()) return false; diff --git a/tests/auto/quickcontrols/qquickmenu/data/animationOnHeight.qml b/tests/auto/quickcontrols/qquickmenu/data/animationOnHeight.qml new file mode 100644 index 0000000000..265679429b --- /dev/null +++ b/tests/auto/quickcontrols/qquickmenu/data/animationOnHeight.qml @@ -0,0 +1,71 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Templates as T + +ApplicationWindow { + width: 600 + height: 400 + + Action { + id: copyAction + text: "Copy" + } + + // Based on FluentWinUI3's Menu. + T.Menu { + id: menu + popupType: Popup.Item + + implicitWidth: Math.max(implicitBackgroundWidth + leftInset + rightInset, + implicitContentWidth + leftPadding + rightPadding) + implicitHeight: Math.max(implicitBackgroundHeight + topInset + bottomInset, + implicitContentHeight + topPadding + bottomPadding) + + delegate: MenuItem { } + + contentItem: ListView { + implicitHeight: contentHeight + model: menu.contentModel + interactive: Window.window + ? contentHeight + menu.topPadding + menu.bottomPadding > menu.height + : false + currentIndex: menu.currentIndex + spacing: 4 + clip: true + + ScrollIndicator.vertical: ScrollIndicator {} + } + + enter: Transition { + NumberAnimation { + property: "height" + from: menu.implicitHeight * 0.33 + to: menu.implicitHeight + easing.type: Easing.OutCubic + duration: 1 + } + } + + background: Rectangle { + implicitWidth: 200 + implicitHeight: 30 + border.width: 1 + } + + T.Overlay.modal: Rectangle { + color: "transparent" + } + + T.Overlay.modeless: Rectangle { + color: "transparent" + } + + Action { + text: "Cut" + } + } + + Component.onCompleted: { + menu.addAction(copyAction) + } +} diff --git a/tests/auto/quickcontrols/qquickmenu/tst_qquickmenu.cpp b/tests/auto/quickcontrols/qquickmenu/tst_qquickmenu.cpp index 0e0050be6b..dc4584cb50 100644 --- a/tests/auto/quickcontrols/qquickmenu/tst_qquickmenu.cpp +++ b/tests/auto/quickcontrols/qquickmenu/tst_qquickmenu.cpp @@ -16,6 +16,7 @@ #include <QtQml/qqmlcontext.h> #include <QtQuick/qquickview.h> #include <QtQuick/private/qquickitem_p.h> +#include <QtQuick/private/qquicklistview_p.h> #include <QtQuick/private/qquickmousearea_p.h> #include <QtQuick/private/qquickrectangle_p.h> #include <QtQuickTest/quicktest.h> @@ -124,6 +125,7 @@ private slots: void resetCurrentIndexUponPopup(); void mousePropagationWithinPopup(); void shortcutInNestedSubMenuAction(); + void animationOnHeight(); private: bool nativeMenuSupported = false; @@ -3477,6 +3479,27 @@ void tst_QQuickMenu::shortcutInNestedSubMenuAction() QCOMPARE(window->property("triggeredCount").value<int>(), 1); } +void tst_QQuickMenu::animationOnHeight() +{ + QQuickControlsApplicationHelper helper(this, QLatin1String("animationOnHeight.qml")); + QVERIFY2(helper.ready, helper.failureMessage()); + QQuickApplicationWindow *window = helper.appWindow; + window->show(); + QVERIFY(QTest::qWaitForWindowExposed(window)); + + auto *menu = window->findChild<QQuickMenu *>(); + QVERIFY(menu); + menu->popup(100, 100); + QTRY_VERIFY(menu->isOpened()); + + // After adding the Action, the contentHeight of the Menu's ListView should + // be not be greater than its height; it shouldn't be possible to scroll. + auto *listView = qobject_cast<QQuickListView *>(menu->contentItem()); + const qreal itemHeight = menu->itemAt(0)->height(); + QCOMPARE(listView->contentHeight(), itemHeight * 2 + listView->spacing()); + QCOMPARE_GE(listView->height(), listView->contentHeight()); +} + QTEST_QUICKCONTROLS_MAIN(tst_QQuickMenu) #include "tst_qquickmenu.moc" |