diff options
Diffstat (limited to 'tests/auto/quickcontrols2/controls/data/tst_tumbler.qml')
-rw-r--r-- | tests/auto/quickcontrols2/controls/data/tst_tumbler.qml | 1281 |
1 files changed, 1281 insertions, 0 deletions
diff --git a/tests/auto/quickcontrols2/controls/data/tst_tumbler.qml b/tests/auto/quickcontrols2/controls/data/tst_tumbler.qml new file mode 100644 index 0000000000..3a09bd9fe9 --- /dev/null +++ b/tests/auto/quickcontrols2/controls/data/tst_tumbler.qml @@ -0,0 +1,1281 @@ +/**************************************************************************** +** +** Copyright (C) 2019 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the test suite of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:BSD$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** BSD License Usage +** Alternatively, you may use this file under the terms of the BSD license +** as follows: +** +** "Redistribution and use in source and binary forms, with or without +** modification, are permitted provided that the following conditions are +** met: +** * Redistributions of source code must retain the above copyright +** notice, this list of conditions and the following disclaimer. +** * Redistributions in binary form must reproduce the above copyright +** notice, this list of conditions and the following disclaimer in +** the documentation and/or other materials provided with the +** distribution. +** * Neither the name of The Qt Company Ltd nor the names of its +** contributors may be used to endorse or promote products derived +** from this software without specific prior written permission. +** +** +** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +import QtQuick +import QtTest +import QtQuick.Controls + +TestCase { + id: testCase + width: 300 + height: 300 + visible: true + when: windowShown + name: "Tumbler" + + property var tumbler: null + readonly property real implicitTumblerWidth: 60 + readonly property real implicitTumblerHeight: 200 + readonly property real defaultImplicitDelegateHeight: implicitTumblerHeight / 3 + readonly property real defaultListViewTumblerOffset: -defaultImplicitDelegateHeight + readonly property real tumblerDelegateHeight: tumbler ? tumbler.availableHeight / tumbler.visibleItemCount : 0 + property Item tumblerView: null + + Component { + id: tumblerComponent + + Tumbler { + visibleItemCount: 3 + } + } + + Component { + id: itemComponent + + Item { + anchors.fill: parent + } + } + + function createTumbler(args) { + tumbler = createTemporaryObject(tumblerComponent, testCase, args); + verify(tumbler, "Tumbler: failed to create an instance"); + tumblerView = findView(tumbler); + verify(tumblerView); + } + + function tumblerXCenter() { + return tumbler.leftPadding + tumbler.width / 2; + } + + function tumblerYCenter() { + return tumbler.topPadding + tumbler.height / 2; + } + + // visualItemIndex is from 0 to the amount of visible items. + function itemCenterPos(visualItemIndex) { + var halfDelegateHeight = tumblerDelegateHeight / 2; + var yCenter = tumbler.y + tumbler.topPadding + halfDelegateHeight + + (tumblerDelegateHeight * visualItemIndex); + return Qt.point(tumblerXCenter(), yCenter); + } + + function itemTopLeftPos(visualItemIndex) { + return Qt.point(tumbler.leftPadding, tumbler.topPadding + (tumblerDelegateHeight * visualItemIndex)); + } + + function checkItemSizes() { + var contentChildren = tumbler.wrap ? tumblerView.children : tumblerView.contentItem.children; + verify(contentChildren.length >= tumbler.count); + for (var i = 0; i < contentChildren.length; ++i) { + compare(contentChildren[i].width, tumbler.availableWidth); + compare(contentChildren[i].height, tumblerDelegateHeight); + } + } + + function findView(parent) { + for (var i = 0; i < parent.children.length; ++i) { + var child = parent.children[i]; + if (child.hasOwnProperty("currentIndex")) { + return child; + } + + var grandChild = findView(child); + if (grandChild) + return grandChild; + } + + return null; + } + + function findDelegateWithText(parent, text) { + for (var i = 0; i < parent.children.length; ++i) { + var child = parent.children[i]; + if (child.hasOwnProperty("text") && child.text === text) { + return child; + } + + var grandChild = findDelegateWithText(child, text); + if (grandChild) + return grandChild; + } + + return null; + } + + property Component noAttachedPropertiesDelegate: Text { + text: modelData + } + + function test_wrapWithoutAttachedProperties() { + createTumbler(); + verify(tumbler.wrap); + + tumbler.delegate = noAttachedPropertiesDelegate; + // Shouldn't assert. + tumbler.wrap = false; + verify(findView(tumbler)); + } + + // TODO: test that currentIndex is maintained between contentItem changes... +// function tst_dynamicContentItemChange() { +// } + + function test_currentIndex() { + createTumbler(); + compare(tumbler.contentItem.parent, tumbler); + + tumbler.model = 5; + + compare(tumbler.currentIndex, 0); + waitForRendering(tumbler); + + // Set it through user interaction. + var pos = Qt.point(tumblerXCenter(), tumbler.height / 2); + mouseDrag(tumbler, pos.x, pos.y, 0, tumbler.height / 3, Qt.LeftButton, Qt.NoModifier, 200); + tryCompare(tumblerView, "offset", 1); + compare(tumbler.currentIndex, 4); + compare(tumblerView.currentIndex, 4); + + // Set it manually. + tumbler.currentIndex = 2; + tryCompare(tumbler, "currentIndex", 2); + compare(tumblerView.currentIndex, 2); + + tumbler.model = null; + tryCompare(tumbler, "currentIndex", -1); + // PathView will only use 0 as the currentIndex when there are no items. + compare(tumblerView.currentIndex, 0); + + tumbler.model = ["A", "B", "C"]; + tryCompare(tumbler, "currentIndex", 0); + + // Setting a negative current index should have no effect, because the model isn't empty. + tumbler.currentIndex = -1; + compare(tumbler.currentIndex, 0); + + tumbler.model = 1; + compare(tumbler.currentIndex, 0); + + tumbler.model = 5; + compare(tumbler.count, 5); + tumblerView = findView(tumbler); + tryCompare(tumblerView, "count", 5); + tumbler.currentIndex = 4; + compare(tumbler.currentIndex, 4); + compare(tumblerView.currentIndex, 4); + + --tumbler.model; + compare(tumbler.count, 4); + compare(tumblerView.count, 4); + // Removing an item from an integer-based model will cause views to reset their currentIndex to 0. + compare(tumbler.currentIndex, 0); + compare(tumblerView.currentIndex, 0); + + tumbler.model = 0; + compare(tumbler.currentIndex, -1); + } + + Component { + id: currentIndexTumbler + + Tumbler { + model: 5 + currentIndex: 2 + visibleItemCount: 3 + } + } + + Component { + id: currentIndexTumblerNoWrap + + Tumbler { + model: 5 + currentIndex: 2 + wrap: false + visibleItemCount: 3 + } + } + + Component { + id: currentIndexTumblerNoWrapReversedOrder + + Tumbler { + model: 5 + wrap: false + currentIndex: 2 + visibleItemCount: 3 + } + } + + Component { + id: negativeCurrentIndexTumblerNoWrap + + Tumbler { + model: 5 + wrap: false + currentIndex: -1 + visibleItemCount: 3 + } + } + + Component { + id: currentIndexTooLargeTumbler + + Tumbler { + objectName: "currentIndexTooLargeTumbler" + model: 10 + currentIndex: 10 + } + } + + + function test_currentIndexAtCreation_data() { + return [ + { tag: "wrap: implicit, expected currentIndex: 2", currentIndex: 2, wrap: true, component: currentIndexTumbler }, + { tag: "wrap: false, expected currentIndex: 2", currentIndex: 2, wrap: false, component: currentIndexTumblerNoWrap }, + // Order of property assignments shouldn't matter + { tag: "wrap: false, expected currentIndex: 2, reversed property assignment order", + currentIndex: 2, wrap: false, component: currentIndexTumblerNoWrapReversedOrder }, + { tag: "wrap: false, expected currentIndex: 0", currentIndex: 0, wrap: false, component: negativeCurrentIndexTumblerNoWrap }, + { tag: "wrap: implicit, expected currentIndex: 0", currentIndex: 0, wrap: true, component: currentIndexTooLargeTumbler } + ] + } + + function test_currentIndexAtCreation(data) { + // Test setting currentIndex at creation time + tumbler = createTemporaryObject(data.component, testCase); + verify(tumbler); + // A "statically declared" currentIndex will be pending until the count has changed, + // which happens when the model is set, which happens on the TumblerView's next polish. + tryCompare(tumbler, "currentIndex", data.currentIndex); + + tumblerView = findView(tumbler); + tryVerify(function() { return tumblerView.currentItem }); + compare(tumblerView.currentIndex, data.currentIndex); + compare(tumblerView.currentItem.text, data.currentIndex.toString()); + + if (data.wrap) { + tryCompare(tumblerView, "offset", data.currentIndex > 0 ? tumblerView.count - data.currentIndex : 0); + } else { + tryCompare(tumblerView, "contentY", tumblerDelegateHeight * data.currentIndex - tumblerView.preferredHighlightBegin); + } + } + + function test_keyboardNavigation() { + createTumbler(); + + tumbler.model = 5; + tumbler.forceActiveFocus(); + tumblerView.highlightMoveDuration = 0; + + // Navigate upwards through entire wheel. + for (var j = 0; j < tumbler.count - 1; ++j) { + keyClick(Qt.Key_Up, Qt.NoModifier); + tryCompare(tumblerView, "offset", j + 1); + compare(tumbler.currentIndex, tumbler.count - 1 - j); + } + + keyClick(Qt.Key_Up, Qt.NoModifier); + tryCompare(tumblerView, "offset", 0); + compare(tumbler.currentIndex, 0); + + // Navigate downwards through entire wheel. + for (j = 0; j < tumbler.count - 1; ++j) { + keyClick(Qt.Key_Down, Qt.NoModifier); + tryCompare(tumblerView, "offset", tumbler.count - 1 - j); + compare(tumbler.currentIndex, j + 1); + } + + keyClick(Qt.Key_Down, Qt.NoModifier); + tryCompare(tumblerView, "offset", 0); + compare(tumbler.currentIndex, 0); + } + + function test_itemsCorrectlyPositioned() { + createTumbler(); + + tumbler.model = 4; + tumbler.height = 120; + compare(tumblerDelegateHeight, 40); + checkItemSizes(); + + wait(tumblerView.highlightMoveDuration); + var firstItemCenterPos = itemCenterPos(1); + var firstItem = tumblerView.itemAt(firstItemCenterPos.x, firstItemCenterPos.y); + var actualPos = testCase.mapFromItem(firstItem, 0, 0); + compare(actualPos.x, tumbler.leftPadding); + compare(actualPos.y, tumbler.topPadding + 40); + + tumbler.forceActiveFocus(); + keyClick(Qt.Key_Down); + tryCompare(tumblerView, "offset", 3.0); + tryCompare(tumbler, "moving", false); + firstItemCenterPos = itemCenterPos(0); + firstItem = tumblerView.itemAt(firstItemCenterPos.x, firstItemCenterPos.y); + verify(firstItem); + // Test QTBUG-40298. + actualPos = testCase.mapFromItem(firstItem, 0, 0); + fuzzyCompare(actualPos.x, tumbler.leftPadding, 0.0001); + fuzzyCompare(actualPos.y, tumbler.topPadding, 0.0001); + + var secondItemCenterPos = itemCenterPos(1); + var secondItem = tumblerView.itemAt(secondItemCenterPos.x, secondItemCenterPos.y); + verify(secondItem); + verify(firstItem.y < secondItem.y); + + var thirdItemCenterPos = itemCenterPos(2); + var thirdItem = tumblerView.itemAt(thirdItemCenterPos.x, thirdItemCenterPos.y); + verify(thirdItem); + verify(firstItem.y < thirdItem.y); + verify(secondItem.y < thirdItem.y); + } + + function test_focusPastTumbler() { + tumbler = createTemporaryObject(tumblerComponent, testCase); + verify(tumbler); + + var mouseArea = createTemporaryQmlObject( + "import QtQuick; TextInput { activeFocusOnTab: true; width: 50; height: 50 }", testCase, ""); + + tumbler.forceActiveFocus(); + verify(tumbler.activeFocus); + + keyClick(Qt.Key_Tab); + verify(!tumbler.activeFocus); + verify(mouseArea.activeFocus); + } + + function test_datePicker() { + var component = Qt.createComponent("TumblerDatePicker.qml"); + compare(component.status, Component.Ready, component.errorString()); + tumbler = createTemporaryObject(component, testCase); + // Should not be any warnings. + + tryCompare(tumbler.dayTumbler, "currentIndex", 0); + compare(tumbler.dayTumbler.count, 31); + compare(tumbler.monthTumbler.currentIndex, 0); + compare(tumbler.monthTumbler.count, 12); + compare(tumbler.yearTumbler.currentIndex, 0); + tryCompare(tumbler.yearTumbler, "count", 100); + + verify(findView(tumbler.dayTumbler).children.length >= tumbler.dayTumbler.visibleItemCount); + verify(findView(tumbler.monthTumbler).children.length >= tumbler.monthTumbler.visibleItemCount); + // TODO: do this properly somehow + wait(100); + verify(findView(tumbler.yearTumbler).children.length >= tumbler.yearTumbler.visibleItemCount); + + // March. + tumbler.monthTumbler.currentIndex = 2; + tryCompare(tumbler.monthTumbler, "currentIndex", 2); + + // 30th of March. + tumbler.dayTumbler.currentIndex = 29; + tryCompare(tumbler.dayTumbler, "currentIndex", 29); + + // February. + tumbler.monthTumbler.currentIndex = 1; + tryCompare(tumbler.monthTumbler, "currentIndex", 1); + tryCompare(tumbler.dayTumbler, "currentIndex", 27); + } + + Component { + id: timePickerComponent + + Row { + property alias minuteTumbler: minuteTumbler + property alias amPmTumbler: amPmTumbler + + Tumbler { + id: minuteTumbler + currentIndex: 6 + model: 60 + width: 50 + height: 150 + } + + Tumbler { + id: amPmTumbler + model: ["AM", "PM"] + width: 50 + height: 150 + contentItem: ListView { + anchors.fill: parent + model: amPmTumbler.model + delegate: amPmTumbler.delegate + } + } + } + } + + function test_listViewTimePicker() { + var root = createTemporaryObject(timePickerComponent, testCase); + verify(root); + + mouseDrag(root.minuteTumbler, root.minuteTumbler.width / 2, root.minuteTumbler.height / 2, 0, 50); + // Shouldn't crash. + mouseDrag(root.amPmTumbler, root.amPmTumbler.width / 2, root.amPmTumbler.height / 2, 0, 50); + } + + function test_displacement_data() { + var data = [ + // At 0 offset, the first item is current. + { count: 6, index: 0, offset: 0, expectedDisplacement: 0 }, + { count: 6, index: 1, offset: 0, expectedDisplacement: -1 }, + { count: 6, index: 5, offset: 0, expectedDisplacement: 1 }, + // When we start to move the first item down, the second item above it starts to become current. + { count: 6, index: 0, offset: 0.25, expectedDisplacement: -0.25 }, + { count: 6, index: 1, offset: 0.25, expectedDisplacement: -1.25 }, + { count: 6, index: 5, offset: 0.25, expectedDisplacement: 0.75 }, + { count: 6, index: 0, offset: 0.5, expectedDisplacement: -0.5 }, + { count: 6, index: 1, offset: 0.5, expectedDisplacement: -1.5 }, + { count: 6, index: 5, offset: 0.5, expectedDisplacement: 0.5 }, + // By this stage, the delegate at index 1 is destroyed, so we can't test its displacement. + { count: 6, index: 0, offset: 0.75, expectedDisplacement: -0.75 }, + { count: 6, index: 5, offset: 0.75, expectedDisplacement: 0.25 }, + { count: 6, index: 0, offset: 4.75, expectedDisplacement: 1.25 }, + { count: 6, index: 1, offset: 4.75, expectedDisplacement: 0.25 }, + { count: 6, index: 0, offset: 4.5, expectedDisplacement: 1.5 }, + { count: 6, index: 1, offset: 4.5, expectedDisplacement: 0.5 }, + { count: 6, index: 0, offset: 4.25, expectedDisplacement: 1.75 }, + { count: 6, index: 1, offset: 4.25, expectedDisplacement: 0.75 }, + // count == visibleItemCount + { count: 3, index: 0, offset: 0, expectedDisplacement: 0 }, + { count: 3, index: 1, offset: 0, expectedDisplacement: -1 }, + { count: 3, index: 2, offset: 0, expectedDisplacement: 1 }, + // count < visibleItemCount + { count: 2, index: 0, offset: 0, expectedDisplacement: 0 }, + { count: 2, index: 1, offset: 0, expectedDisplacement: 1 }, + // count == 1 + { count: 1, index: 0, offset: 0, expectedDisplacement: 0 } + ]; + for (var i = 0; i < data.length; ++i) { + var row = data[i]; + row.tag = "delegate" + row.index + " offset=" + row.offset + " expectedDisplacement=" + row.expectedDisplacement; + } + return data; + } + + property Component displacementDelegate: Text { + objectName: "delegate" + index + text: modelData + opacity: 0.2 + Math.max(0, 1 - Math.abs(Tumbler.displacement)) * 0.8 + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + + Text { + text: parent.displacement.toFixed(2) + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + } + + property real displacement: Tumbler.displacement + } + + function test_displacement(data) { + createTumbler(); + + // TODO: test setting these in the opposite order (delegate after model + // doesn't seem to cause a change in delegates in PathView) + tumbler.wrap = true; + tumbler.delegate = displacementDelegate; + tumbler.model = data.count; + compare(tumbler.count, data.count); + + var delegate = findChild(tumblerView, "delegate" + data.index); + verify(delegate); + + tumblerView.offset = data.offset; + compare(delegate.displacement, data.expectedDisplacement); + + // test displacement after adding and removing items + } + + function test_wrap() { + createTumbler(); + + tumbler.model = 5; + compare(tumbler.count, 5); + + tumbler.currentIndex = 2; + compare(tumblerView.currentIndex, 2); + + tumbler.wrap = false; + tumblerView = findView(tumbler); + compare(tumbler.count, 5); + compare(tumbler.currentIndex, 2); + // Tumbler's count hasn't changed (the model hasn't changed), + // but the new view needs time to instantiate its items. + tryCompare(tumblerView, "count", 5); + compare(tumblerView.currentIndex, 2); + } + + Component { + id: twoItemTumbler + + Tumbler { + model: 2 + } + } + + Component { + id: tenItemTumbler + + Tumbler { + model: 10 + } + } + + function test_countWrap() { + tumbler = createTemporaryObject(tumblerComponent, testCase); + verify(tumbler); + + // Check that a count that is less than visibleItemCount results in wrap being set to false. + verify(2 < tumbler.visibleItemCount); + tumbler.model = 2; + compare(tumbler.count, 2); + compare(tumbler.wrap, false); + } + + function test_explicitlyNonwrapping() { + // Check that explicitly setting wrap to false works even when it was implicitly false. + var explicitlyNonWrapping = createTemporaryObject(twoItemTumbler, testCase); + verify(explicitlyNonWrapping); + tryCompare(explicitlyNonWrapping, "wrap", false); + + explicitlyNonWrapping.wrap = false; + // wrap shouldn't be set to true now that there are more items than there are visible ones. + verify(10 > explicitlyNonWrapping.visibleItemCount); + explicitlyNonWrapping.model = 10; + compare(explicitlyNonWrapping.wrap, false); + + // Test resetting wrap back to the default behavior. + explicitlyNonWrapping.wrap = undefined; + compare(explicitlyNonWrapping.wrap, true); + } + + function test_explicitlyWrapping() { + // Check that explicitly setting wrap to true works even when it was implicitly true. + var explicitlyWrapping = createTemporaryObject(tenItemTumbler, testCase); + verify(explicitlyWrapping); + compare(explicitlyWrapping.wrap, true); + + explicitlyWrapping.wrap = true; + // wrap shouldn't be set to false now that there are more items than there are visible ones. + explicitlyWrapping.model = 2; + compare(explicitlyWrapping.wrap, true); + + // Test resetting wrap back to the default behavior. + explicitlyWrapping.wrap = undefined; + compare(explicitlyWrapping.wrap, false); + } + + Component { + id: customListViewTumblerComponent + + Tumbler { + id: listViewTumbler + + contentItem: ListView { + anchors.fill: parent + model: listViewTumbler.model + delegate: listViewTumbler.delegate + + snapMode: ListView.SnapToItem + highlightRangeMode: ListView.StrictlyEnforceRange + preferredHighlightBegin: height / 2 - (height / listViewTumbler.visibleItemCount / 2) + preferredHighlightEnd: height / 2 + (height / listViewTumbler.visibleItemCount / 2) + clip: true + } + } + } + + Component { + id: customPathViewTumblerComponent + + Tumbler { + id: pathViewTumbler + + contentItem: PathView { + id: pathView + model: pathViewTumbler.model + delegate: pathViewTumbler.delegate + clip: true + pathItemCount: pathViewTumbler.visibleItemCount + 1 + preferredHighlightBegin: 0.5 + preferredHighlightEnd: 0.5 + dragMargin: width / 2 + + path: Path { + startX: pathView.width / 2 + startY: -pathView.delegateHeight / 2 + PathLine { + x: pathView.width / 2 + y: pathView.pathItemCount * pathView.delegateHeight - pathView.delegateHeight / 2 + } + } + + property real delegateHeight: pathViewTumbler.availableHeight / pathViewTumbler.visibleItemCount + } + } + } + + function test_customContentItemAtConstruction_data() { + return [ + { tag: "ListView", component: customListViewTumblerComponent }, + { tag: "PathView", component: customPathViewTumblerComponent } + ]; + } + + function test_customContentItemAtConstruction(data) { + var tumbler = createTemporaryObject(data.component, testCase); + // Shouldn't assert. + + tumbler.model = 5; + compare(tumbler.count, 5); + + tumbler.currentIndex = 2; + var tumblerView = findView(tumbler); + compare(tumblerView.currentIndex, 2); + + tumblerView.incrementCurrentIndex(); + compare(tumblerView.currentIndex, 3); + compare(tumbler.currentIndex, 3); + + // Shouldn't have any affect. + tumbler.wrap = false; + compare(tumbler.count, 5); + compare(tumblerView.currentIndex, 3); + compare(tumbler.currentIndex, 3); + } + + function findFirstDelegateWithText(view, text) { + var delegate = null; + var contentItem = view.hasOwnProperty("contentItem") ? view.contentItem : view; + for (var i = 0; i < contentItem.children.length && !delegate; ++i) { + var child = contentItem.children[i]; + if (child.hasOwnProperty("text") && child.text === text) + delegate = child; + } + return delegate; + } + + function test_customContentItemAfterConstruction_data() { + return [ + { tag: "ListView", componentPath: "TumblerListView.qml" }, + { tag: "PathView", componentPath: "TumblerPathView.qml" } + ]; + } + + function test_customContentItemAfterConstruction(data) { + createTumbler(); + + tumbler.model = 5; + compare(tumbler.count, 5); + + tumbler.currentIndex = 2; + compare(tumblerView.currentIndex, 2); + + var contentItemComponent = Qt.createComponent(data.componentPath); + compare(contentItemComponent.status, Component.Ready); + + var customContentItem = createTemporaryObject(contentItemComponent, tumbler); + tumbler.contentItem = customContentItem; + compare(tumbler.count, 5); + tumblerView = findView(tumbler); + compare(tumblerView.currentIndex, 2); + + var delegate = findFirstDelegateWithText(tumblerView, "Custom2"); + verify(delegate); + compare(delegate.height, defaultImplicitDelegateHeight); + tryCompare(delegate.Tumbler, "displacement", 0); + + tumblerView.incrementCurrentIndex(); + compare(tumblerView.currentIndex, 3); + compare(tumbler.currentIndex, 3); + } + + function test_displacementListView_data() { + var offset = defaultListViewTumblerOffset; + + var data = [ + // At 0 contentY, the first item is current. + { contentY: offset, expectedDisplacements: [ + { index: 0, displacement: 0 }, + { index: 1, displacement: -1 }, + { index: 2, displacement: -2 } ] + }, + // When we start to move the first item down, the second item above it starts to become current. + { contentY: offset + defaultImplicitDelegateHeight * 0.25, expectedDisplacements: [ + { index: 0, displacement: 0.25 }, + { index: 1, displacement: -0.75 }, + { index: 2, displacement: -1.75 } ] + }, + { contentY: offset + defaultImplicitDelegateHeight * 0.5, expectedDisplacements: [ + { index: 0, displacement: 0.5 }, + { index: 1, displacement: -0.5 }, + { index: 2, displacement: -1.5 } ] + }, + { contentY: offset + defaultImplicitDelegateHeight * 0.75, expectedDisplacements: [ + { index: 0, displacement: 0.75 }, + { index: 1, displacement: -0.25 } ] + }, + { contentY: offset + defaultImplicitDelegateHeight * 3.5, expectedDisplacements: [ + { index: 3, displacement: 0.5 }, + { index: 4, displacement: -0.5 } ] + } + ]; + for (var i = 0; i < data.length; ++i) { + var row = data[i]; + row.tag = "contentY=" + row.contentY; + } + return data; + } + + function test_displacementListView(data) { + createTumbler(); + + tumbler.wrap = false; + tumbler.delegate = displacementDelegate; + tumbler.model = 5; + compare(tumbler.count, 5); + // Ensure assumptions about the tumbler used in our data() function are correct. + tumblerView = findView(tumbler); + compare(tumblerView.contentY, -defaultImplicitDelegateHeight); + var delegateCount = 0; + var listView = tumblerView; + var listViewContentItem = tumblerView.contentItem; + + // We use the mouse instead of setting contentY directly, otherwise the + // items snap back into place. This doesn't seem to be an issue for + // PathView for some reason. + // + // I tried lots of things to get this test to work with small changes + // in ListView's contentY (to match the tests for a PathView-based Tumbler), but they didn't work: + // + // - Pressing once and then directly moving the mouse to the correct location + // - Pressing once and interpolating the mouse position to the correct location + // - Pressing once and doing some dragging up and down to trigger the + // overThreshold of QQuickFlickable + // + // Even after the last item above, QQuickFlickable wouldn't consider it a drag. + // It seems that overThreshold is set too late, and because the drag distance is quite small + // to begin with, nothing changes (the displacement was always very close to 0 in the end). + + // Ensure that we at least cover the distance required to reach the desired contentY. + var distanceToReachContentY = data.contentY - defaultListViewTumblerOffset; + var distance = Math.abs(distanceToReachContentY) + tumbler.height / 2; + // If distanceToReachContentY is 0, we're testing 0 displacement, so we don't need to do anything. + if (distanceToReachContentY != 0) { + mousePress(tumbler, tumblerXCenter(), tumblerYCenter()); + + var dragDirection = distanceToReachContentY > 0 ? -1 : 1; + for (var i = 0; i < distance && Math.floor(listView.contentY) !== Math.floor(data.contentY); ++i) { + mouseMove(tumbler, tumblerXCenter(), tumblerYCenter() + i * dragDirection); + wait(1); // because Flickable pays attention to velocity, we need some time between movements (qtdeclarative ebf07c3) + } + } + + for (var i = 0; i < data.expectedDisplacements.length; ++i) { + var delegate = findChild(listViewContentItem, "delegate" + data.expectedDisplacements[i].index); + verify(delegate); + compare(delegate.height, defaultImplicitDelegateHeight); + // Due to the way we must perform this test, we can't expect high precision. + var expectedDisplacement = data.expectedDisplacements[i].displacement; + fuzzyCompare(delegate.displacement, expectedDisplacement, 0.1, + "Delegate of ListView-based Tumbler at index " + data.expectedDisplacements[i].index + + " has displacement of " + delegate.displacement + " when it should be " + expectedDisplacement); + } + + if (distanceToReachContentY != 0) + mouseRelease(tumbler, tumblerXCenter(), itemCenterPos(1) + (data.contentY - defaultListViewTumblerOffset), Qt.LeftButton); + } + + function test_listViewFlickAboveBounds_data() { + // Tests that flicking above the bounds when already at the top of the + // tumbler doesn't result in an incorrect displacement. + var data = []; + // Less than two items doesn't make sense. The default visibleItemCount + // is 3, so we test a bit more than double that. + for (var i = 2; i <= 7; ++i) { + data.push({ tag: i + " items", model: i }); + } + return data; + } + + function test_listViewFlickAboveBounds(data) { + createTumbler(); + + tumbler.wrap = false; + tumbler.delegate = displacementDelegate; + tumbler.model = data.model; + tumblerView = findView(tumbler); + + mousePress(tumbler, tumblerXCenter(), tumblerYCenter()); + + // Ensure it's stationary. + var listView = tumblerView; + compare(listView.contentY, defaultListViewTumblerOffset); + + // We could just move up until the contentY changed, but this is safer. + var distance = tumbler.height; + var changed = false; + + for (var i = 0; i < distance && !changed; ++i) { + mouseMove(tumbler, tumblerXCenter(), tumblerYCenter() + i, 10); + + // Don't test until the contentY has actually changed. + if (Math.abs(listView.contentY) - listView.preferredHighlightBegin > 0.01) { + + for (var delegateIndex = 0; delegateIndex < Math.min(tumbler.count, tumbler.visibleItemCount); ++delegateIndex) { + var delegate = findChild(listView.contentItem, "delegate" + delegateIndex); + verify(delegate); + + verify(delegate.displacement <= -delegateIndex, "Delegate at index " + delegateIndex + " has a displacement of " + + delegate.displacement + " when it should be less than or equal to " + -delegateIndex); + verify(delegate.displacement > -delegateIndex - 0.1, "Delegate at index 0 has a displacement of " + + delegate.displacement + " when it should be greater than ~ " + -delegateIndex - 0.1); + } + + changed = true; + } + } + + // Sanity check that something was actually tested. + verify(changed); + + mouseRelease(tumbler, tumblerXCenter(), tumbler.topPadding); + } + + property Component objectNameDelegate: Text { + objectName: "delegate" + index + text: modelData + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + } + + function test_visibleItemCount_data() { + var data = [ + // e.g. {0: 2} = {delegate index: y pos / delegate height} + // Skip item at index 3, because it's out of view. + { model: 6, visibleItemCount: 5, expectedYPositions: {0: 2, 1: 3, 2: 4, 4: 0} }, + { model: 5, visibleItemCount: 3, expectedYPositions: {0: 1, 1: 2, 4: 0} }, + // Takes up the whole view. + { model: 2, visibleItemCount: 1, expectedYPositions: {0: 0} }, + ]; + + for (var i = 0; i < data.length; ++i) { + data[i].tag = "items=" + data[i].model + ", visibleItemCount=" + data[i].visibleItemCount; + } + return data; + } + + function test_visibleItemCount(data) { + createTumbler(); + + tumbler.delegate = objectNameDelegate; + tumbler.visibleItemCount = data.visibleItemCount; + + tumbler.model = data.model; + compare(tumbler.count, data.model); + + for (var delegateIndex = 0; delegateIndex < data.visibleItemCount; ++delegateIndex) { + if (data.expectedYPositions.hasOwnProperty(delegateIndex)) { + var delegate = findChild(tumblerView, "delegate" + delegateIndex); + verify(delegate, "Delegate found at index " + delegateIndex); + var expectedYPos = data.expectedYPositions[delegateIndex] * tumblerDelegateHeight; + compare(delegate.mapToItem(tumbler.contentItem, 0, 0).y, expectedYPos); + } + } + } + + property Component wrongDelegateTypeComponent: QtObject { + property real displacement: Tumbler.displacement + } + + property Component noParentDelegateComponent: Item { + property real displacement: Tumbler.displacement + } + + function test_attachedProperties() { + tumbler = createTemporaryObject(tumblerComponent, testCase); + verify(tumbler); + + // TODO: crashes somewhere in QML's guts +// tumbler.model = 5; +// tumbler.delegate = wrongDelegateTypeComponent; +// ignoreWarning("Attached properties of Tumbler must be accessed from within a delegate item"); +// // Cause displacement to be changed. The warning isn't triggered if we don't do this. +// tumbler.contentItem.offset += 1; + + ignoreWarning("Tumbler: attached properties must be accessed through a delegate item that has a parent"); + createTemporaryObject(noParentDelegateComponent, null); + + ignoreWarning("Tumbler: attempting to access attached property on item without an \"index\" property"); + var object = createTemporaryObject(noParentDelegateComponent, testCase); + verify(object); + } + + property Component paddingDelegate: Text { + objectName: "delegate" + index + text: modelData + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + + Rectangle { + anchors.fill: parent + color: "transparent" + border.color: "red" + border.width: 1 + } + } + + function test_padding_data() { + var data = []; + + data.push({ padding: 0 }); + data.push({ padding: 10 }); + data.push({ left: 10, top: 10 }); + data.push({ right: 10, bottom: 10 }); + + for (var i = 0; i < data.length; ++i) { + var tag = ""; + + if (data[i].padding !== undefined) + tag += "padding: " + data[i].padding + " "; + if (data[i].left !== undefined) + tag += "left: " + data[i].left + " "; + if (data[i].right !== undefined) + tag += "right: " + data[i].right + " "; + if (data[i].top !== undefined) + tag += "top: " + data[i].top + " "; + if (data[i].bottom !== undefined) + tag += "bottom: " + data[i].bottom + " "; + tag = tag.slice(0, -1); + + data[i].tag = tag; + } + + return data; + } + + function test_padding(data) { + createTumbler(); + + tumbler.delegate = paddingDelegate; + tumbler.model = 5; + compare(tumbler.padding, 0); + compare(tumbler.leftPadding, 0); + compare(tumbler.rightPadding, 0); + compare(tumbler.topPadding, 0); + compare(tumbler.bottomPadding, 0); + compare(tumbler.contentItem.x, 0); + compare(tumbler.contentItem.y, 0); + + if (data.padding !== undefined) + tumbler.padding = data.padding; + if (data.left !== undefined) + tumbler.leftPadding = data.left; + if (data.right !== undefined) + tumbler.rightPadding = data.right; + if (data.top !== undefined) + tumbler.topPadding = data.top; + if (data.bottom !== undefined) + tumbler.bottomPadding = data.bottom; + + compare(tumbler.availableWidth, tumbler.implicitWidth - tumbler.leftPadding - tumbler.rightPadding); + compare(tumbler.availableHeight, tumbler.implicitHeight - tumbler.topPadding - tumbler.bottomPadding); + compare(tumbler.contentItem.x, tumbler.leftPadding); + compare(tumbler.contentItem.y, tumbler.topPadding); + + var pathView = tumbler.contentItem; + var expectedDelegateHeight = tumbler.availableHeight / tumbler.visibleItemCount; + var itemIndicesInVisualOrder = [4, 0, 1]; + for (var i = 0; i < itemIndicesInVisualOrder.length; ++i) { + var delegate = findChild(pathView, "delegate" + itemIndicesInVisualOrder[i]); + verify(delegate, "Couldn't find delegate at index " + itemIndicesInVisualOrder[i] + + " (iteration " + i + " out of " + (pathView.children.length - 1) + ")"); + + compare(delegate.width, tumbler.availableWidth); + compare(delegate.height, expectedDelegateHeight); + + var expectedY = tumbler.topPadding + i * expectedDelegateHeight; + var mappedPos = delegate.mapToItem(null, delegate.width / 2, 0); + fuzzyCompare(mappedPos.y, expectedY, 0.5, + "Tumbler's PathView delegate at index " + itemIndicesInVisualOrder[i] + + " should have a y pos of " + expectedY + ", but it's actually " + mappedPos.y.toFixed(20)); + + var expectedX = tumbler.leftPadding; + compare(delegate.mapToItem(null, 0, 0).x, expectedX, + "Tumbler's PathView delegate at index " + itemIndicesInVisualOrder[i] + + " should have a x pos of " + expectedX + ", but it's actually " + mappedPos.x.toFixed(20)); + } + + // Force new items to be created, as there was a bug where the path was correct until this happened. + compare(tumblerView.offset, 0); + ++tumbler.currentIndex; + tryCompare(tumblerView, "offset", 4, tumblerView.highlightMoveDuration * 2); + } + + function test_moving_data() { + return [ + { tag: "wrap:true", wrap: true }, + { tag: "wrap:false", wrap: false } + ] + } + + function test_moving(data) { + createTumbler({wrap: data.wrap, model: 5}) + compare(tumbler.wrap, data.wrap) + compare(tumbler.moving, false) + + waitForRendering(tumbler) + + mousePress(tumbler, tumbler.width / 2, tumbler.height / 2, Qt.LeftButton) + compare(tumbler.moving, false) + + for (var y = tumbler.height / 2; y >= tumbler.height / 4; y -= 10) + mouseMove(tumbler, tumbler.width / 2, y, 1) + compare(tumbler.moving, true) + + mouseRelease(tumbler, tumbler.width / 2, tumbler.height / 4, Qt.LeftButton) + compare(tumbler.moving, true) + tryCompare(tumbler, "moving", false) + } + + Component { + id: qtbug61374Component + + Row { + property alias tumbler: tumbler + property alias label: label + + Component.onCompleted: { + tumbler.currentIndex = 2 + } + + Tumbler { + id: tumbler + model: 5 + // ... + } + + Label { + id: label + text: tumbler.currentItem.text + } + } + } + + function test_qtbug61374() { + var row = createTemporaryObject(qtbug61374Component, testCase); + verify(row); + + var tumbler = row.tumbler; + tryCompare(tumbler, "currentIndex", 2); + + tumblerView = findView(tumbler); + + var label = row.label; + compare(label.text, "2"); + } + + function test_positionViewAtIndex_data() { + return [ + // Should be 20, 21, ... but there is a documented limitation for this in positionViewAtIndex()'s docs. + { tag: "wrap=true, mode=Beginning", wrap: true, mode: Tumbler.Beginning, expectedVisibleIndices: [21, 22, 23, 24, 25] }, + { tag: "wrap=true, mode=Center", wrap: true, mode: Tumbler.Center, expectedVisibleIndices: [18, 19, 20, 21, 22] }, + { tag: "wrap=true, mode=End", wrap: true, mode: Tumbler.End, expectedVisibleIndices: [16, 17, 18, 19, 20] }, + // Same as Beginning; should start at 20. + { tag: "wrap=true, mode=Contain", wrap: true, mode: Tumbler.Contain, expectedVisibleIndices: [21, 22, 23, 24, 25] }, + { tag: "wrap=true, mode=SnapPosition", wrap: true, mode: Tumbler.SnapPosition, expectedVisibleIndices: [18, 19, 20, 21, 22] }, + { tag: "wrap=false, mode=Beginning", wrap: false, mode: Tumbler.Beginning, expectedVisibleIndices: [20, 21, 22, 23, 24] }, + { tag: "wrap=false, mode=Center", wrap: false, mode: Tumbler.Center, expectedVisibleIndices: [18, 19, 20, 21, 22] }, + { tag: "wrap=false, mode=End", wrap: false, mode: Tumbler.End, expectedVisibleIndices: [16, 17, 18, 19, 20] }, + { tag: "wrap=false, mode=Visible", wrap: false, mode: Tumbler.Visible, expectedVisibleIndices: [16, 17, 18, 19, 20] }, + { tag: "wrap=false, mode=Contain", wrap: false, mode: Tumbler.Contain, expectedVisibleIndices: [16, 17, 18, 19, 20] }, + { tag: "wrap=false, mode=SnapPosition", wrap: false, mode: Tumbler.SnapPosition, expectedVisibleIndices: [18, 19, 20, 21, 22] } + ] + } + + function test_positionViewAtIndex(data) { + createTumbler({ wrap: data.wrap, model: 40, visibleItemCount: 5 }) + compare(tumbler.wrap, data.wrap) + + waitForRendering(tumbler) + + tumbler.positionViewAtIndex(20, data.mode) + tryCompare(tumbler, "moving", false) + + compare(tumbler.visibleItemCount, 5) + for (var i = 0; i < 5; ++i) { + // Find the item through its text, as that's easier than child/itemAt(). + var text = data.expectedVisibleIndices[i].toString() + var item = findDelegateWithText(tumblerView, text) + verify(item, "found no item with text \"" + text + "\"") + compare(item.text, data.expectedVisibleIndices[i].toString()) + + // Ensure that it's at the position we expect. + var expectedPos = itemTopLeftPos(i) + var actualPos = testCase.mapFromItem(item, 0, 0) + compare(actualPos.x, expectedPos.x, "expected delegate with text " + item.text + + " to have an x pos of " + expectedPos.x + " but it was " + actualPos.x) + compare(actualPos.y, expectedPos.y, "expected delegate with text " + item.text + + " to have an y pos of " + expectedPos.y + " but it was " + actualPos.y) + } + } + + Component { + id: setCurrentIndexOnImperativeModelChangeComponent + + Tumbler { + onModelChanged: currentIndex = model - 2 + } + } + + function test_setCurrentIndexOnImperativeModelChange() { + var tumbler = createTemporaryObject(setCurrentIndexOnImperativeModelChangeComponent, testCase); + verify(tumbler); + + tumbler.model = 4 + compare(tumbler.count, 4); + tumblerView = findView(tumbler); + tryCompare(tumblerView, "count", 4); + + // 4 - 2 = 2 + compare(tumbler.currentIndex, 2); + + ++tumbler.model; + compare(tumbler.count, 5); + compare(tumbler.wrap, true); + tumblerView = findView(tumbler); + tryCompare(tumblerView, "count", 5); + // 5 - 2 = 3 + compare(tumbler.currentIndex, 3); + } + + Component { + id: setCurrentIndexOnDeclarativeModelChangeComponent + + Item { + property alias tumbler: tumbler + + property int setting: 4 + + Tumbler { + id: tumbler + model: setting + onModelChanged: currentIndex = model - 2 + } + } + } + + function test_setCurrentIndexOnDeclarativeModelChange() { + var root = createTemporaryObject(setCurrentIndexOnDeclarativeModelChangeComponent, testCase); + verify(root); + + var tumbler = root.tumbler; + compare(tumbler.count, 4); + compare(tumbler.wrap, false); + tumblerView = findView(tumbler); + tryCompare(tumblerView, "count", 4); + // 4 - 2 = 2 + compare(tumbler.currentIndex, 2); + + ++root.setting; + compare(tumbler.count, 5); + compare(tumbler.wrap, true); + tumblerView = findView(tumbler); + tryCompare(tumblerView, "count", 5); + // 5 - 2 = 3 + compare(tumbler.currentIndex, 3); + } + + function test_displacementAfterResizing() { + createTumbler({ + width: 200, + wrap: false, + delegate: displacementDelegate, + model: 30, + visibleItemCount: 7, + currentIndex: 15 + }) + + var delegate = findChild(tumblerView, "delegate15") + verify(delegate) + + tryCompare(delegate, "displacement", 0) + + // Resizing the Tumbler shouldn't affect the displacement. + tumbler.height *= 1.4 + tryCompare(delegate, "displacement", 0) + } + + //QTBUG-84426 + Component { + id: initialCurrentIndexTumbler + + Tumbler { + anchors.centerIn: parent + width: 60 + height: 200 + delegate: Text {text: modelData} + model: 10 + currentIndex: 4 + } + } + + function test_initialCurrentIndex() { + var tumbler = createTemporaryObject(initialCurrentIndexTumbler, testCase, {wrap: true}); + compare(tumbler.currentIndex, 4); + tumbler = createTemporaryObject(initialCurrentIndexTumbler, testCase, {wrap: false}); + compare(tumbler.currentIndex, 4); + } +} |