diff options
Diffstat (limited to 'tests/auto/quick/qquicklistview2')
37 files changed, 2667 insertions, 0 deletions
diff --git a/tests/auto/quick/qquicklistview2/BLACKLIST b/tests/auto/quick/qquicklistview2/BLACKLIST new file mode 100644 index 0000000000..0162bbc852 --- /dev/null +++ b/tests/auto/quick/qquicklistview2/BLACKLIST @@ -0,0 +1,5 @@ +[tapDelegateDuringFlicking] +android # QTBUG-104471 +[flickDuringFlicking] +android # QTBUG-104471 +macos ci # QTBUG-105190 diff --git a/tests/auto/quick/qquicklistview2/CMakeLists.txt b/tests/auto/quick/qquicklistview2/CMakeLists.txt new file mode 100644 index 0000000000..faa86ce733 --- /dev/null +++ b/tests/auto/quick/qquicklistview2/CMakeLists.txt @@ -0,0 +1,46 @@ +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: BSD-3-Clause + +if(NOT QT_BUILD_STANDALONE_TESTS AND NOT QT_BUILDING_QT) + cmake_minimum_required(VERSION 3.16) + project(tst_qquicklistview2 LANGUAGES CXX) + find_package(Qt6BuildInternals REQUIRED COMPONENTS STANDALONE_TEST) +endif() + +# Collect test data +file(GLOB_RECURSE test_data_glob + RELATIVE ${CMAKE_CURRENT_SOURCE_DIR} + data/*) +list(APPEND test_data ${test_data_glob}) + +qt_internal_add_test(tst_qquicklistview2 + SOURCES + typerolemodel.h typerolemodel.cpp + tst_qquicklistview2.cpp + LIBRARIES + Qt::CorePrivate + Qt::Gui + Qt::GuiPrivate + Qt::QmlModelsPrivate + Qt::QmlPrivate + Qt::QuickPrivate + Qt::QuickTest + Qt::QuickTestUtilsPrivate + TESTDATA ${test_data} +) + +qt_policy(SET QTP0001 NEW) + +qt6_add_qml_module(tst_qquicklistview2 + URI Test +) + +qt_internal_extend_target(tst_qquicklistview2 CONDITION ANDROID OR IOS + DEFINES + QT_QMLTEST_DATADIR=":/data" +) + +qt_internal_extend_target(tst_qquicklistview2 CONDITION NOT ANDROID AND NOT IOS + DEFINES + QT_QMLTEST_DATADIR="${CMAKE_CURRENT_SOURCE_DIR}/data" +) diff --git a/tests/auto/quick/qquicklistview2/data/areaZeroView.qml b/tests/auto/quick/qquicklistview2/data/areaZeroView.qml new file mode 100644 index 0000000000..e0329f4e83 --- /dev/null +++ b/tests/auto/quick/qquicklistview2/data/areaZeroView.qml @@ -0,0 +1,22 @@ +import QtQuick + +Window { + width: 600 + height: 600 + visible: true + property int delegateCreationCounter: 0 + + ListView { + id: lv + anchors.fill: parent + model: 6000 + + delegate: Rectangle { + width: ListView.view.width + height: ListView.view.width / 6 + color: "white" + border.width: 1 + Component.onCompleted: ++delegateCreationCounter + } + } +} diff --git a/tests/auto/quick/qquicklistview2/data/bindOnHeaderAndFooterXPosition.qml b/tests/auto/quick/qquicklistview2/data/bindOnHeaderAndFooterXPosition.qml new file mode 100644 index 0000000000..69431fb525 --- /dev/null +++ b/tests/auto/quick/qquicklistview2/data/bindOnHeaderAndFooterXPosition.qml @@ -0,0 +1,29 @@ +import QtQuick + +ListView { + width: 200 + height: 300 + spacing: 20 + orientation: ListView.Vertical + + header: Rectangle { + x: (ListView.view.width - width) / 2 + color: 'tomato' + width: 50 + height: 50 + } + + footer: Rectangle { + x: (ListView.view.width - width) / 2 + color: 'lime' + width: 50 + height: 50 + } + + model: 3 + delegate: Text { + text: 'Foobar' + horizontalAlignment: Text.AlignHCenter + width: ListView.view.width + } +} diff --git a/tests/auto/quick/qquicklistview2/data/bindOnHeaderAndFooterYPosition.qml b/tests/auto/quick/qquicklistview2/data/bindOnHeaderAndFooterYPosition.qml new file mode 100644 index 0000000000..a484a154a7 --- /dev/null +++ b/tests/auto/quick/qquicklistview2/data/bindOnHeaderAndFooterYPosition.qml @@ -0,0 +1,29 @@ +import QtQuick + +ListView { + width: 300 + height: 200 + spacing: 20 + orientation: ListView.Horizontal + + header: Rectangle { + y: (ListView.view.height - height) / 2 + color: 'tomato' + width: 50 + height: 50 + } + + footer: Rectangle { + y: (ListView.view.height - height) / 2 + color: 'lime' + width: 50 + height: 50 + } + + model: 3 + delegate: Text { + text: 'Foobar' + verticalAlignment: Text.AlignVCenter + height: ListView.view.height + } +} diff --git a/tests/auto/quick/qquicklistview2/data/boundDelegateComponent.qml b/tests/auto/quick/qquicklistview2/data/boundDelegateComponent.qml new file mode 100644 index 0000000000..5e4de1a08d --- /dev/null +++ b/tests/auto/quick/qquicklistview2/data/boundDelegateComponent.qml @@ -0,0 +1,64 @@ +pragma ComponentBehavior: Bound + +import QtQuick +Item { + id: outer + objectName: "outer" + ListView { + id: listView + width: 100 + height: 100 + model: 1 + property string foo: "foo" + delegate: Text { + property var notThere: index + objectName: listView.foo + outer.objectName + notThere + } + } + + ListView { + id: listView2 + width: 100 + height: 100 + model: 1 + delegate: Text { + required property int index + objectName: listView.foo + outer.objectName + index + } + } + + Component { + id: outerComponent + Item { + ListModel { + id: listModel + ListElement { + myColor: "red" + } + + ListElement { + myColor: "green" + } + + ListElement { + myColor: "blue" + } + } + + Component { + id: innerComponent + Rectangle { + objectName: model.myColor + } + } + + ListView { + width: 100 + height: 100 + id: innerListView + model: listModel + delegate: innerComponent + } + } + } +} diff --git a/tests/auto/quick/qquicklistview2/data/buttonDelegate.qml b/tests/auto/quick/qquicklistview2/data/buttonDelegate.qml new file mode 100644 index 0000000000..a40ba1cd7e --- /dev/null +++ b/tests/auto/quick/qquicklistview2/data/buttonDelegate.qml @@ -0,0 +1,27 @@ +import QtQuick 2.15 +import QtQuick.Controls 2.15 + +ListView { + id: root + width: 320 + height: 480 + model: 100 + + property var pressedDelegates: [] + property var releasedDelegates: [] + property var tappedDelegates: [] + property var canceledDelegates: [] + + delegate: Button { + required property int index + objectName: text + text: "button " + index + height: 100 + width: 320 + + onPressed: root.pressedDelegates.push(index) + onReleased: root.releasedDelegates.push(index) + onClicked: root.tappedDelegates.push(index) + onCanceled: root.canceledDelegates.push(index) + } +} diff --git a/tests/auto/quick/qquicklistview2/data/changingOrientationWithListModel.qml b/tests/auto/quick/qquicklistview2/data/changingOrientationWithListModel.qml new file mode 100644 index 0000000000..366b20b029 --- /dev/null +++ b/tests/auto/quick/qquicklistview2/data/changingOrientationWithListModel.qml @@ -0,0 +1,47 @@ +import QtQuick + +ListView { + id: root + + function allDelegates(valueSelector) { + let sum = 0; + for (let i = 0; i < root.count; i++) + sum += valueSelector(root.itemAtIndex(i)); + return sum; + } + + readonly property bool isXReset: allDelegates(function(item) { return item?.x ?? 0; }) === 0 + readonly property bool isYReset: allDelegates(function(item) { return item?.y ?? 0; }) === 0 + + width: 500 + height: 500 + delegate: Rectangle { + width: root.width + height: root.height + color: c + } + model: ListModel { + ListElement { + c: "red" + } + ListElement { + c: "green" + } + ListElement { + c: "blue" + } + ListElement { + c: "cyan" + } + ListElement { + c: "magenta" + } + ListElement { + c: "teal" + } + } + clip: true + orientation: ListView.Vertical + snapMode: ListView.SnapOneItem + highlightRangeMode: ListView.StrictlyEnforceRange +}
\ No newline at end of file diff --git a/tests/auto/quick/qquicklistview2/data/changingOrientationWithObjectModel.qml b/tests/auto/quick/qquicklistview2/data/changingOrientationWithObjectModel.qml new file mode 100644 index 0000000000..1d165a496e --- /dev/null +++ b/tests/auto/quick/qquicklistview2/data/changingOrientationWithObjectModel.qml @@ -0,0 +1,54 @@ +import QtQuick + +ListView { + id: root + + readonly property bool isXReset: red.x === 0 && green.x === 0 && blue.x === 0 && cyan.x === 0 && magenta.x === 0 && teal.x === 0 + + readonly property bool isYReset: red.y === 0 && green.y === 0 && blue.y === 0 && cyan.y === 0 && magenta.y === 0 && teal.y === 0 + + width: 500 + height: 500 + model: ObjectModel { + Rectangle { + id: red + width: root.width + height: root.height + color: "red" + } + Rectangle { + id: green + width: root.width + height: root.height + color: "green" + } + Rectangle { + id: blue + width: root.width + height: root.height + color: "blue" + } + Rectangle { + id: cyan + width: root.width + height: root.height + color: "cyan" + } + Rectangle { + id: magenta + width: root.width + height: root.height + color: "magenta" + } + Rectangle { + id: teal + width: root.width + height: root.height + color: "teal" + } + } + clip: true + orientation: ListView.Vertical + snapMode: ListView.SnapOneItem + highlightRangeMode: ListView.StrictlyEnforceRange +}
\ No newline at end of file diff --git a/tests/auto/quick/qquicklistview2/data/delegateChooserEnumRole.qml b/tests/auto/quick/qquicklistview2/data/delegateChooserEnumRole.qml new file mode 100644 index 0000000000..66e92c5616 --- /dev/null +++ b/tests/auto/quick/qquicklistview2/data/delegateChooserEnumRole.qml @@ -0,0 +1,41 @@ +// Copyright (C) 2021 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only + +import QtQuick +import Qt.labs.qmlmodels +import Test + +ListView { + width: 300; height: 300 + model: TypeRoleModel {} + delegate: DelegateChooser { + role: "type" + DelegateChoice { + roleValue: 0 + Text { + property int delegateType: 0 + text: model.text + " of type " + model.type + } + } + DelegateChoice { + roleValue: "Markdown" + Text { + property int delegateType: 1 + text: model.text + " of **type** " + model.type + textFormat: Text.MarkdownText + } + } + DelegateChoice { + roleValue: TypeRoleModel.Rect + Rectangle { + property int delegateType: 2 + width: 300; height: 20 + color: "wheat" + Text { + text: model.text + " of type " + model.type + anchors.verticalCenter: parent.verticalCenter + } + } + } + } +} diff --git a/tests/auto/quick/qquicklistview2/data/delegateContextHandling.qml b/tests/auto/quick/qquicklistview2/data/delegateContextHandling.qml new file mode 100644 index 0000000000..4c513df905 --- /dev/null +++ b/tests/auto/quick/qquicklistview2/data/delegateContextHandling.qml @@ -0,0 +1,75 @@ +pragma ComponentBehavior: Bound + +import QtQuick +import QtQuick.Window + +Item { + id: win + height: 640 + width: 480 + + property string currentModel: 'foo' + + function toggle() : Item { + var ret = listView.itemAtIndex(0); + win.currentModel = win.currentModel === 'foo' ? 'bar' : 'foo' + + switch (win.currentModel) { + case 'foo': + if (listView.model) { + listView.model.destroy() + } + listView.model = fooModelComponent.createObject(win) + break + + case 'bar': + if (listView.model) { + listView.model.destroy() + } + listView.model = barModelComponent.createObject(win) + break + } + + return ret; + } + + Component { + id: fooModelComponent + ListModel { + ListElement { textValue: "foo1" } + } + } + + Component { + id: barModelComponent + ListModel { + ListElement { textValue: "bar1" } + } + } + + ListView { + states: [ + State { + when: win.currentModel === 'bar' + PropertyChanges { + listView.section.property: 'sectionProp' + } + } + ] + + id: listView + model: fooModelComponent.createObject(win) + anchors.fill: parent + + section.delegate: Text { + required property string section + text: section + } + + delegate: Text { + id: delg + text: delg.textValue + required property string textValue + } + } +} diff --git a/tests/auto/quick/qquicklistview2/data/delegateModelRefresh.qml b/tests/auto/quick/qquicklistview2/data/delegateModelRefresh.qml new file mode 100644 index 0000000000..7a22f1a0f4 --- /dev/null +++ b/tests/auto/quick/qquicklistview2/data/delegateModelRefresh.qml @@ -0,0 +1,53 @@ +// Copyright (C) 2021 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only + +import QtQuick +import QtQuick.Controls +import QtQml.Models + +ApplicationWindow { + id: window + visible: true + width: 400 + height: 800 + property bool done: false + + ListView { + model: delegateModel + anchors.fill: parent + } + + DelegateModel { + id: delegateModel + model: ListModel { + ListElement { + available: true + } + ListElement { + available: true + } + ListElement { + available: true + } + } + + Component.onCompleted: { + delegateModel.refresh() + done = true; + } + function refresh() { + var rowCount = delegateModel.model.count; + const flatItemsList = [] + for (var i = 0; i < rowCount; i++) { + var entry = delegateModel.model.get(i); + flatItemsList.push(entry) + } + + for (i = 0; i < flatItemsList.length; ++i) { + var item = flatItemsList[i] + if (item !== null) + items.insert(item) + } + } + } +} diff --git a/tests/auto/quick/qquicklistview2/data/delegateWithMouseArea.qml b/tests/auto/quick/qquicklistview2/data/delegateWithMouseArea.qml new file mode 100644 index 0000000000..f447f913e6 --- /dev/null +++ b/tests/auto/quick/qquicklistview2/data/delegateWithMouseArea.qml @@ -0,0 +1,73 @@ +// Copyright (C) 2021 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only + +import QtQuick + +Rectangle { + + width: 240 + height: 320 + color: "#ffffff" + + Component { + id: myDelegate + Rectangle { + id: wrapper + width: list.orientation == ListView.Vertical ? 240 : 20 + height: list.orientation == ListView.Vertical ? 20 : 240 + border.width: 1 + border.color: "black" + MouseArea { + anchors.fill: parent + } + Text { + text: index + ":" + (list.orientation == ListView.Vertical ? parent.y : parent.x).toFixed(0) + } + color: ListView.isCurrentItem ? "lightsteelblue" : "white" + } + } + + ListView { + id: list + objectName: "list" + focus: true + width: 240 + height: 200 + clip: true + model: 30 + headerPositioning: ListView.OverlayHeader + delegate: myDelegate + + header: Rectangle { + width: list.orientation == Qt.Vertical ? 240 : 30 + height: list.orientation == Qt.Vertical ? 30 : 240 + color: "green" + z: 11 + Text { + anchors.centerIn: parent + text: "header " + (list.orientation == ListView.Vertical ? parent.y : parent.x).toFixed(1) + } + } + } + + // debug + Rectangle { + color: "#40ff0000" + border.width: txt.x + border.color: "black" + radius: 5 + width: txt.implicitWidth + 50 + height: txt.implicitHeight + 2 * txt.x + anchors.bottom: parent.bottom + anchors.right: parent.right + anchors.left: parent.left + + Text { + id: txt + x: 3 + y: x + text: "header position: " + (list.orientation == ListView.Vertical ? list.headerItem.y : list.headerItem.x).toFixed(1) + + "\ncontent position: " + (list.orientation == ListView.Vertical ? list.contentY : list.contentX).toFixed(1) + } + } +} diff --git a/tests/auto/quick/qquicklistview2/data/fetchMore.qml b/tests/auto/quick/qquicklistview2/data/fetchMore.qml new file mode 100644 index 0000000000..4ce53e4d28 --- /dev/null +++ b/tests/auto/quick/qquicklistview2/data/fetchMore.qml @@ -0,0 +1,21 @@ +import QtQuick +import org.qtproject.Test + +ListView { + id: listView + width: 300 + height: 150 + flickDeceleration: 10000 + + model: FetchMoreModel + delegate: Text { + height: 50 + text: model.display + } + + Text { + anchors.right: parent.right + text: "count " + listView.count + color: listView.moving ? "red" : "blue" + } +} diff --git a/tests/auto/quick/qquicklistview2/data/footerUpdate.qml b/tests/auto/quick/qquicklistview2/data/footerUpdate.qml new file mode 100644 index 0000000000..c5729ad633 --- /dev/null +++ b/tests/auto/quick/qquicklistview2/data/footerUpdate.qml @@ -0,0 +1,39 @@ +// Copyright (C) 2021 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only + +import QtQuick + +Rectangle { + id: root + width: 800 + height: 800 + Component.onCompleted: { list.model.remove(0); } + ListView { + id: list + objectName: "list" + anchors.fill: parent + model: ListModel { + ListElement { + txt: "Foo" + } + } + delegate: Rectangle { + id: myDelegate + color: "red" + width: 800 + height: 100 + ListView.onRemove: SequentialAnimation { + PropertyAction { target: myDelegate; property: "ListView.delayRemove"; value: true } + NumberAnimation { target: myDelegate; property: "scale"; to: 0; duration: 1; } + PropertyAction { target: myDelegate; property: "ListView.delayRemove"; value: false } + } + + } + footer: Rectangle { + id: listFooter + color: "blue" + width: 800 + height: 100 + } + } +} diff --git a/tests/auto/quick/qquicklistview2/data/highlightWithBound.qml b/tests/auto/quick/qquicklistview2/data/highlightWithBound.qml new file mode 100644 index 0000000000..6cedd3e7d3 --- /dev/null +++ b/tests/auto/quick/qquicklistview2/data/highlightWithBound.qml @@ -0,0 +1,10 @@ +pragma ComponentBehavior: Bound +import QtQuick + +ListView { + model: 3 + delegate: Item {} + highlight: Item { + objectName: "highlight" + } +} diff --git a/tests/auto/quick/qquicklistview2/data/innerRequired.qml b/tests/auto/quick/qquicklistview2/data/innerRequired.qml new file mode 100644 index 0000000000..c0862cec0d --- /dev/null +++ b/tests/auto/quick/qquicklistview2/data/innerRequired.qml @@ -0,0 +1,35 @@ +import QtQuick + +Item { + ListModel { + id: myModel + ListElement { type: "Dog"; age: 8; noise: "meow" } + ListElement { type: "Cat"; age: 5; noise: "woof" } + } + + component SomeDelegate: Item { + required property int age + property string text + } + + component AnotherDelegate: Item { + property int age + property string text + + SomeDelegate { + age: 0 + text: "" + } + } + + ListView { + id: listView + model: myModel + width: 100 + height: 100 + delegate: AnotherDelegate { + age: model.age + text: model.noise + } + } +} diff --git a/tests/auto/quick/qquicklistview2/data/maxXExtent.qml b/tests/auto/quick/qquicklistview2/data/maxXExtent.qml new file mode 100644 index 0000000000..d72f825654 --- /dev/null +++ b/tests/auto/quick/qquicklistview2/data/maxXExtent.qml @@ -0,0 +1,29 @@ +// Copyright (C) 2022 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only + +import QtQuick + +Item { + property alias view: view + + ListView { + id: view + model: 10 + width: 200 + height: 200 + + Rectangle { + anchors.fill: parent + color: "transparent" + border.color: "darkorange" + } + + delegate: Rectangle { + width: 100 + height: 100 + Text { + text: modelData + } + } + } +} diff --git a/tests/auto/quick/qquicklistview2/data/maxYExtent.qml b/tests/auto/quick/qquicklistview2/data/maxYExtent.qml new file mode 100644 index 0000000000..b8a1f0e12b --- /dev/null +++ b/tests/auto/quick/qquicklistview2/data/maxYExtent.qml @@ -0,0 +1,30 @@ +// Copyright (C) 2022 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only + +import QtQuick + +Item { + property alias view: view + + ListView { + id: view + model: 10 + width: 200 + height: 200 + orientation: ListView.Horizontal + + Rectangle { + anchors.fill: parent + color: "transparent" + border.color: "darkorange" + } + + delegate: Rectangle { + width: 100 + height: 100 + Text { + text: modelData + } + } + } +} diff --git a/tests/auto/quick/qquicklistview2/data/metaSequenceAsModel.qml b/tests/auto/quick/qquicklistview2/data/metaSequenceAsModel.qml new file mode 100644 index 0000000000..461450239f --- /dev/null +++ b/tests/auto/quick/qquicklistview2/data/metaSequenceAsModel.qml @@ -0,0 +1,14 @@ +import QtQuick + +ListView { + id: view + width: 100 + height: 100 + property list<rect> rects: [ Qt.rect(1, 2, 3, 4), Qt.rect(5, 6, 7, 8) ] + property list<string> texts + + model: rects + delegate: Item { + Component.onCompleted: view.texts.push(modelData.x + "/" + modelData.y) + } +} diff --git a/tests/auto/quick/qquicklistview2/data/mouseAreaDelegate.qml b/tests/auto/quick/qquicklistview2/data/mouseAreaDelegate.qml new file mode 100644 index 0000000000..ad556913a5 --- /dev/null +++ b/tests/auto/quick/qquicklistview2/data/mouseAreaDelegate.qml @@ -0,0 +1,30 @@ +import QtQuick 2.15 + +ListView { + id: root + width: 320 + height: 480 + model: 100 + + property var pressedDelegates: [] + property var releasedDelegates: [] + property var tappedDelegates: [] + property var canceledDelegates: [] + + delegate: MouseArea { + height: 100 + width: 320 + + onPressed: root.pressedDelegates.push(index) + onReleased: root.releasedDelegates.push(index) + onClicked: root.tappedDelegates.push(index) + onCanceled: root.canceledDelegates.push(index) + + Rectangle { + id: buttonArea + anchors.fill: parent + border.color: "#41cd52" + color: parent.pressed ? "lightsteelblue" : "beige" + } + } +} diff --git a/tests/auto/quick/qquicklistview2/data/noCrashOnIndexChange.qml b/tests/auto/quick/qquicklistview2/data/noCrashOnIndexChange.qml new file mode 100644 index 0000000000..6065d09981 --- /dev/null +++ b/tests/auto/quick/qquicklistview2/data/noCrashOnIndexChange.qml @@ -0,0 +1,48 @@ +import QtQuick +import QtQml.Models + +Item { + ListModel { + id: myModel + ListElement { role_display: "One"; role_value: 0; } + ListElement { role_display: "One"; role_value: 2; } + ListElement { role_display: "One"; role_value: 3; } + ListElement { role_display: "One"; role_value: 4; } + ListElement { role_details: "Two"; role_value: 5; } + ListElement { role_details: "Three"; role_value: 6; } + ListElement { role_details: "Four"; role_value: 7; } + ListElement { role_details: "Five"; role_value: 8; } + ListElement { role_details: "Six"; role_value: 9; } + ListElement { role_keyID: "Seven"; role_value: 10; } + ListElement { role_keyID: "Eight"; role_value: 11; } + ListElement { role_keyID: "hello"; role_value: 12; } + } + + DelegateModel { + id: displayDelegateModel + delegate: Text { text: role_display } + model: myModel + groups: [ + DelegateModelGroup { + includeByDefault: false + name: "displayField" + } + ] + filterOnGroup: "displayField" + Component.onCompleted: { + var rowCount = myModel.count; + items.remove(0, rowCount); + for (var i = 0; i < rowCount; i++) { + var entry = myModel.get(i); + if (entry.role_display) { + items.insert(entry, "displayField"); + } + } + } + } + + ListView { + model: displayDelegateModel + } +} + diff --git a/tests/auto/quick/qquicklistview2/data/qtbug104679_footer.qml b/tests/auto/quick/qquicklistview2/data/qtbug104679_footer.qml new file mode 100644 index 0000000000..919cf4d2ec --- /dev/null +++ b/tests/auto/quick/qquicklistview2/data/qtbug104679_footer.qml @@ -0,0 +1,21 @@ +import QtQuick + +Rectangle { + width: 640 + height: 480 + + ListView { + anchors.fill: parent + spacing: 5 + + footerPositioning: ListView.PullBackFooter + footer: Rectangle { width: ListView.view.width; color: "blue"; implicitHeight: 46 } + + model: 3 // crashed if less items than a full list page + delegate: Rectangle { + width: ListView.view.width + height: 50 + color: index % 2 ? "black" : "gray" + } + } +} diff --git a/tests/auto/quick/qquicklistview2/data/qtbug104679_header.qml b/tests/auto/quick/qquicklistview2/data/qtbug104679_header.qml new file mode 100644 index 0000000000..40ddf27988 --- /dev/null +++ b/tests/auto/quick/qquicklistview2/data/qtbug104679_header.qml @@ -0,0 +1,21 @@ +import QtQuick + +Rectangle { + width: 640 + height: 480 + + ListView { + anchors.fill: parent + spacing: 5 + + headerPositioning: ListView.PullBackHeader + header: Rectangle { width: ListView.view.width; color: "red"; implicitHeight: 46 } + + model: 3 // crashed if less items than a full list page + delegate: Rectangle { + width: ListView.view.width + height: 50 + color: index % 2 ? "black" : "gray" + } + } +} diff --git a/tests/auto/quick/qquicklistview2/data/qtbug86744.qml b/tests/auto/quick/qquicklistview2/data/qtbug86744.qml new file mode 100644 index 0000000000..d8b89a147d --- /dev/null +++ b/tests/auto/quick/qquicklistview2/data/qtbug86744.qml @@ -0,0 +1,25 @@ +// Copyright (C) 2022 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only + +import QtQuick +import QtQml.Models + +Item { + height: 200 + width: 100 + DelegateModel { + id: dm + model: 2 + delegate: Item { + width: 100 + height: 20 + property bool isCurrent: ListView.isCurrentItem + } + } + ListView { + objectName: "listView" + model: dm + currentIndex: 1 + anchors.fill: parent + } +} diff --git a/tests/auto/quick/qquicklistview2/data/qtbug98315.qml b/tests/auto/quick/qquicklistview2/data/qtbug98315.qml new file mode 100644 index 0000000000..4035915c6d --- /dev/null +++ b/tests/auto/quick/qquicklistview2/data/qtbug98315.qml @@ -0,0 +1,98 @@ +// Copyright (C) 2022 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only + +import QtQuick +import QtQml.Models + +Item { + width: 500 + height: 200 + + property list<QtObject> myModel: [ + QtObject { + objectName: "Item 0" + property bool selected: true + }, + QtObject { + objectName: "Item 1" + property bool selected: false + }, + QtObject { + objectName: "Item 2" + property bool selected: false + }, + QtObject { + objectName: "Item 3" + property bool selected: true + }, + QtObject { + objectName: "Item 4" + property bool selected: true + }, + QtObject { + objectName: "Item 5" + property bool selected: true + }, + QtObject { + objectName: "Item 6" + property bool selected: false + } + ] + + ListView { + objectName: "listView" + id: listview + width: 500 + height: 200 + + focus: true + clip: true + spacing: 2 + orientation: ListView.Horizontal + highlightMoveDuration: 300 + highlightMoveVelocity: -1 + preferredHighlightBegin: (500 - 100) / 2 + preferredHighlightEnd: (500 + 100) / 2 + highlightRangeMode: ListView.StrictlyEnforceRange + cacheBuffer: 500 + currentIndex: 1 + + model: DelegateModel { + id: delegateModel + filterOnGroup: "visible" + model: myModel + groups: [ + DelegateModelGroup { + name: "visible" + includeByDefault: true + } + ] + delegate: Rectangle { + id: tile + objectName: model.modelData.objectName + + width: 100 + height: 100 + border.width: 0 + anchors.verticalCenter: parent.verticalCenter + + visible: model.modelData.selected + Component.onCompleted: { + DelegateModel.inPersistedItems = true + DelegateModel.inVisible = Qt.binding(function () { + return model.modelData.selected + }) + } + + property bool isCurrent: ListView.isCurrentItem + color: isCurrent ? "red" : "green" + + Text { + id: valueText + anchors.centerIn: parent + text: model.modelData.objectName + } + } + } + } +} diff --git a/tests/auto/quick/qquicklistview2/data/qtbug_92809.qml b/tests/auto/quick/qquicklistview2/data/qtbug_92809.qml new file mode 100644 index 0000000000..7507e83f73 --- /dev/null +++ b/tests/auto/quick/qquicklistview2/data/qtbug_92809.qml @@ -0,0 +1,71 @@ +// Copyright (C) 2021 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only + +import QtQuick + +Rectangle { + id: root + width: 800 + height: 480 + + property list<QtObject> myModel: [ + QtObject { property string name: "Item 0"; property bool selected: true }, + QtObject { property string name: "Item 1"; property bool selected: true }, + QtObject { property string name: "Item 2"; property bool selected: true }, + QtObject { property string name: "Item 3"; property bool selected: true }, + QtObject { property string name: "Item 4"; property bool selected: true }, + QtObject { property string name: "Item 5"; property bool selected: true }, + QtObject { property string name: "Item 6"; property bool selected: true }, + QtObject { property string name: "Item 7"; property bool selected: true }, + QtObject { property string name: "Item 8"; property bool selected: true }, + QtObject { property string name: "Item 9"; property bool selected: true }, + QtObject { property string name: "Press Enter here"; property bool selected: true } + ] + + DelegateModel { + objectName: "model" + id: visualModel + model: myModel + filterOnGroup: "selected" + + groups: [ + DelegateModelGroup { + name: "selected" + includeByDefault: true + } + ] + + delegate: Rectangle { + width: 180 + height: 180 + visible: DelegateModel.inSelected + color: ListView.isCurrentItem ? "orange" : "yellow" + Component.onCompleted: { + DelegateModel.inPersistedItems = true + DelegateModel.inSelected = Qt.binding(function() { return model.selected }) + } + } + } + + ListView { + objectName: "list" + anchors.fill: parent + spacing: 180/15 + orientation: ListView.Horizontal + model: visualModel + focus: true + currentIndex: 0 + preferredHighlightBegin: (width-180)/2 + preferredHighlightEnd: (width+180)/2 + highlightRangeMode: ListView.StrictlyEnforceRange + highlightMoveDuration: 300 + highlightMoveVelocity: -1 + cacheBuffer: 0 + + onCurrentIndexChanged: { + if (currentIndex === 10) { + myModel[6].selected = !myModel[6].selected + } + } + } +} diff --git a/tests/auto/quick/qquicklistview2/data/sectionBoundComponent.qml b/tests/auto/quick/qquicklistview2/data/sectionBoundComponent.qml new file mode 100644 index 0000000000..74ab6b59fa --- /dev/null +++ b/tests/auto/quick/qquicklistview2/data/sectionBoundComponent.qml @@ -0,0 +1,14 @@ +pragma ComponentBehavior: Bound +import QtQuick +ListView { + id: view + width: 100 + height: 100 + model: ListModel { + ListElement { name: "foo"; age: 42 } + ListElement { name: "bar"; age: 13 } + } + delegate: Text { required property string name; text: name} + section.property: "age" + section.delegate: Rectangle { color: "gray"; width: view.width; height: 20; required property string section; Text {text: parent.section} } +} diff --git a/tests/auto/quick/qquicklistview2/data/sectionGeometryChange.qml b/tests/auto/quick/qquicklistview2/data/sectionGeometryChange.qml new file mode 100644 index 0000000000..6981af51ec --- /dev/null +++ b/tests/auto/quick/qquicklistview2/data/sectionGeometryChange.qml @@ -0,0 +1,58 @@ +import QtQuick +import QtQuick.Controls + +Rectangle { + width: 640 + height: 480 + color: "#FFFFFF" + ListView { + objectName: "list" + anchors.fill: parent + + delegate: Rectangle { + objectName: value + implicitHeight: text.implicitHeight + color: "#ff3" + + Text { + id: text + width: parent.width + padding: 5 + font.pixelSize: 20 + text: value + } + } + + section { + property: "section" + + delegate: Rectangle { + objectName: section + width: parent.width + implicitHeight: text.implicitHeight + color: "#3ff" + + Text { + id: text + width: parent.width + padding: 5 + font.pixelSize: 20 + text: section + wrapMode: Text.Wrap + } + } + } + + model: ListModel { + ListElement { value: "Element1"; section: "Section1" } + ListElement { value: "Element2"; section: "Section1" } + ListElement { value: "Element3"; section: "Section1" } + ListElement { value: "Element4"; section: "Section2" } + ListElement { value: "Element5"; section: "Section2" } + ListElement { value: "Element6"; section: "Section2" } + ListElement { value: "Element7"; section: "Section2" } + ListElement { value: "Element8"; section: "Section3" } + ListElement { value: "Element9"; section: "Section3" } + } + } +} diff --git a/tests/auto/quick/qquicklistview2/data/sectionsNoOverlap.qml b/tests/auto/quick/qquicklistview2/data/sectionsNoOverlap.qml new file mode 100644 index 0000000000..3a22626032 --- /dev/null +++ b/tests/auto/quick/qquicklistview2/data/sectionsNoOverlap.qml @@ -0,0 +1,77 @@ +// Copyright (C) 2022 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only + +import QtQuick +import QtQuick.Controls + +Rectangle { + property string sectionProperty: "section" + property int sectionPositioning: ViewSection.InlineLabels + + width: 640 + height: 480 + color: "#FFFFFF" + + resources: [ + Component { + id: myDelegate + Text { + objectName: model.title + width: parent.width + height: 40 + text: "NormalDelegate: " + model.title + visible: model.isVisible + verticalAlignment: Text.AlignVCenter + } + } + ] + ListView { + id: list + objectName: "list" + anchors.fill: parent + clip: true + + model: ListModel { + ListElement { + title: "element1" + isVisible: true + section: "section1" + } + ListElement { + title: "element2" + isVisible: true + section: "section1" + } + ListElement { + title: "element3" + isVisible: true + section: "section2" + } + ListElement { + title: "element4" + isVisible: true + section: "section2" + } + } + + delegate: myDelegate + + section.property: "section" + section.criteria: ViewSection.FullString + section.delegate: Component { + Text { + id: sectionDelegate + objectName: section + visible: false + width: parent.width + height: visible ? 48 : 0 + text: "Section delegate: " + section + verticalAlignment: Text.AlignVCenter + elide: Text.ElideMiddle + Component.onCompleted: function(){ + Qt.callLater(function(){sectionDelegate.visible = true}) + } + } + } + } +} diff --git a/tests/auto/quick/qquicklistview2/data/singletonModelLifetime.qml b/tests/auto/quick/qquicklistview2/data/singletonModelLifetime.qml new file mode 100644 index 0000000000..f230786723 --- /dev/null +++ b/tests/auto/quick/qquicklistview2/data/singletonModelLifetime.qml @@ -0,0 +1,32 @@ +import QtQuick 2.15 +import QtQuick.Window 2.15 +import test 1.0 + +Window { + id: root + visible: true + width: 800 + height: 680 + property bool alive: false + + Component { + id: view + ListView { + model: SingletonModel + } + } + function compare(a,b) { + root.alive = (a === b) + } + + function test_singletonModelCrash() { + SingletonModel.objectName = "model" + var o = view.createObject(root) + o.destroy() + Qt.callLater(function() { + compare(SingletonModel.objectName, "model") + }) + } + + Component.onCompleted: root.test_singletonModelCrash() +} diff --git a/tests/auto/quick/qquicklistview2/data/snapOneItem.qml b/tests/auto/quick/qquicklistview2/data/snapOneItem.qml new file mode 100644 index 0000000000..a27f220865 --- /dev/null +++ b/tests/auto/quick/qquicklistview2/data/snapOneItem.qml @@ -0,0 +1,34 @@ +import QtQuick + +ListView { + id: list + snapMode: ListView.SnapOneItem + model: 4 + width: 200 + height: 200 + highlightRangeMode: ListView.StrictlyEnforceRange + highlight: Rectangle { width: 200; height: 200; color: "yellow" } + delegate: Rectangle { + id: wrapper + width: list.width + height: list.height + Column { + Text { + text: index + } + Text { + text: wrapper.x + ", " + wrapper.y + } + } + color: ListView.isCurrentItem ? "lightsteelblue" : "transparent" + } + // speed up test runs + flickDeceleration: 5000 + rebound: Transition { + NumberAnimation { + properties: "x,y" + duration: 30 + easing.type: Easing.OutBounce + } + } +} diff --git a/tests/auto/quick/qquicklistview2/data/urlListModel.qml b/tests/auto/quick/qquicklistview2/data/urlListModel.qml new file mode 100644 index 0000000000..38237234e0 --- /dev/null +++ b/tests/auto/quick/qquicklistview2/data/urlListModel.qml @@ -0,0 +1,22 @@ +// Copyright (C) 2021 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only + +import QtQuick + +Rectangle { + id: root + + property var model + property alias view: view + + ListView { + id: view + anchors.fill: parent + objectName: "view" + model: root.model + delegate: Text { + height: view.width + text: modelData + } + } +} diff --git a/tests/auto/quick/qquicklistview2/data/viewportAvoidUndesiredMovementOnSetCurrentIndex.qml b/tests/auto/quick/qquicklistview2/data/viewportAvoidUndesiredMovementOnSetCurrentIndex.qml new file mode 100644 index 0000000000..cd3865d55b --- /dev/null +++ b/tests/auto/quick/qquicklistview2/data/viewportAvoidUndesiredMovementOnSetCurrentIndex.qml @@ -0,0 +1,47 @@ +import QtQuick + +Item { + id: root + width: 400 + height: 600 + + ListView { + id: rawList + objectName: "list" + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + height: 300 + + // full disabling of automatic viewport positioning + highlightFollowsCurrentItem: false + snapMode: ListView.NoSnap + highlightRangeMode: ListView.NoHighlightRange + + delegate: Rectangle { + color: model.index === rawList.currentIndex ? "red" : "white" + border.color: rawList.currentItem === this ? "red" : "black" + height: 100 + width: 400 + + Text { + anchors.centerIn: parent + text: model.index + font.pixelSize: 50 + } + + MouseArea { + // only for using this file to do manual testing + // autotest calls setCurrentIndex + anchors.fill: parent + + onClicked: { + rawList.currentIndex = model.index; + } + } + } + + model: 30 + } + +} diff --git a/tests/auto/quick/qquicklistview2/tst_qquicklistview2.cpp b/tests/auto/quick/qquicklistview2/tst_qquicklistview2.cpp new file mode 100644 index 0000000000..bdac2112b6 --- /dev/null +++ b/tests/auto/quick/qquicklistview2/tst_qquicklistview2.cpp @@ -0,0 +1,1255 @@ +// Copyright (C) 2021 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only + +#include <QtTest/QtTest> +#include <QtQuick/qquickview.h> +#include <QtQuick/private/qquickitemview_p_p.h> +#include <QtQuick/private/qquicklistview_p.h> +#include <QtQuickTest/QtQuickTest> +#include <QStringListModel> +#include <QQmlApplicationEngine> + +#include <QtQuickTestUtils/private/viewtestutils_p.h> +#include <QtQuickTestUtils/private/visualtestutils_p.h> +#include <QtQuickTestUtils/private/qmlutils_p.h> + +Q_LOGGING_CATEGORY(lcTests, "qt.quick.tests") + +using namespace QQuickViewTestUtils; +using namespace QQuickVisualTestUtils; + +class tst_QQuickListView2 : public QQmlDataTest +{ + Q_OBJECT +public: + tst_QQuickListView2(); + +private slots: + void urlListModel(); + void dragDelegateWithMouseArea_data(); + void dragDelegateWithMouseArea(); + void delegateChooserEnumRole(); + void QTBUG_92809(); + void footerUpdate(); + void singletonModelLifetime(); + void delegateModelRefresh(); + void wheelSnap(); + void wheelSnap_data(); + + void sectionsNoOverlap(); + void metaSequenceAsModel(); + void noCrashOnIndexChange(); + void innerRequired(); + void boundDelegateComponent(); + void tapDelegateDuringFlicking_data(); + void tapDelegateDuringFlicking(); + void flickDuringFlicking_data(); + void flickDuringFlicking(); + void maxExtent_data(); + void maxExtent(); + void isCurrentItem_DelegateModel(); + void isCurrentItem_NoRegressionWithDelegateModelGroups(); + + void pullbackSparseList(); + void highlightWithBound(); + void sectionIsCompatibleWithBoundComponents(); + void sectionGeometryChange(); + void areaZeroviewDoesNotNeedlesslyPopulateWholeModel(); + void viewportAvoidUndesiredMovementOnSetCurrentIndex(); + + void delegateContextHandling(); + void fetchMore_data(); + void fetchMore(); + + void changingOrientationResetsPreviousAxisValues_data(); + void changingOrientationResetsPreviousAxisValues(); + void bindingDirectlyOnPositionInHeaderAndFooterDelegates_data(); + void bindingDirectlyOnPositionInHeaderAndFooterDelegates(); + + void clearObjectListModel(); + +private: + void flickWithTouch(QQuickWindow *window, const QPoint &from, const QPoint &to); + QScopedPointer<QPointingDevice> touchDevice = QScopedPointer<QPointingDevice>(QTest::createTouchDevice()); +}; + +tst_QQuickListView2::tst_QQuickListView2() + : QQmlDataTest(QT_QMLTEST_DATADIR) +{ +} + +void tst_QQuickListView2::urlListModel() +{ + QScopedPointer<QQuickView> window(createView()); + QVERIFY(window); + + QList<QUrl> model = { QUrl::fromLocalFile("abc"), QUrl::fromLocalFile("123") }; + window->setInitialProperties({{ "model", QVariant::fromValue(model) }}); + + window->setSource(testFileUrl("urlListModel.qml")); + window->show(); + QVERIFY(QTest::qWaitForWindowExposed(window.data())); + + QQuickListView *view = window->rootObject()->property("view").value<QQuickListView*>(); + QVERIFY(view); + if (QQuickTest::qIsPolishScheduled(view)) + QVERIFY(QQuickTest::qWaitForPolish(view)); + QCOMPARE(view->count(), model.size()); +} + +static void dragListView(QWindow *window, QPoint *startPos, const QPoint &delta) +{ + auto drag_helper = [&](QWindow *window, QPoint *startPos, const QPoint &d) { + QPoint pos = *startPos; + const int dragDistance = d.manhattanLength(); + const QPoint unitVector(qBound(-1, d.x(), 1), qBound(-1, d.y(), 1)); + for (int i = 0; i < dragDistance; ++i) { + QTest::mouseMove(window, pos); + pos += unitVector; + } + // Move to the final position + pos = *startPos + d; + QTest::mouseMove(window, pos); + *startPos = pos; + }; + + if (delta.manhattanLength() == 0) + return; + const int dragThreshold = QGuiApplication::styleHints()->startDragDistance(); + const QPoint unitVector(qBound(-1, delta.x(), 1), qBound(-1, delta.y(), 1)); + // go just beyond the drag theshold + drag_helper(window, startPos, unitVector * (dragThreshold + 1)); + drag_helper(window, startPos, unitVector); + + // next drag will actually scroll the listview + drag_helper(window, startPos, delta); +} + +void tst_QQuickListView2::dragDelegateWithMouseArea_data() +{ + QTest::addColumn<QQuickItemView::LayoutDirection>("layoutDirection"); + + for (int layDir = QQuickItemView::LeftToRight; layDir <= (int)QQuickItemView::VerticalBottomToTop; layDir++) { + const char *enumValueName = QMetaEnum::fromType<QQuickItemView::LayoutDirection>().valueToKey(layDir); + QTest::newRow(enumValueName) << static_cast<QQuickItemView::LayoutDirection>(layDir); + } +} + +void tst_QQuickListView2::viewportAvoidUndesiredMovementOnSetCurrentIndex() +{ + QScopedPointer<QQuickView> window(createView()); + QVERIFY(window); + window->setFlag(Qt::FramelessWindowHint); + window->setSource(testFileUrl("viewportAvoidUndesiredMovementOnSetCurrentIndex.qml")); + window->show(); + QVERIFY(QTest::qWaitForWindowExposed(window.data())); + QVERIFY(window->rootObject()); + QQuickListView *listview = findItem<QQuickListView>(window->rootObject(), "list"); + QVERIFY(listview); + listview->setCurrentIndex(2); // change current item + // partially obscure first item + QCOMPARE(listview->contentY(), 0); + listview->setContentY(50); + QTRY_COMPARE(listview->contentY(), 50); + listview->setCurrentIndex(0); // change current item back to first one + QVERIFY(QQuickTest::qWaitForPolish(listview)); + // that shouldn't have caused any movement + QCOMPARE(listview->contentY(), 50); + + // that even applies to the case where the current item is completely out of the viewport + listview->setCurrentIndex(25); + QVERIFY(QQuickTest::qWaitForPolish(listview)); + QCOMPARE(listview->contentY(), 50); +} + +void tst_QQuickListView2::dragDelegateWithMouseArea() +{ + QFETCH(QQuickItemView::LayoutDirection, layoutDirection); + + QScopedPointer<QQuickView> window(createView()); + QVERIFY(window); + window->setFlag(Qt::FramelessWindowHint); + window->setSource(testFileUrl("delegateWithMouseArea.qml")); + window->show(); + QVERIFY(QTest::qWaitForWindowExposed(window.data())); + + QQuickListView *listview = findItem<QQuickListView>(window->rootObject(), "list"); + QVERIFY(listview != nullptr); + + const bool horizontal = layoutDirection < QQuickItemView::VerticalTopToBottom; + listview->setOrientation(horizontal ? QQuickListView::Horizontal : QQuickListView::Vertical); + + if (horizontal) + listview->setLayoutDirection(static_cast<Qt::LayoutDirection>(layoutDirection)); + else + listview->setVerticalLayoutDirection(static_cast<QQuickItemView::VerticalLayoutDirection>(layoutDirection)); + + QVERIFY(QQuickTest::qWaitForPolish(listview)); + + auto contentPosition = [&](QQuickListView *listview) { + return (listview->orientation() == QQuickListView::Horizontal ? listview->contentX(): listview->contentY()); + }; + + qreal expectedContentPosition = contentPosition(listview); + QPoint startPos = (QPointF(listview->width(), listview->height())/2).toPoint(); + QTest::mousePress(window.data(), Qt::LeftButton, Qt::NoModifier, startPos, 200); + + QPoint dragDelta(0, -10); + + if (layoutDirection == QQuickItemView::RightToLeft || layoutDirection == QQuickItemView::VerticalBottomToTop) + dragDelta = -dragDelta; + expectedContentPosition -= dragDelta.y(); + if (horizontal) + dragDelta = dragDelta.transposed(); + + dragListView(window.data(), &startPos, dragDelta); + + QTest::mouseRelease(window.data(), Qt::LeftButton, Qt::NoModifier, startPos, 200); // Wait 200 ms before we release to avoid trigger a flick + + // wait for the "fixup" animation to finish + QVERIFY(QTest::qWaitFor([&]() + { return !listview->isMoving();} + )); + + QCOMPARE(contentPosition(listview), expectedContentPosition); +} + + +void tst_QQuickListView2::delegateChooserEnumRole() +{ + QQuickView window; + QVERIFY(QQuickTest::showView(window, testFileUrl("delegateChooserEnumRole.qml"))); + QQuickListView *listview = qobject_cast<QQuickListView*>(window.rootObject()); + QVERIFY(listview); + QTRY_COMPARE(listview->count(), 3); + QCOMPARE(listview->itemAtIndex(0)->property("delegateType").toInt(), 0); + QCOMPARE(listview->itemAtIndex(1)->property("delegateType").toInt(), 1); + QCOMPARE(listview->itemAtIndex(2)->property("delegateType").toInt(), 2); +} + +void tst_QQuickListView2::QTBUG_92809() +{ + QScopedPointer<QQuickView> window(createView()); + QTRY_VERIFY(window); + window->setSource(testFileUrl("qtbug_92809.qml")); + window->show(); + QVERIFY(QTest::qWaitForWindowExposed(window.data())); + + QQuickListView *listview = findItem<QQuickListView>(window->rootObject(), "list"); + QTRY_VERIFY(listview != nullptr); + QVERIFY(QQuickTest::qWaitForPolish(listview)); + listview->setCurrentIndex(1); + QVERIFY(QQuickTest::qWaitForPolish(listview)); + listview->setCurrentIndex(2); + QVERIFY(QQuickTest::qWaitForPolish(listview)); + listview->setCurrentIndex(3); + QVERIFY(QQuickTest::qWaitForPolish(listview)); + QTest::qWait(500); + listview->setCurrentIndex(10); + QVERIFY(QQuickTest::qWaitForPolish(listview)); + QTest::qWait(500); + int currentIndex = listview->currentIndex(); + QTRY_COMPARE(currentIndex, 9); +} + +void tst_QQuickListView2::footerUpdate() +{ + QScopedPointer<QQuickView> window(createView()); + QTRY_VERIFY(window); + window->setSource(testFileUrl("footerUpdate.qml")); + window->show(); + QVERIFY(QTest::qWaitForWindowExposed(window.data())); + + QQuickListView *listview = findItem<QQuickListView>(window->rootObject(), "list"); + QTRY_VERIFY(listview != nullptr); + QVERIFY(QQuickTest::qWaitForPolish(listview)); + QQuickItem *footer = listview->footerItem(); + QTRY_VERIFY(footer); + QVERIFY(QQuickTest::qWaitForPolish(footer)); + QTRY_COMPARE(footer->y(), 0); +} + +void tst_QQuickListView2::sectionsNoOverlap() +{ + QScopedPointer<QQuickView> window(createView()); + QTRY_VERIFY(window); + window->setSource(testFileUrl("sectionsNoOverlap.qml")); + window->show(); + QVERIFY(QTest::qWaitForWindowExposed(window.data())); + + QQuickListView *listview = findItem<QQuickListView>(window->rootObject(), "list"); + QTRY_VERIFY(listview != nullptr); + + QQuickItem *contentItem = listview->contentItem(); + QTRY_VERIFY(contentItem != nullptr); + QVERIFY(QQuickTest::qWaitForPolish(listview)); + + const unsigned int sectionCount = 2, normalDelegateCount = 2; + const unsigned int expectedSectionHeight = 48; + const unsigned int expectedNormalDelegateHeight = 40; + + unsigned int normalDelegateCounter = 0; + for (unsigned int sectionIndex = 0; sectionIndex < sectionCount; ++sectionIndex) { + QQuickItem *sectionDelegate = + findItem<QQuickItem>(contentItem, "section" + QString::number(sectionIndex + 1)); + QVERIFY(sectionDelegate); + + QCOMPARE(sectionDelegate->height(), expectedSectionHeight); + QVERIFY(sectionDelegate->isVisible()); + QCOMPARE(sectionDelegate->y(), + qreal(sectionIndex * expectedSectionHeight + + (sectionIndex * normalDelegateCount * expectedNormalDelegateHeight))); + + for (; normalDelegateCounter < ((sectionIndex + 1) * normalDelegateCount); + ++normalDelegateCounter) { + QQuickItem *normalDelegate = findItem<QQuickItem>( + contentItem, "element" + QString::number(normalDelegateCounter + 1)); + QVERIFY(normalDelegate); + + QCOMPARE(normalDelegate->height(), expectedNormalDelegateHeight); + QVERIFY(normalDelegate->isVisible()); + QCOMPARE(normalDelegate->y(), + qreal((sectionIndex + 1) * expectedSectionHeight + + normalDelegateCounter * expectedNormalDelegateHeight + + listview->spacing() * normalDelegateCounter)); + } + } +} + +void tst_QQuickListView2::metaSequenceAsModel() +{ + QQmlEngine engine; + QQmlComponent c(&engine, testFileUrl("metaSequenceAsModel.qml")); + QVERIFY2(c.isReady(), qPrintable(c.errorString())); + QScopedPointer<QObject> o(c.create()); + QVERIFY(!o.isNull()); + QStringList strings = qvariant_cast<QStringList>(o->property("texts")); + QCOMPARE(strings.size(), 2); + QCOMPARE(strings[0], QStringLiteral("1/2")); + QCOMPARE(strings[1], QStringLiteral("5/6")); +} + +void tst_QQuickListView2::noCrashOnIndexChange() +{ + QQmlEngine engine; + QQmlComponent c(&engine, testFileUrl("noCrashOnIndexChange.qml")); + QVERIFY2(c.isReady(), qPrintable(c.errorString())); + QScopedPointer<QObject> o(c.create()); + QVERIFY(!o.isNull()); + + QObject *delegateModel = qmlContext(o.data())->objectForName("displayDelegateModel"); + QVERIFY(delegateModel); + + QObject *items = qvariant_cast<QObject *>(delegateModel->property("items")); + QCOMPARE(items->property("name").toString(), QStringLiteral("items")); + QCOMPARE(items->property("count").toInt(), 4); +} + +void tst_QQuickListView2::innerRequired() +{ + QQmlEngine engine; + const QUrl url(testFileUrl("innerRequired.qml")); + QQmlComponent component(&engine, url); + QVERIFY2(component.isReady(), qPrintable(component.errorString())); + + QScopedPointer<QObject> o(component.create()); + QVERIFY2(!o.isNull(), qPrintable(component.errorString())); + + QQuickListView *a = qobject_cast<QQuickListView *>( + qmlContext(o.data())->objectForName(QStringLiteral("listView"))); + QVERIFY(a); + + QCOMPARE(a->count(), 2); + QCOMPARE(a->itemAtIndex(0)->property("age").toInt(), 8); + QCOMPARE(a->itemAtIndex(0)->property("text").toString(), u"meow"); + QCOMPARE(a->itemAtIndex(1)->property("age").toInt(), 5); + QCOMPARE(a->itemAtIndex(1)->property("text").toString(), u"woof"); +} + +void tst_QQuickListView2::boundDelegateComponent() +{ + QQmlEngine engine; + const QUrl url(testFileUrl("boundDelegateComponent.qml")); + QQmlComponent c(&engine, url); + QVERIFY2(c.isReady(), qPrintable(c.errorString())); + + QTest::ignoreMessage( + QtWarningMsg, qPrintable(QLatin1String("%1:14: ReferenceError: index is not defined") + .arg(url.toString()))); + + QScopedPointer<QObject> o(c.create()); + QVERIFY(!o.isNull()); + + QQmlContext *context = qmlContext(o.data()); + + QObject *inner = context->objectForName(QLatin1String("listView")); + QVERIFY(inner != nullptr); + QQuickListView *listView = qobject_cast<QQuickListView *>(inner); + QVERIFY(listView != nullptr); + QObject *item = listView->itemAtIndex(0); + QVERIFY(item); + QCOMPARE(item->objectName(), QLatin1String("fooouterundefined")); + + QObject *inner2 = context->objectForName(QLatin1String("listView2")); + QVERIFY(inner2 != nullptr); + QQuickListView *listView2 = qobject_cast<QQuickListView *>(inner2); + QVERIFY(listView2 != nullptr); + QObject *item2 = listView2->itemAtIndex(0); + QVERIFY(item2); + QCOMPARE(item2->objectName(), QLatin1String("fooouter0")); + + QQmlComponent *comp = qobject_cast<QQmlComponent *>( + context->objectForName(QLatin1String("outerComponent"))); + QVERIFY(comp != nullptr); + + for (int i = 0; i < 3; ++i) { + QTest::ignoreMessage( + QtWarningMsg, + qPrintable(QLatin1String("%1:51:21: ReferenceError: model is not defined") + .arg(url.toString()))); + } + + QScopedPointer<QObject> outerItem(comp->create(context)); + QVERIFY(!outerItem.isNull()); + QQuickListView *innerListView = qobject_cast<QQuickListView *>( + qmlContext(outerItem.data())->objectForName(QLatin1String("innerListView"))); + QVERIFY(innerListView != nullptr); + QCOMPARE(innerListView->count(), 3); + for (int i = 0; i < 3; ++i) + QVERIFY(innerListView->itemAtIndex(i)->objectName().isEmpty()); +} + +void tst_QQuickListView2::tapDelegateDuringFlicking_data() +{ + QTest::addColumn<QByteArray>("qmlFile"); + QTest::addColumn<QQuickFlickable::BoundsBehavior>("boundsBehavior"); + QTest::addColumn<bool>("expectCanceled"); + + QTest::newRow("Button StopAtBounds") << QByteArray("buttonDelegate.qml") + << QQuickFlickable::BoundsBehavior(QQuickFlickable::StopAtBounds) << false; + QTest::newRow("MouseArea StopAtBounds") << QByteArray("mouseAreaDelegate.qml") + << QQuickFlickable::BoundsBehavior(QQuickFlickable::StopAtBounds) << true; + QTest::newRow("Button DragOverBounds") << QByteArray("buttonDelegate.qml") + << QQuickFlickable::BoundsBehavior(QQuickFlickable::DragOverBounds) << false; + QTest::newRow("MouseArea DragOverBounds") << QByteArray("mouseAreaDelegate.qml") + << QQuickFlickable::BoundsBehavior(QQuickFlickable::DragOverBounds) << true; + QTest::newRow("Button OvershootBounds") << QByteArray("buttonDelegate.qml") + << QQuickFlickable::BoundsBehavior(QQuickFlickable::OvershootBounds) << false; + QTest::newRow("MouseArea OvershootBounds") << QByteArray("mouseAreaDelegate.qml") + << QQuickFlickable::BoundsBehavior(QQuickFlickable::OvershootBounds) << true; + QTest::newRow("Button DragAndOvershootBounds") << QByteArray("buttonDelegate.qml") + << QQuickFlickable::BoundsBehavior(QQuickFlickable::DragAndOvershootBounds) << false; + QTest::newRow("MouseArea DragAndOvershootBounds") << QByteArray("mouseAreaDelegate.qml") + << QQuickFlickable::BoundsBehavior(QQuickFlickable::DragAndOvershootBounds) << true; +} + +void tst_QQuickListView2::tapDelegateDuringFlicking() // QTBUG-103832 +{ + QFETCH(QByteArray, qmlFile); + QFETCH(QQuickFlickable::BoundsBehavior, boundsBehavior); + QFETCH(bool, expectCanceled); + + QQuickView window; + QVERIFY(QQuickTest::showView(window, testFileUrl(qmlFile.constData()))); + QQuickListView *listView = qobject_cast<QQuickListView*>(window.rootObject()); + QVERIFY(listView); + listView->setBoundsBehavior(boundsBehavior); + + flickWithTouch(&window, {100, 400}, {100, 100}); + QTRY_VERIFY(listView->contentY() > 501); // let it flick some distance + QVERIFY(listView->isFlicking()); // we want to test the case when it's still moving while we tap + // @y = 400 we pressed the 4th delegate; started flicking, and the press was canceled + QCOMPARE(listView->property("pressedDelegates").toList().first(), 4); + // At first glance one would expect MouseArea and Button would be consistent about this; + // but in fact, before ListView takes over the grab via filtering, + // Button.pressed transitions to false because QQuickAbstractButtonPrivate::handleMove + // sees that the touchpoint has strayed outside its bounds, but it does NOT emit the canceled signal + if (expectCanceled) { + const QVariantList canceledDelegates = listView->property("canceledDelegates").toList(); + QCOMPARE(canceledDelegates.size(), 1); + QCOMPARE(canceledDelegates.first(), 4); + } + QCOMPARE(listView->property("releasedDelegates").toList().size(), 0); + + // press a delegate during flicking (at y > 501 + 100, so likely delegate 6) + QTest::touchEvent(&window, touchDevice.data()).press(0, {100, 100}); + QQuickTouchUtils::flush(&window); + QTest::touchEvent(&window, touchDevice.data()).release(0, {100, 100}); + QQuickTouchUtils::flush(&window); + + const QVariantList pressedDelegates = listView->property("pressedDelegates").toList(); + const QVariantList releasedDelegates = listView->property("releasedDelegates").toList(); + const QVariantList tappedDelegates = listView->property("tappedDelegates").toList(); + const QVariantList canceledDelegates = listView->property("canceledDelegates").toList(); + + qCDebug(lcTests) << "pressed" << pressedDelegates; // usually [4, 6] + qCDebug(lcTests) << "released" << releasedDelegates; + qCDebug(lcTests) << "tapped" << tappedDelegates; + qCDebug(lcTests) << "canceled" << canceledDelegates; + + // which delegate received the second press, during flicking? + const int lastPressed = pressedDelegates.last().toInt(); + QVERIFY(lastPressed > 5); + QCOMPARE(releasedDelegates.last(), lastPressed); + QCOMPARE(tappedDelegates.last(), lastPressed); + QCOMPARE(canceledDelegates.size(), expectCanceled ? 1 : 0); // only the first press was canceled, not the second +} + +void tst_QQuickListView2::flickDuringFlicking_data() +{ + QTest::addColumn<QByteArray>("qmlFile"); + QTest::addColumn<QQuickFlickable::BoundsBehavior>("boundsBehavior"); + + QTest::newRow("Button StopAtBounds") << QByteArray("buttonDelegate.qml") + << QQuickFlickable::BoundsBehavior(QQuickFlickable::StopAtBounds); + QTest::newRow("MouseArea StopAtBounds") << QByteArray("mouseAreaDelegate.qml") + << QQuickFlickable::BoundsBehavior(QQuickFlickable::StopAtBounds); + QTest::newRow("Button DragOverBounds") << QByteArray("buttonDelegate.qml") + << QQuickFlickable::BoundsBehavior(QQuickFlickable::DragOverBounds); + QTest::newRow("MouseArea DragOverBounds") << QByteArray("mouseAreaDelegate.qml") + << QQuickFlickable::BoundsBehavior(QQuickFlickable::DragOverBounds); + QTest::newRow("Button OvershootBounds") << QByteArray("buttonDelegate.qml") + << QQuickFlickable::BoundsBehavior(QQuickFlickable::OvershootBounds); + QTest::newRow("MouseArea OvershootBounds") << QByteArray("mouseAreaDelegate.qml") + << QQuickFlickable::BoundsBehavior(QQuickFlickable::OvershootBounds); + QTest::newRow("Button DragAndOvershootBounds") << QByteArray("buttonDelegate.qml") + << QQuickFlickable::BoundsBehavior(QQuickFlickable::DragAndOvershootBounds); + QTest::newRow("MouseArea DragAndOvershootBounds") << QByteArray("mouseAreaDelegate.qml") + << QQuickFlickable::BoundsBehavior(QQuickFlickable::DragAndOvershootBounds); +} + +void tst_QQuickListView2::flickDuringFlicking() // QTBUG-103832 +{ + QFETCH(QByteArray, qmlFile); + QFETCH(QQuickFlickable::BoundsBehavior, boundsBehavior); + + QQuickView window; + QVERIFY(QQuickTest::showView(window, testFileUrl(qmlFile.constData()))); + QQuickListView *listView = qobject_cast<QQuickListView*>(window.rootObject()); + QVERIFY(listView); + listView->setBoundsBehavior(boundsBehavior); + + flickWithTouch(&window, {100, 400}, {100, 100}); + // let it flick some distance + QTRY_COMPARE_GT(listView->contentY(), 500); + QVERIFY(listView->isFlicking()); // we want to test the case when it's moving and then we flick again + const qreal posBeforeSecondFlick = listView->contentY(); + + // flick again during flicking, and make sure that it doesn't jump back to the first delegate, + // but flicks incrementally further from the position at that time + QTest::touchEvent(&window, touchDevice.data()).press(0, {100, 400}); + QQuickTouchUtils::flush(&window); + qCDebug(lcTests) << "second press: contentY" << posBeforeSecondFlick << "->" << listView->contentY(); + qCDebug(lcTests) << "pressed delegates" << listView->property("pressedDelegates").toList(); + QVERIFY(listView->contentY() >= posBeforeSecondFlick); + + QTest::qWait(20); + QTest::touchEvent(&window, touchDevice.data()).move(0, {100, 300}); + QQuickTouchUtils::flush(&window); + qCDebug(lcTests) << "first move after second press: contentY" << posBeforeSecondFlick << "->" << listView->contentY(); + QVERIFY(listView->contentY() >= posBeforeSecondFlick); + + QTest::qWait(20); + QTest::touchEvent(&window, touchDevice.data()).move(0, {100, 200}); + QQuickTouchUtils::flush(&window); + qCDebug(lcTests) << "second move after second press: contentY" << posBeforeSecondFlick << "->" << listView->contentY(); + QVERIFY(listView->contentY() >= posBeforeSecondFlick + 100); + + QTest::touchEvent(&window, touchDevice.data()).release(0, {100, 100}); +} + +void tst_QQuickListView2::flickWithTouch(QQuickWindow *window, const QPoint &from, const QPoint &to) +{ + QTest::touchEvent(window, touchDevice.data()).press(0, from, window); + QQuickTouchUtils::flush(window); + + QPoint diff = to - from; + for (int i = 1; i <= 8; ++i) { + QTest::touchEvent(window, touchDevice.data()).move(0, from + i * diff / 8, window); + QQuickTouchUtils::flush(window); + } + QTest::touchEvent(window, touchDevice.data()).release(0, to, window); + QQuickTouchUtils::flush(window); +} + +class SingletonModel : public QStringListModel +{ + Q_OBJECT +public: + SingletonModel(QObject* parent = nullptr) : QStringListModel(parent) { } +}; + +void tst_QQuickListView2::singletonModelLifetime() +{ + // this does not really test any functionality of listview, but we do not have a good way + // to unit test QQmlAdaptorModel in isolation. + qmlRegisterSingletonType<SingletonModel>("test", 1, 0, "SingletonModel", + [](QQmlEngine* , QJSEngine*) -> QObject* { return new SingletonModel; }); + + QQmlApplicationEngine engine(testFile("singletonModelLifetime.qml")); + // needs event loop iteration for callLater to execute + QTRY_VERIFY(engine.rootObjects().first()->property("alive").toBool()); +} + +void tst_QQuickListView2::delegateModelRefresh() +{ + // Test case originates from QTBUG-100161 + QQmlApplicationEngine engine(testFile("delegateModelRefresh.qml")); + QVERIFY(!engine.rootObjects().isEmpty()); + // needs event loop iteration for callLater to execute + QTRY_VERIFY(engine.rootObjects().first()->property("done").toBool()); +} + +void tst_QQuickListView2::wheelSnap() +{ + QFETCH(QQuickListView::Orientation, orientation); + QFETCH(Qt::LayoutDirection, layoutDirection); + QFETCH(QQuickItemView::VerticalLayoutDirection, verticalLayoutDirection); + QFETCH(QQuickItemView::HighlightRangeMode, highlightRangeMode); + QFETCH(QPoint, forwardAngleDelta); + QFETCH(qreal, snapAlignment); + QFETCH(qreal, endExtent); + QFETCH(qreal, startExtent); + QFETCH(qreal, preferredHighlightBegin); + QFETCH(qreal, preferredHighlightEnd); + + // Helpers begin + quint64 timestamp = 10; + auto sendWheelEvent = [×tamp](QQuickView *window, const QPoint &angleDelta) { + QPoint pos(100, 100); + QWheelEvent event(pos, window->mapToGlobal(pos), QPoint(), angleDelta, Qt::NoButton, + Qt::NoModifier, Qt::NoScrollPhase, false); + event.setAccepted(false); + event.setTimestamp(timestamp); + QGuiApplication::sendEvent(window, &event); + timestamp += 50; + }; + + auto atEnd = [&layoutDirection, &orientation, + &verticalLayoutDirection](QQuickListView *listview) { + if (orientation == QQuickListView::Horizontal) { + if (layoutDirection == Qt::LeftToRight) + return listview->isAtXEnd(); + + return listview->isAtXBeginning(); + } else { + if (verticalLayoutDirection == QQuickItemView::VerticalLayoutDirection::TopToBottom) + return listview->isAtYEnd(); + + return listview->isAtYBeginning(); + } + }; + + auto atBegin = [&layoutDirection, &orientation, + &verticalLayoutDirection](QQuickListView *listview) { + if (orientation == QQuickListView::Horizontal) { + if (layoutDirection == Qt::LeftToRight) + return listview->isAtXBeginning(); + + return listview->isAtXEnd(); + } else { + if (verticalLayoutDirection == QQuickItemView::VerticalLayoutDirection::TopToBottom) + return listview->isAtYBeginning(); + + return listview->isAtYEnd(); + } + }; + // Helpers end + + QScopedPointer<QQuickView> window(createView()); + QTRY_VERIFY(window); + QQuickViewTestUtils::moveMouseAway(window.data()); + window->setSource(testFileUrl("snapOneItem.qml")); + window->show(); + QVERIFY(QTest::qWaitForWindowExposed(window.data())); + + QQuickListView *listview = qobject_cast<QQuickListView *>(window->rootObject()); + QTRY_VERIFY(listview); + + listview->setOrientation(orientation); + listview->setVerticalLayoutDirection(verticalLayoutDirection); + listview->setLayoutDirection(layoutDirection); + listview->setHighlightRangeMode(highlightRangeMode); + listview->setPreferredHighlightBegin(preferredHighlightBegin); + listview->setPreferredHighlightEnd(preferredHighlightEnd); + QVERIFY(QQuickTest::qWaitForPolish(listview)); + + QQuickItem *contentItem = listview->contentItem(); + QTRY_VERIFY(contentItem); + + QSignalSpy currentIndexSpy(listview, &QQuickListView::currentIndexChanged); + + // confirm that a flick hits the next item boundary + int indexCounter = 0; + sendWheelEvent(window.data(), forwardAngleDelta); + QTRY_VERIFY(listview->isMoving() == false); // wait until it stops + + if (orientation == QQuickListView::Vertical) + QCOMPARE(listview->contentY(), snapAlignment); + else + QCOMPARE(listview->contentX(), snapAlignment); + + if (highlightRangeMode == QQuickItemView::StrictlyEnforceRange) { + ++indexCounter; + QTRY_VERIFY(listview->currentIndex() == indexCounter); + } + + // flick to end + do { + sendWheelEvent(window.data(), forwardAngleDelta); + QTRY_VERIFY(listview->isMoving() == false); // wait until it stops + if (highlightRangeMode == QQuickItemView::StrictlyEnforceRange) { + ++indexCounter; + QTRY_VERIFY(listview->currentIndex() == indexCounter); + } + } while (!atEnd(listview)); + + if (orientation == QQuickListView::Vertical) + QCOMPARE(listview->contentY(), endExtent); + else + QCOMPARE(listview->contentX(), endExtent); + + if (highlightRangeMode == QQuickItemView::StrictlyEnforceRange) { + QCOMPARE(listview->currentIndex(), listview->count() - 1); + QCOMPARE(currentIndexSpy.count(), listview->count() - 1); + } + + // flick to start + const QPoint backwardAngleDelta(-forwardAngleDelta.x(), -forwardAngleDelta.y()); + do { + sendWheelEvent(window.data(), backwardAngleDelta); + QTRY_VERIFY(listview->isMoving() == false); // wait until it stops + if (highlightRangeMode == QQuickItemView::StrictlyEnforceRange) { + --indexCounter; + QTRY_VERIFY(listview->currentIndex() == indexCounter); + } + } while (!atBegin(listview)); + + if (orientation == QQuickListView::Vertical) + QCOMPARE(listview->contentY(), startExtent); + else + QCOMPARE(listview->contentX(), startExtent); + + if (highlightRangeMode == QQuickItemView::StrictlyEnforceRange) { + QCOMPARE(listview->currentIndex(), 0); + QCOMPARE(currentIndexSpy.count(), (listview->count() - 1) * 2); + } +} + +void tst_QQuickListView2::wheelSnap_data() +{ + QTest::addColumn<QQuickListView::Orientation>("orientation"); + QTest::addColumn<Qt::LayoutDirection>("layoutDirection"); + QTest::addColumn<QQuickItemView::VerticalLayoutDirection>("verticalLayoutDirection"); + QTest::addColumn<QQuickItemView::HighlightRangeMode>("highlightRangeMode"); + QTest::addColumn<QPoint>("forwardAngleDelta"); + QTest::addColumn<qreal>("snapAlignment"); + QTest::addColumn<qreal>("endExtent"); + QTest::addColumn<qreal>("startExtent"); + QTest::addColumn<qreal>("preferredHighlightBegin"); + QTest::addColumn<qreal>("preferredHighlightEnd"); + + QTest::newRow("vertical, top to bottom") + << QQuickListView::Vertical << Qt::LeftToRight << QQuickItemView::TopToBottom + << QQuickItemView::NoHighlightRange << QPoint(20, -240) << 200.0 << 600.0 << 0.0 << 0.0 + << 0.0; + + QTest::newRow("vertical, bottom to top") + << QQuickListView::Vertical << Qt::LeftToRight << QQuickItemView::BottomToTop + << QQuickItemView::NoHighlightRange << QPoint(20, 240) << -400.0 << -800.0 << -200.0 + << 0.0 << 0.0; + + QTest::newRow("horizontal, left to right") + << QQuickListView::Horizontal << Qt::LeftToRight << QQuickItemView::TopToBottom + << QQuickItemView::NoHighlightRange << QPoint(-240, 20) << 200.0 << 600.0 << 0.0 << 0.0 + << 0.0; + + QTest::newRow("horizontal, right to left") + << QQuickListView::Horizontal << Qt::RightToLeft << QQuickItemView::TopToBottom + << QQuickItemView::NoHighlightRange << QPoint(240, 20) << -400.0 << -800.0 << -200.0 + << 0.0 << 0.0; + + QTest::newRow("vertical, top to bottom, enforce range") + << QQuickListView::Vertical << Qt::LeftToRight << QQuickItemView::TopToBottom + << QQuickItemView::StrictlyEnforceRange << QPoint(20, -240) << 200.0 << 600.0 << 0.0 + << 0.0 << 0.0; + + QTest::newRow("vertical, bottom to top, enforce range") + << QQuickListView::Vertical << Qt::LeftToRight << QQuickItemView::BottomToTop + << QQuickItemView::StrictlyEnforceRange << QPoint(20, 240) << -400.0 << -800.0 << -200.0 + << 0.0 << 0.0; + + QTest::newRow("horizontal, left to right, enforce range") + << QQuickListView::Horizontal << Qt::LeftToRight << QQuickItemView::TopToBottom + << QQuickItemView::StrictlyEnforceRange << QPoint(-240, 20) << 200.0 << 600.0 << 0.0 + << 0.0 << 0.0; + + QTest::newRow("horizontal, right to left, enforce range") + << QQuickListView::Horizontal << Qt::RightToLeft << QQuickItemView::TopToBottom + << QQuickItemView::StrictlyEnforceRange << QPoint(240, 20) << -400.0 << -800.0 << -200.0 + << 0.0 << 0.0; + + QTest::newRow("vertical, top to bottom, apply range") + << QQuickListView::Vertical << Qt::LeftToRight << QQuickItemView::TopToBottom + << QQuickItemView::ApplyRange << QPoint(20, -240) << 200.0 << 600.0 << 0.0 << 0.0 + << 0.0; + + QTest::newRow("vertical, bottom to top, apply range") + << QQuickListView::Vertical << Qt::LeftToRight << QQuickItemView::BottomToTop + << QQuickItemView::ApplyRange << QPoint(20, 240) << -400.0 << -800.0 << -200.0 << 0.0 + << 0.0; + + QTest::newRow("horizontal, left to right, apply range") + << QQuickListView::Horizontal << Qt::LeftToRight << QQuickItemView::TopToBottom + << QQuickItemView::ApplyRange << QPoint(-240, 20) << 200.0 << 600.0 << 0.0 << 0.0 + << 0.0; + + QTest::newRow("horizontal, right to left, apply range") + << QQuickListView::Horizontal << Qt::RightToLeft << QQuickItemView::TopToBottom + << QQuickItemView::ApplyRange << QPoint(240, 20) << -400.0 << -800.0 << -200.0 << 0.0 + << 0.0; + + QTest::newRow("vertical, top to bottom with highlightRange") + << QQuickListView::Vertical << Qt::LeftToRight << QQuickItemView::TopToBottom + << QQuickItemView::NoHighlightRange << QPoint(20, -240) << 190.0 << 600.0 << 0.0 << 10.0 + << 210.0; + + QTest::newRow("vertical, bottom to top with highlightRange") + << QQuickListView::Vertical << Qt::LeftToRight << QQuickItemView::BottomToTop + << QQuickItemView::NoHighlightRange << QPoint(20, 240) << -390.0 << -800.0 << -200.0 + << 10.0 << 210.0; + + QTest::newRow("horizontal, left to right with highlightRange") + << QQuickListView::Horizontal << Qt::LeftToRight << QQuickItemView::TopToBottom + << QQuickItemView::NoHighlightRange << QPoint(-240, 20) << 190.0 << 600.0 << 0.0 << 10.0 + << 210.0; + + QTest::newRow("horizontal, right to left with highlightRange") + << QQuickListView::Horizontal << Qt::RightToLeft << QQuickItemView::TopToBottom + << QQuickItemView::NoHighlightRange << QPoint(240, 20) << -390.0 << -800.0 << -200.0 + << 10.0 << 210.0; + + QTest::newRow("vertical, top to bottom, enforce range with highlightRange") + << QQuickListView::Vertical << Qt::LeftToRight << QQuickItemView::TopToBottom + << QQuickItemView::StrictlyEnforceRange << QPoint(20, -240) << 190.0 << 590.0 << -10.0 + << 10.0 << 210.0; + + QTest::newRow("vertical, bottom to top, enforce range with highlightRange") + << QQuickListView::Vertical << Qt::LeftToRight << QQuickItemView::BottomToTop + << QQuickItemView::StrictlyEnforceRange << QPoint(20, 240) << -390.0 << -790.0 << -190.0 + << 10.0 << 210.0; + + QTest::newRow("horizontal, left to right, enforce range with highlightRange") + << QQuickListView::Horizontal << Qt::LeftToRight << QQuickItemView::TopToBottom + << QQuickItemView::StrictlyEnforceRange << QPoint(-240, 20) << 190.0 << 590.0 << -10.0 + << 10.0 << 210.0; + + QTest::newRow("horizontal, right to left, enforce range with highlightRange") + << QQuickListView::Horizontal << Qt::RightToLeft << QQuickItemView::TopToBottom + << QQuickItemView::StrictlyEnforceRange << QPoint(240, 20) << -390.0 << -790.0 << -190.0 + << 10.0 << 210.0; + + QTest::newRow("vertical, top to bottom, apply range with highlightRange") + << QQuickListView::Vertical << Qt::LeftToRight << QQuickItemView::TopToBottom + << QQuickItemView::ApplyRange << QPoint(20, -240) << 190.0 << 600.0 << 0.0 << 10.0 + << 210.0; + + QTest::newRow("vertical, bottom to top, apply range with highlightRange") + << QQuickListView::Vertical << Qt::LeftToRight << QQuickItemView::BottomToTop + << QQuickItemView::ApplyRange << QPoint(20, 240) << -390.0 << -800.0 << -200.0 << 10.0 + << 210.0; + + QTest::newRow("horizontal, left to right, apply range with highlightRange") + << QQuickListView::Horizontal << Qt::LeftToRight << QQuickItemView::TopToBottom + << QQuickItemView::ApplyRange << QPoint(-240, 20) << 190.0 << 600.0 << 0.0 << 10.0 + << 210.0; + + QTest::newRow("horizontal, right to left, apply range with highlightRange") + << QQuickListView::Horizontal << Qt::RightToLeft << QQuickItemView::TopToBottom + << QQuickItemView::ApplyRange << QPoint(240, 20) << -390.0 << -800.0 << -200.0 << 10.0 + << 210.0; +} + +class FriendlyItemView : public QQuickItemView +{ + friend class ItemViewAccessor; +}; + +class ItemViewAccessor +{ +public: + ItemViewAccessor(QQuickItemView *itemView) : + mItemView(reinterpret_cast<FriendlyItemView*>(itemView)) + { + } + + qreal maxXExtent() const + { + return mItemView->maxXExtent(); + } + + qreal maxYExtent() const + { + return mItemView->maxYExtent(); + } + +private: + FriendlyItemView *mItemView = nullptr; +}; + +void tst_QQuickListView2::maxExtent_data() +{ + QTest::addColumn<QString>("qmlFilePath"); + QTest::addRow("maxXExtent") << "maxXExtent.qml"; + QTest::addRow("maxYExtent") << "maxYExtent.qml"; +} + +void tst_QQuickListView2::maxExtent() +{ + QFETCH(QString, qmlFilePath); + + QScopedPointer<QQuickView> window(createView()); + QVERIFY(window); + window->setSource(testFileUrl(qmlFilePath)); + QVERIFY2(window->status() == QQuickView::Ready, qPrintable(QDebug::toString(window->errors()))); + window->resize(640, 480); + window->show(); + QVERIFY(QTest::qWaitForWindowExposed(window.data())); + + QQuickListView *view = window->rootObject()->property("view").value<QQuickListView*>(); + QVERIFY(view); + ItemViewAccessor viewAccessor(view); + if (view->orientation() == QQuickListView::Vertical) + QCOMPARE(viewAccessor.maxXExtent(), 0); + else if (view->orientation() == QQuickListView::Horizontal) + QCOMPARE(viewAccessor.maxYExtent(), 0); +} + +void tst_QQuickListView2::isCurrentItem_DelegateModel() +{ + QScopedPointer<QQuickView> window(createView()); + window->setSource(testFileUrl("qtbug86744.qml")); + window->resize(640, 480); + window->show(); + QVERIFY(QTest::qWaitForWindowExposed(window.data())); + QQuickListView* listView = window->rootObject()->findChild<QQuickListView*>("listView"); + QVERIFY(listView); + QVariant value = listView->itemAtIndex(1)->property("isCurrent"); + QVERIFY(value.toBool() == true); +} + +void tst_QQuickListView2::isCurrentItem_NoRegressionWithDelegateModelGroups() +{ + QScopedPointer<QQuickView> window(createView()); + window->setSource(testFileUrl("qtbug98315.qml")); + window->show(); + QVERIFY(QTest::qWaitForWindowExposed(window.data())); + QQuickListView* listView = window->rootObject()->findChild<QQuickListView*>("listView"); + QVERIFY(listView); + + QQuickItem *item3 = listView->itemAtIndex(1); + QVERIFY(item3); + QCOMPARE(item3->property("isCurrent").toBool(), true); + + QObject *item0 = listView->itemAtIndex(0); + QVERIFY(item0); + QCOMPARE(item0->property("isCurrent").toBool(), false); + + // Press left arrow key -> Item 1 should become current, Item 3 should not + // be current anymore. After a previous fix of QTBUG-86744 it was working + // incorrectly - see QTBUG-98315 + QTest::keyPress(window.get(), Qt::Key_Left); + + QTRY_COMPARE(item0->property("isCurrent").toBool(), true); + QCOMPARE(item3->property("isCurrent").toBool(), false); +} + +void tst_QQuickListView2::pullbackSparseList() // QTBUG_104679 +{ + // check if PullbackHeader crashes + QScopedPointer<QQuickView> window(createView()); + QVERIFY(window); + window->setSource(testFileUrl("qtbug104679_header.qml")); + QVERIFY2(window->status() == QQuickView::Ready, qPrintable(QDebug::toString(window->errors()))); + window->resize(640, 480); + window->show(); + QVERIFY(QTest::qWaitForWindowExposed(window.data())); + + // check if PullbackFooter crashes + window.reset(createView()); + QVERIFY(window); + window->setSource(testFileUrl("qtbug104679_footer.qml")); + QVERIFY2(window->status() == QQuickView::Ready, qPrintable(QDebug::toString(window->errors()))); + window->resize(640, 480); + window->show(); + QVERIFY(QTest::qWaitForWindowExposed(window.data())); +} + +void tst_QQuickListView2::highlightWithBound() +{ + QQmlEngine engine; + QQmlComponent c(&engine, testFileUrl("highlightWithBound.qml")); + QVERIFY2(c.isReady(), qPrintable(c.errorString())); + QScopedPointer<QObject> o(c.create()); + QVERIFY(!o.isNull()); + QQuickListView *listView = qobject_cast<QQuickListView *>(o.data()); + QVERIFY(listView); + QQuickItem *highlight = listView->highlightItem(); + QVERIFY(highlight); + QCOMPARE(highlight->objectName(), QStringLiteral("highlight")); +} + +void tst_QQuickListView2::sectionIsCompatibleWithBoundComponents() +{ + QTest::failOnWarning(".?"); + QQmlEngine engine; + QQmlComponent c(&engine, testFileUrl("sectionBoundComponent.qml")); + QVERIFY2(c.isReady(), qPrintable(c.errorString())); + QScopedPointer<QObject> o(c.create()); + QVERIFY(!o.isNull()); + QQuickListView *listView = qobject_cast<QQuickListView *>(o.data()); + QVERIFY(listView); + QTRY_COMPARE(listView->currentSection(), "42"); +} + +void tst_QQuickListView2::sectionGeometryChange() +{ + QScopedPointer<QQuickView> window(createView()); + QTRY_VERIFY(window); + window->setSource(testFileUrl("sectionGeometryChange.qml")); + window->show(); + QVERIFY(QTest::qWaitForWindowExposed(window.data())); + + QQuickListView *listview = findItem<QQuickListView>(window->rootObject(), "list"); + QTRY_VERIFY(listview); + + QQuickItem *contentItem = listview->contentItem(); + QTRY_VERIFY(contentItem); + QVERIFY(QQuickTest::qWaitForPolish(listview)); + + QQuickItem *section1 = findItem<QQuickItem>(contentItem, "Section1"); + QVERIFY(section1); + QQuickItem *element1 = findItem<QQuickItem>(contentItem, "Element1"); + QVERIFY(element1); + + QCOMPARE(element1->y(), section1->y() + section1->height()); + + // Update the height of the section delegate and verify that the next element is not overlapping + section1->setHeight(section1->height() + 10); + QTRY_COMPARE(element1->y(), section1->y() + section1->height()); +} + +void tst_QQuickListView2::areaZeroviewDoesNotNeedlesslyPopulateWholeModel() +{ + QTest::failOnWarning(QRegularExpression(".*")); + QQmlEngine engine; + QQmlComponent c(&engine, testFileUrl("areaZeroView.qml")); + QVERIFY2(c.isReady(), qPrintable(c.errorString())); + std::unique_ptr<QObject> root(c.create()); + QVERIFY(root); + auto delegateCreationCounter = [&]() { + return root->property("delegateCreationCounter").toInt(); + }; + // wait for onComplete to be settled + QTRY_VERIFY(delegateCreationCounter() != 0); + auto view = qobject_cast<QQuickListView *>(qmlContext(root.get())->objectForName("lv")); + QVERIFY(view); + QCOMPARE(view->count(), 6'000); + // we use 100, which is < 6000, but larger than the actual expected value + // that's to give the test some leniency in case the ListView implementation + // changes in the future to instantiate a few more items outside of the viewport + QVERIFY(delegateCreationCounter() < 100); +} + +void tst_QQuickListView2::delegateContextHandling() +{ + QQmlEngine engine; + QQmlComponent c(&engine, testFileUrl("delegateContextHandling.qml")); + QVERIFY2(c.isReady(), qPrintable(c.errorString())); + std::unique_ptr<QObject> o(c.create()); + QVERIFY(o); + + for (int i = 0; i < 10; ++i) { + QQuickItem *delegate = nullptr; + QMetaObject::invokeMethod(o.get(), "toggle", Q_RETURN_ARG(QQuickItem *, delegate)); + QVERIFY(delegate); + } + +} + +class TestFetchMoreModel : public QAbstractListModel +{ + Q_OBJECT + +public: + QVariant data(const QModelIndex& index, int role) const override + { + if (role == Qt::DisplayRole) + return QString::number(index.row()); + return {}; + } + + int columnCount(const QModelIndex&) const override { return 1; } + + int rowCount(const QModelIndex& parent) const override + { + return parent.isValid() ? 0 : m_lines; + } + + QModelIndex parent(const QModelIndex&) const override { return {}; } + + bool canFetchMore(const QModelIndex &) const override { return true; } + + void fetchMore(const QModelIndex & parent) override + { + if (Q_UNLIKELY(parent.isValid())) + return; + beginInsertRows(parent, m_lines, m_lines); + m_lines++; + endInsertRows(); + } + + int m_lines = 3; +}; + +void tst_QQuickListView2::fetchMore_data() +{ + QTest::addColumn<bool>("reuseItems"); + QTest::addColumn<int>("cacheBuffer"); + + QTest::newRow("no reuseItems, default buffer") << false << -1; + QTest::newRow("reuseItems, default buffer") << true << -1; + QTest::newRow("no reuseItems, no buffer") << false << 0; + QTest::newRow("reuseItems, no buffer") << true << 0; + QTest::newRow("no reuseItems, buffer 100 px") << false << 100; + QTest::newRow("reuseItems, buffer 100 px") << true << 100; +} + +void tst_QQuickListView2::fetchMore() // QTBUG-95107 +{ + QFETCH(bool, reuseItems); + QFETCH(int, cacheBuffer); + + TestFetchMoreModel model; + qmlRegisterSingletonInstance("org.qtproject.Test", 1, 0, "FetchMoreModel", &model); + QQuickView window; + QVERIFY(QQuickTest::showView(window, testFileUrl("fetchMore.qml"))); + auto *listView = qobject_cast<QQuickListView*>(window.rootObject()); + QVERIFY(listView); + listView->setReuseItems(reuseItems); + if (cacheBuffer >= 0) + listView->setCacheBuffer(cacheBuffer); + + for (int i = 0; i < 3; ++i) { + const int rowCount = listView->count(); + if (lcTests().isDebugEnabled()) QTest::qWait(1000); + listView->flick(0, -5000); + QTRY_VERIFY(!listView->isMoving()); + qCDebug(lcTests) << "after flick: contentY" << listView->contentY() + << "rows" << rowCount << "->" << listView->count(); + QCOMPARE_GT(listView->count(), rowCount); + QCOMPARE_GE(model.m_lines, listView->count()); // fetchMore() was called + } +} + +void tst_QQuickListView2::changingOrientationResetsPreviousAxisValues_data() +{ + QTest::addColumn<QByteArray>("sourceFile"); + QTest::newRow("ObjectModel") << QByteArray("changingOrientationWithObjectModel.qml"); + QTest::newRow("ListModel") << QByteArray("changingOrientationWithListModel.qml"); +} + +void tst_QQuickListView2::changingOrientationResetsPreviousAxisValues() // QTBUG-115696 +{ + QFETCH(QByteArray, sourceFile); + + QQuickView window; + QVERIFY(QQuickTest::showView(window, testFileUrl(QString::fromLatin1(sourceFile)))); + auto *listView = qobject_cast<QQuickListView *>(window.rootObject()); + QVERIFY(listView); + + // Starts of with vertical orientation. X should be 0 for all delegates, but not Y. + QVERIFY(listView->property("isXReset").toBool()); + QVERIFY(!listView->property("isYReset").toBool()); + + listView->setOrientation(QQuickListView::Orientation::Horizontal); + + // Y should be 0 for all delegates, but not X. + QVERIFY(!listView->property("isXReset").toBool()); + QVERIFY(listView->property("isYReset").toBool()); + + listView->setOrientation(QQuickListView::Orientation::Vertical); + + // X should be 0 for all delegates, but not Y. + QVERIFY(listView->property("isXReset").toBool()); + QVERIFY(!listView->property("isYReset").toBool()); +} + +void tst_QQuickListView2::bindingDirectlyOnPositionInHeaderAndFooterDelegates_data() +{ + QTest::addColumn<QByteArray>("sourceFile"); + QTest::addColumn<qreal(QQuickItem::*)()const>("pos"); + QTest::addColumn<qreal(QQuickItem::*)()const>("size"); + QTest::newRow("XPosition") << QByteArray("bindOnHeaderAndFooterXPosition.qml") << &QQuickItem::x << &QQuickItem::width; + QTest::newRow("YPosition") << QByteArray("bindOnHeaderAndFooterYPosition.qml") << &QQuickItem::y << &QQuickItem::height; +} +void tst_QQuickListView2::bindingDirectlyOnPositionInHeaderAndFooterDelegates() +{ + + typedef qreal (QQuickItem::*position_func_t)() const; + QFETCH(QByteArray, sourceFile); + QFETCH(position_func_t, pos); + QFETCH(position_func_t, size); + + QQuickView window; + QVERIFY(QQuickTest::showView(window, testFileUrl(QString::fromLatin1(sourceFile)))); + auto *listView = qobject_cast<QQuickListView *>(window.rootObject()); + QVERIFY(listView); + + const qreal widthOrHeight = (listView->*size)(); + + QCOMPARE((listView->headerItem()->*pos)(), (widthOrHeight - 50) / 2); + QCOMPARE((listView->footerItem()->*pos)(), (widthOrHeight - 50) / 2); + + // Verify that the "regular" delegate items, don't honor x and y bindings. + // This should only be allowed for header and footer delegates. + for (int i = 0; i < listView->count(); ++i) + QCOMPARE((listView->itemAtIndex(i)->*pos)(), 0); +} + +void tst_QQuickListView2::clearObjectListModel() +{ + QQmlEngine engine; + QQmlComponent delegate(&engine); + + // Need one required property to trigger the incremental rebuilding of metaobjects. + delegate.setData("import QtQuick\nItem { required property int index }", QUrl()); + + QQuickListView list; + engine.setContextForObject(&list, engine.rootContext()); + list.setDelegate(&delegate); + list.setWidth(640); + list.setHeight(480); + + QScopedPointer modelObject(new QObject); + + // Use a list that might also carry something non-QObject + + list.setModel(QVariantList { + QVariant::fromValue(modelObject.data()), + QVariant::fromValue(modelObject.data()) + }); + + QVERIFY(list.itemAtIndex(0)); + + modelObject.reset(); + + // list should not access dangling pointer from old model data anymore. + list.setModel(QVariantList()); + + QVERIFY(!list.itemAtIndex(0)); +} + +QTEST_MAIN(tst_QQuickListView2) + +#include "tst_qquicklistview2.moc" diff --git a/tests/auto/quick/qquicklistview2/typerolemodel.cpp b/tests/auto/quick/qquicklistview2/typerolemodel.cpp new file mode 100644 index 0000000000..94da87beda --- /dev/null +++ b/tests/auto/quick/qquicklistview2/typerolemodel.cpp @@ -0,0 +1,41 @@ +// Copyright (C) 2021 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only + +#include "typerolemodel.h" + +TypeRoleModel::TypeRoleModel(QObject *parent) + : QAbstractListModel(parent) +{ + _mapRoleNames[TypeRole] = "type"; + _mapRoleNames[TextRole] = "text"; +} + +int TypeRoleModel::rowCount(const QModelIndex &) const +{ + return 3; +} + +QVariant TypeRoleModel::data(const QModelIndex &index, int role) const +{ + if (!index.isValid()) + return {}; + + constexpr Type types[] = { + Type::PlainText, + Type::Markdown, + Type::Rect + }; + switch (role) { + case TypeRole: { + const Type type = types[index.row() % std::size(types)]; + return QVariant::fromValue(type); + } + case TextRole: { + if (index.row() % std::size(types) == int(Type::Markdown)) + return "*row* " + QString::number(index.row()); + return "row " + QString::number(index.row()); + } + } + + return {}; +} diff --git a/tests/auto/quick/qquicklistview2/typerolemodel.h b/tests/auto/quick/qquicklistview2/typerolemodel.h new file mode 100644 index 0000000000..f47400a88c --- /dev/null +++ b/tests/auto/quick/qquicklistview2/typerolemodel.h @@ -0,0 +1,30 @@ +// Copyright (C) 2021 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only + +#include <QAbstractListModel> +#include <qqml.h> + +class TypeRoleModel : public QAbstractListModel +{ + Q_OBJECT + QML_ELEMENT + +public: + enum Role { + TypeRole = Qt::UserRole + 1, + TextRole, + }; + Q_ENUM(Role) + + enum class Type { PlainText, Markdown, Rect }; + Q_ENUM(Type) + + explicit TypeRoleModel(QObject *parent = nullptr); + + int rowCount(const QModelIndex &parent = QModelIndex()) const override; + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + QHash<int, QByteArray> roleNames() const override { return _mapRoleNames; } + +private: + QHash<int, QByteArray> _mapRoleNames; +}; |