From 2df9abf7047afbe20b19d156ac37b58d9b047575 Mon Sep 17 00:00:00 2001 From: Bea Lam Date: Thu, 9 Feb 2012 17:59:44 +1000 Subject: Built-in transition support for ListView & GridView ListView and GridView can now be assigned transitions to be run when: - Populating the view (when initially setting the model / resetting) - Adding items - Removing items - Moving items The ViewTransition attached object can be used from within a transition declaration to access various information about the items that are being transitioned. Task-number: QTBUG-21504 Change-Id: Ie5c75ea511c8b15acc3f06fccf19abe34d3677f9 Reviewed-by: Martin Jones --- doc/src/images/viewtransitions-basic.gif | Bin 0 -> 49932 bytes doc/src/images/viewtransitions-delayedbyindex.gif | Bin 0 -> 69623 bytes .../images/viewtransitions-intermediatemove.gif | Bin 0 -> 109530 bytes doc/src/images/viewtransitions-interruptedbad.gif | Bin 0 -> 30112 bytes doc/src/images/viewtransitions-interruptedgood.gif | Bin 0 -> 38076 bytes doc/src/images/viewtransitions-pathanim.gif | Bin 0 -> 43741 bytes doc/src/images/viewtransitions-scriptactionbad.gif | Bin 0 -> 49200 bytes .../viewtransitions/viewtransitions-basic.qml | 70 ++ .../viewtransitions-delayedbyindex.qml | 78 ++ .../viewtransitions-intermediatemove.qml | 90 ++ .../viewtransitions-interruptedgood.qml | 74 ++ .../viewtransitions/viewtransitions-pathanim.qml | 105 ++ .../viewtransitions-scriptactionbad.qml | 81 ++ .../viewtransitions-scriptactiongood.qml | 84 ++ doc/src/whatsnew.qdoc | 5 +- src/quick/items/qquickgridview.cpp | 416 +++++++- src/quick/items/qquickitemsmodule.cpp | 1 + src/quick/items/qquickitemview.cpp | 1126 +++++++++++++++++++- src/quick/items/qquickitemview_p.h | 86 +- src/quick/items/qquickitemview_p_p.h | 139 ++- src/quick/items/qquicklistview.cpp | 417 +++++++- .../qquickgridview/data/addTransitions.qml | 129 +++ .../qquickgridview/data/moveTransitions.qml | 143 +++ .../qquickgridview/data/multipleTransitions.qml | 123 +++ .../qquickgridview/data/populateTransitions.qml | 103 ++ .../qquickgridview/data/removeTransitions.qml | 146 +++ .../qtquick2/qquickgridview/tst_qquickgridview.cpp | 977 ++++++++++++++++- .../qquicklistview/data/addTransitions.qml | 134 +++ .../qquicklistview/data/moveTransitions.qml | 141 +++ .../qquicklistview/data/multipleTransitions.qml | 121 +++ .../qquicklistview/data/populateTransitions.qml | 102 ++ .../qquicklistview/data/removeTransitions.qml | 144 +++ .../qtquick2/qquicklistview/tst_qquicklistview.cpp | 950 ++++++++++++++++- tests/auto/qtquick2/shared/viewtestutil.cpp | 148 +++ tests/auto/qtquick2/shared/viewtestutil.h | 48 +- tests/auto/qtquick2/shared/visualtestutil.h | 10 + 36 files changed, 6039 insertions(+), 152 deletions(-) create mode 100644 doc/src/images/viewtransitions-basic.gif create mode 100644 doc/src/images/viewtransitions-delayedbyindex.gif create mode 100644 doc/src/images/viewtransitions-intermediatemove.gif create mode 100644 doc/src/images/viewtransitions-interruptedbad.gif create mode 100644 doc/src/images/viewtransitions-interruptedgood.gif create mode 100644 doc/src/images/viewtransitions-pathanim.gif create mode 100644 doc/src/images/viewtransitions-scriptactionbad.gif create mode 100644 doc/src/snippets/declarative/viewtransitions/viewtransitions-basic.qml create mode 100644 doc/src/snippets/declarative/viewtransitions/viewtransitions-delayedbyindex.qml create mode 100644 doc/src/snippets/declarative/viewtransitions/viewtransitions-intermediatemove.qml create mode 100644 doc/src/snippets/declarative/viewtransitions/viewtransitions-interruptedgood.qml create mode 100644 doc/src/snippets/declarative/viewtransitions/viewtransitions-pathanim.qml create mode 100644 doc/src/snippets/declarative/viewtransitions/viewtransitions-scriptactionbad.qml create mode 100644 doc/src/snippets/declarative/viewtransitions/viewtransitions-scriptactiongood.qml create mode 100644 tests/auto/qtquick2/qquickgridview/data/addTransitions.qml create mode 100644 tests/auto/qtquick2/qquickgridview/data/moveTransitions.qml create mode 100644 tests/auto/qtquick2/qquickgridview/data/multipleTransitions.qml create mode 100644 tests/auto/qtquick2/qquickgridview/data/populateTransitions.qml create mode 100644 tests/auto/qtquick2/qquickgridview/data/removeTransitions.qml create mode 100644 tests/auto/qtquick2/qquicklistview/data/addTransitions.qml create mode 100644 tests/auto/qtquick2/qquicklistview/data/moveTransitions.qml create mode 100644 tests/auto/qtquick2/qquicklistview/data/multipleTransitions.qml create mode 100644 tests/auto/qtquick2/qquicklistview/data/populateTransitions.qml create mode 100644 tests/auto/qtquick2/qquicklistview/data/removeTransitions.qml diff --git a/doc/src/images/viewtransitions-basic.gif b/doc/src/images/viewtransitions-basic.gif new file mode 100644 index 0000000000..b2a6a618c0 Binary files /dev/null and b/doc/src/images/viewtransitions-basic.gif differ diff --git a/doc/src/images/viewtransitions-delayedbyindex.gif b/doc/src/images/viewtransitions-delayedbyindex.gif new file mode 100644 index 0000000000..4ece2a2ffb Binary files /dev/null and b/doc/src/images/viewtransitions-delayedbyindex.gif differ diff --git a/doc/src/images/viewtransitions-intermediatemove.gif b/doc/src/images/viewtransitions-intermediatemove.gif new file mode 100644 index 0000000000..e826183b19 Binary files /dev/null and b/doc/src/images/viewtransitions-intermediatemove.gif differ diff --git a/doc/src/images/viewtransitions-interruptedbad.gif b/doc/src/images/viewtransitions-interruptedbad.gif new file mode 100644 index 0000000000..d1f88f99bb Binary files /dev/null and b/doc/src/images/viewtransitions-interruptedbad.gif differ diff --git a/doc/src/images/viewtransitions-interruptedgood.gif b/doc/src/images/viewtransitions-interruptedgood.gif new file mode 100644 index 0000000000..1d59db387e Binary files /dev/null and b/doc/src/images/viewtransitions-interruptedgood.gif differ diff --git a/doc/src/images/viewtransitions-pathanim.gif b/doc/src/images/viewtransitions-pathanim.gif new file mode 100644 index 0000000000..e6bc737456 Binary files /dev/null and b/doc/src/images/viewtransitions-pathanim.gif differ diff --git a/doc/src/images/viewtransitions-scriptactionbad.gif b/doc/src/images/viewtransitions-scriptactionbad.gif new file mode 100644 index 0000000000..9e618d911c Binary files /dev/null and b/doc/src/images/viewtransitions-scriptactionbad.gif differ diff --git a/doc/src/snippets/declarative/viewtransitions/viewtransitions-basic.qml b/doc/src/snippets/declarative/viewtransitions/viewtransitions-basic.qml new file mode 100644 index 0000000000..8a05491ee1 --- /dev/null +++ b/doc/src/snippets/declarative/viewtransitions/viewtransitions-basic.qml @@ -0,0 +1,70 @@ +/**************************************************************************** +** +** Copyright (C) 2012 Nokia Corporation and/or its subsidiary(-ies). +** Contact: http://www.qt-project.org/ +** +** This file is part of the documentation of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:BSD$ +** 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 Nokia Corporation and its Subsidiary(-ies) nor +** the names of its contributors may be used to endorse or promote +** products derived from this software without specific prior written +** permission. +** +** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +import QtQuick 2.0 + +//! [0] +ListView { + width: 240; height: 320 + model: ListModel {} + + delegate: Rectangle { + width: 100; height: 30 + border.width: 1 + color: "lightsteelblue" + Text { + anchors.centerIn: parent + text: name + } + } + + add: Transition { + NumberAnimation { property: "opacity"; from: 0; to: 1.0; duration: 400 } + NumberAnimation { property: "scale"; from: 0; to: 1.0; duration: 400 } + } + + addDisplaced: Transition { + NumberAnimation { properties: "x,y"; duration: 400; easing.type: Easing.OutBounce } + } + + focus: true + Keys.onSpacePressed: model.insert(0, { "name": "Item " + model.count }) +} +//! [0] diff --git a/doc/src/snippets/declarative/viewtransitions/viewtransitions-delayedbyindex.qml b/doc/src/snippets/declarative/viewtransitions/viewtransitions-delayedbyindex.qml new file mode 100644 index 0000000000..79d00d2d1c --- /dev/null +++ b/doc/src/snippets/declarative/viewtransitions/viewtransitions-delayedbyindex.qml @@ -0,0 +1,78 @@ +/**************************************************************************** +** +** Copyright (C) 2012 Nokia Corporation and/or its subsidiary(-ies). +** Contact: http://www.qt-project.org/ +** +** This file is part of the documentation of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:BSD$ +** 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 Nokia Corporation and its Subsidiary(-ies) nor +** the names of its contributors may be used to endorse or promote +** products derived from this software without specific prior written +** permission. +** +** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +import QtQuick 2.0 + +ListView { + width: 240; height: 320 + model: ListModel {} + + delegate: Rectangle { + width: 100; height: 30 + border.width: 1 + color: "lightsteelblue" + Text { + anchors.centerIn: parent + text: name + } + } + + add: Transition { + NumberAnimation { property: "opacity"; from: 0; to: 1.0; duration: 400 } + NumberAnimation { property: "scale"; from: 0; to: 1.0; duration: 400 } + } + +//! [0] + addDisplaced: Transition { + id: addDispTrans + SequentialAnimation { + PauseAnimation { + duration: (addDispTrans.ViewTransition.index - + addDispTrans.ViewTransition.targetIndexes[0]) * 100 + } + NumberAnimation { properties: "x,y"; duration: 400; easing.type: Easing.OutBounce } + } + } +//! [0] + + focus: true + Keys.onSpacePressed: model.insert(0, { "name": "Item " + model.count }) +} + diff --git a/doc/src/snippets/declarative/viewtransitions/viewtransitions-intermediatemove.qml b/doc/src/snippets/declarative/viewtransitions/viewtransitions-intermediatemove.qml new file mode 100644 index 0000000000..a43d3a8b7f --- /dev/null +++ b/doc/src/snippets/declarative/viewtransitions/viewtransitions-intermediatemove.qml @@ -0,0 +1,90 @@ +/**************************************************************************** +** +** Copyright (C) 2012 Nokia Corporation and/or its subsidiary(-ies). +** Contact: http://www.qt-project.org/ +** +** This file is part of the documentation of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:BSD$ +** 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 Nokia Corporation and its Subsidiary(-ies) nor +** the names of its contributors may be used to endorse or promote +** products derived from this software without specific prior written +** permission. +** +** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +import QtQuick 2.0 + +ListView { + width: 240; height: 320 + model: ListModel {} + + delegate: Rectangle { + width: 100; height: 30 + border.width: 1 + color: "lightsteelblue" + Text { + anchors.centerIn: parent + text: name + } + } + + add: Transition { + NumberAnimation { property: "opacity"; from: 0; to: 1.0; duration: 400 } + NumberAnimation { property: "scale"; from: 0; to: 1.0; duration: 400 } + } + +//! [0] + addDisplaced: Transition { + id: addDispTrans + SequentialAnimation { + PauseAnimation { + duration: (addDispTrans.ViewTransition.index - + addDispTrans.ViewTransition.targetIndexes[0]) * 100 + } + ParallelAnimation { + NumberAnimation { + property: "x"; to: addDispTrans.ViewTransition.item.x + 20 + easing.type: Easing.OutQuad + } + NumberAnimation { + property: "y"; to: addDispTrans.ViewTransition.item.y + 50 + easing.type: Easing.OutQuad + } + } + NumberAnimation { properties: "x,y"; duration: 500; easing.type: Easing.OutBounce } + } + } + +//! [0] + + focus: true + Keys.onSpacePressed: model.insert(0, { "name": "Item " + model.count }) +} + + diff --git a/doc/src/snippets/declarative/viewtransitions/viewtransitions-interruptedgood.qml b/doc/src/snippets/declarative/viewtransitions/viewtransitions-interruptedgood.qml new file mode 100644 index 0000000000..1355268710 --- /dev/null +++ b/doc/src/snippets/declarative/viewtransitions/viewtransitions-interruptedgood.qml @@ -0,0 +1,74 @@ +/**************************************************************************** +** +** Copyright (C) 2012 Nokia Corporation and/or its subsidiary(-ies). +** Contact: http://www.qt-project.org/ +** +** This file is part of the documentation of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:BSD$ +** 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 Nokia Corporation and its Subsidiary(-ies) nor +** the names of its contributors may be used to endorse or promote +** products derived from this software without specific prior written +** permission. +** +** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +import QtQuick 2.0 + +ListView { + width: 240; height: 320 + model: ListModel {} + + delegate: Rectangle { + width: 100; height: 30 + border.width: 1 + color: "lightsteelblue" + Text { + anchors.centerIn: parent + text: name + } + } + + add: Transition { + NumberAnimation { property: "opacity"; from: 0; to: 1.0; duration: 400 } + NumberAnimation { property: "scale"; from: 0; to: 1.0; duration: 400 } + } + +//! [0] + addDisplaced: Transition { + NumberAnimation { properties: "x,y"; duration: 400; easing.type: Easing.OutBounce } + + // ensure opacity and scale values return to 1.0 + NumberAnimation { property: "opacity"; to: 1.0 } + NumberAnimation { property: "scale"; to: 1.0 } + } +//! [0] + + focus: true + Keys.onSpacePressed: model.insert(0, { "name": "Item " + model.count }) +} diff --git a/doc/src/snippets/declarative/viewtransitions/viewtransitions-pathanim.qml b/doc/src/snippets/declarative/viewtransitions/viewtransitions-pathanim.qml new file mode 100644 index 0000000000..3d075c4367 --- /dev/null +++ b/doc/src/snippets/declarative/viewtransitions/viewtransitions-pathanim.qml @@ -0,0 +1,105 @@ +/**************************************************************************** +** +** Copyright (C) 2012 Nokia Corporation and/or its subsidiary(-ies). +** Contact: http://www.qt-project.org/ +** +** This file is part of the documentation of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:BSD$ +** 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 Nokia Corporation and its Subsidiary(-ies) nor +** the names of its contributors may be used to endorse or promote +** products derived from this software without specific prior written +** permission. +** +** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +import QtQuick 2.0 + +ListView { + width: 240; height: 320 + model: ListModel {} + + delegate: Rectangle { + width: 100; height: 30 + border.width: 1 + color: "lightsteelblue" + Text { + anchors.centerIn: parent + text: name + } + } + +//! [0] + add: Transition { + id: addTrans + NumberAnimation { property: "opacity"; from: 0; to: 1.0; duration: 400 } + NumberAnimation { property: "scale"; from: 0; to: 1.0; duration: 400 } + + PathAnimation { + duration: 1000 + path: Path { + startX: addTrans.ViewTransition.destination.x + 200 + startY: addTrans.ViewTransition.destination.y + 200 + PathCurve { relativeX: -100; relativeY: -50 } + PathCurve { relativeX: 50; relativeY: -150 } + PathCurve { + x: addTrans.ViewTransition.destination.x + y: addTrans.ViewTransition.destination.y + } + } + } + } +//! [0] + + addDisplaced: Transition { + id: addDispTrans + SequentialAnimation { + PauseAnimation { + duration: (addDispTrans.ViewTransition.index - + addDispTrans.ViewTransition.targetIndexes[0]) * 100 + } + ParallelAnimation { + NumberAnimation { + property: "x"; to: addDispTrans.ViewTransition.item.x + 20 + easing.type: Easing.OutQuad + } + NumberAnimation { + property: "y"; to: addDispTrans.ViewTransition.item.y + 50 + easing.type: Easing.OutQuad + } + } + NumberAnimation { properties: "x,y"; duration: 500; easing.type: Easing.OutBounce } + } + } + + focus: true + Keys.onSpacePressed: model.insert(0, { "name": "Item " + model.count }) +} + + + diff --git a/doc/src/snippets/declarative/viewtransitions/viewtransitions-scriptactionbad.qml b/doc/src/snippets/declarative/viewtransitions/viewtransitions-scriptactionbad.qml new file mode 100644 index 0000000000..03348356e1 --- /dev/null +++ b/doc/src/snippets/declarative/viewtransitions/viewtransitions-scriptactionbad.qml @@ -0,0 +1,81 @@ +/**************************************************************************** +** +** Copyright (C) 2012 Nokia Corporation and/or its subsidiary(-ies). +** Contact: http://www.qt-project.org/ +** +** This file is part of the documentation of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:BSD$ +** 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 Nokia Corporation and its Subsidiary(-ies) nor +** the names of its contributors may be used to endorse or promote +** products derived from this software without specific prior written +** permission. +** +** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +import QtQuick 2.0 + +//! [0] +ListView { + width: 240; height: 320 + model: ListModel { + Component.onCompleted: { + for (var i=0; i<8; i++) + append({"name": i}) + } + } + + delegate: Rectangle { + width: 100; height: 30 + border.width: 1 + color: "lightsteelblue" + Text { + anchors.centerIn: parent + text: name + } + objectName: name + } + + move: Transition { + id: moveTrans + SequentialAnimation { + ColorAnimation { property: "color"; to: "yellow"; duration: 400 } + NumberAnimation { properties: "x,y"; duration: 800; easing.type: Easing.OutBack } + ScriptAction { script: moveTrans.ViewTransition.item.color = "lightsteelblue" } + } + } + + moveDisplaced: Transition { + NumberAnimation { properties: "x,y"; duration: 400; easing.type: Easing.OutBounce } + } + + focus: true + Keys.onSpacePressed: model.move(5, 1, 3) +} +//! [0] + diff --git a/doc/src/snippets/declarative/viewtransitions/viewtransitions-scriptactiongood.qml b/doc/src/snippets/declarative/viewtransitions/viewtransitions-scriptactiongood.qml new file mode 100644 index 0000000000..eda5c35479 --- /dev/null +++ b/doc/src/snippets/declarative/viewtransitions/viewtransitions-scriptactiongood.qml @@ -0,0 +1,84 @@ +/**************************************************************************** +** +** Copyright (C) 2012 Nokia Corporation and/or its subsidiary(-ies). +** Contact: http://www.qt-project.org/ +** +** This file is part of the documentation of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:BSD$ +** 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 Nokia Corporation and its Subsidiary(-ies) nor +** the names of its contributors may be used to endorse or promote +** products derived from this software without specific prior written +** permission. +** +** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +import QtQuick 2.0 + +ListView { + width: 240; height: 320 + model: ListModel { + Component.onCompleted: { + for (var i=0; i<8; i++) + append({"name": i}) + } + } + + delegate: Rectangle { + width: 100; height: 30 + border.width: 1 + color: "lightsteelblue" + Text { + anchors.centerIn: parent + text: name + } + objectName: name + } + +//! [0] + move: Transition { + id: moveTrans + SequentialAnimation { + ColorAnimation { property: "color"; to: "yellow"; duration: 400 } + NumberAnimation { properties: "x,y"; duration: 800; easing.type: Easing.OutBack } + //ScriptAction { script: moveTrans.ViewTransition.item.color = "lightsteelblue" } BAD! + + PropertyAction { property: "color"; value: "lightsteelblue" } + } + } +//! [0] + + moveDisplaced: Transition { + NumberAnimation { properties: "x,y"; duration: 400; easing.type: Easing.OutBounce } + } + + focus: true + Keys.onSpacePressed: model.move(5, 1, 3) +} + + diff --git a/doc/src/whatsnew.qdoc b/doc/src/whatsnew.qdoc index c7baa26047..34dfe21be5 100644 --- a/doc/src/whatsnew.qdoc +++ b/doc/src/whatsnew.qdoc @@ -134,7 +134,10 @@ a y parameter. PathView now has a \c currentItem property ListView and GridView: - - now have headerItem and footerItem properties (the instantiated header and footer items). + - Can now apply specified transitions whenever items are added, removed or moved in a view. + See the documentation for ViewTransition and ListView.add, ListView.addDisplaced, + GridView.add, GridView.addDisplaced etc. for details. + - These now have headerItem and footerItem properties (the instantiated header and footer items). - In RightToLeft layout the preferredHighlightBegin/End are now also reversed. ListView section.labelPositioning property added to allow keeping the current section label diff --git a/src/quick/items/qquickgridview.cpp b/src/quick/items/qquickgridview.cpp index 522c09ae54..39c7eab518 100644 --- a/src/quick/items/qquickgridview.cpp +++ b/src/quick/items/qquickgridview.cpp @@ -92,9 +92,9 @@ public: qreal rowPos() const { if (view->flow() == QQuickGridView::LeftToRight) - return item->y(); + return itemY(); else - return (view->effectiveLayoutDirection() == Qt::RightToLeft ? -view->cellWidth()-item->x() : item->x()); + return (view->effectiveLayoutDirection() == Qt::RightToLeft ? -view->cellWidth()-itemX() : itemX()); } qreal colPos() const { @@ -102,45 +102,53 @@ public: if (view->effectiveLayoutDirection() == Qt::RightToLeft) { qreal colSize = view->cellWidth(); int columns = view->width()/colSize; - return colSize * (columns-1) - item->x(); + return colSize * (columns-1) - itemX(); } else { - return item->x(); + return itemX(); } } else { - return item->y(); + return itemY(); } } qreal endRowPos() const { if (view->flow() == QQuickGridView::LeftToRight) { - return item->y() + view->cellHeight(); + return itemY() + view->cellHeight(); } else { if (view->effectiveLayoutDirection() == Qt::RightToLeft) - return -item->x(); + return -itemX(); else - return item->x() + view->cellWidth(); + return itemX() + view->cellWidth(); } } void setPosition(qreal col, qreal row) { + moveTo(pointForPosition(col, row)); + } + bool contains(qreal x, qreal y) const { + return (x >= itemX() && x < itemX() + view->cellWidth() && + y >= itemY() && y < itemY() + view->cellHeight()); + } + QQuickItemView *itemView() const { + return view; + } + + QQuickGridView *view; + +private: + QPointF pointForPosition(qreal col, qreal row) const { if (view->effectiveLayoutDirection() == Qt::RightToLeft) { if (view->flow() == QQuickGridView::LeftToRight) { int columns = view->width()/view->cellWidth(); - item->setPos(QPointF((view->cellWidth() * (columns-1) - col), row)); + return QPointF(view->cellWidth() * (columns-1) - col, row); } else { - item->setPos(QPointF(-view->cellWidth()-row, col)); + return QPointF(-view->cellWidth() - row, col); } } else { if (view->flow() == QQuickGridView::LeftToRight) - item->setPos(QPointF(col, row)); + return QPointF(col, row); else - item->setPos(QPointF(row, col)); + return QPointF(row, col); } } - bool contains(qreal x, qreal y) const { - return (x >= item->x() && x < item->x() + view->cellWidth() && - y >= item->y() && y < item->y() + view->cellHeight()); - } - - QQuickGridView *view; }; //---------------------------------------------------------------------------- @@ -173,6 +181,8 @@ public: virtual bool removeNonVisibleItems(qreal bufferFrom, qreal bufferTo); virtual FxViewItem *newViewItem(int index, QQuickItem *item); + virtual void initializeViewItem(FxViewItem *item); + virtual void repositionItemAt(FxViewItem *item, int index, qreal sizeBuffer); virtual void repositionPackageItemAt(QQuickItem *item, int index); virtual void resetFirstItemPosition(qreal pos = 0.0); virtual void adjustFirstItem(qreal forwards, qreal backwards, int changeBeforeVisible); @@ -183,7 +193,8 @@ public: virtual void setPosition(qreal pos); virtual void layoutVisibleItems(int fromModelIndex = 0); - virtual bool applyInsertionChange(const QDeclarativeChangeSet::Insert &insert, ChangeResult *changeResult, QList *addedItems); + virtual bool applyInsertionChange(const QDeclarativeChangeSet::Insert &insert, ChangeResult *changeResult, QList *addedItems, QList *movingIntoView); + virtual void translateAndTransitionItemsAfter(int afterModelIndex, const ChangeResult &insertionResult, const ChangeResult &removalResult); virtual bool needsRefillForAddedOrRemovedIndex(int index) const; virtual qreal headerSize() const; @@ -309,8 +320,11 @@ qreal QQuickGridViewPrivate::colPosAt(int modelIndex) const col = (columns - count + col) % columns; return col * colSize(); } else { - int count = columns - 1 - (modelIndex - visibleItems.last()->index - 1) % columns; - return static_cast(visibleItems.last())->colPos() - count * colSize(); + FxGridItemSG *lastItem = static_cast(visibleItems.last()); + int count = modelIndex - lastItem->index; + int col = lastItem->colPos() / colSize(); + col = (col + count) % columns; + return col * colSize(); } } return (modelIndex % columns) * colSize(); @@ -415,6 +429,15 @@ FxViewItem *QQuickGridViewPrivate::newViewItem(int modelIndex, QQuickItem *item) return new FxGridItemSG(item, q, false); } +void QQuickGridViewPrivate::initializeViewItem(FxViewItem *item) +{ + QQuickItemViewPrivate::initializeViewItem(item); + + // need to track current items that are animating + QQuickItemPrivate *itemPrivate = QQuickItemPrivate::get(item->item); + itemPrivate->addItemChangeListener(this, QQuickItemPrivate::Geometry); +} + bool QQuickGridViewPrivate::addVisibleItems(qreal fillFrom, qreal fillTo, bool doBuffer) { qreal colPos = colPosAt(visibleIndex); @@ -462,7 +485,8 @@ bool QQuickGridViewPrivate::addVisibleItems(qreal fillFrom, qreal fillTo, bool d #endif if (!(item = static_cast(createItem(modelIndex, doBuffer)))) break; - item->setPosition(colPos, rowPos); + if (!(usePopulateTransition && populateTransition)) // pos will be set by layoutVisibleItems() + item->setPosition(colPos, rowPos); item->item->setVisible(!doBuffer); visibleItems.append(item); if (++colNum >= columns) { @@ -499,7 +523,8 @@ bool QQuickGridViewPrivate::addVisibleItems(qreal fillFrom, qreal fillTo, bool d if (!(item = static_cast(createItem(visibleIndex-1, doBuffer)))) break; --visibleIndex; - item->setPosition(colPos, rowPos); + if (!(usePopulateTransition && populateTransition)) // pos will be set by layoutVisibleItems() + item->setPosition(colPos, rowPos); item->item->setVisible(!doBuffer); visibleItems.prepend(item); if (--colNum < 0) { @@ -529,7 +554,15 @@ bool QQuickGridViewPrivate::removeNonVisibleItems(qreal bufferFrom, qreal buffer if (item->index != -1) visibleIndex++; visibleItems.removeFirst(); - releaseItem(item); + if (item->transitionScheduledOrRunning()) { +#ifdef DEBUG_DELEGATE_LIFECYCLE + qDebug() << "\tnot releasing animating item:" << item->index << item->item->objectName(); +#endif + item->releaseAfterTransition = true; + releasePendingTransition.append(item); + } else { + releaseItem(item); + } changed = true; } while (visibleItems.count() > 1 @@ -541,7 +574,15 @@ bool QQuickGridViewPrivate::removeNonVisibleItems(qreal bufferFrom, qreal buffer qDebug() << "refill: remove last" << visibleIndex+visibleItems.count()-1; #endif visibleItems.removeLast(); - releaseItem(item); + if (item->transitionScheduledOrRunning()) { +#ifdef DEBUG_DELEGATE_LIFECYCLE + qDebug() << "\tnot releasing animating item:" << item->index << item->item->objectName(); +#endif + item->releaseAfterTransition = true; + releasePendingTransition.append(item); + } else { + releaseItem(item); + } changed = true; } @@ -567,7 +608,7 @@ void QQuickGridViewPrivate::layoutVisibleItems(int fromModelIndex) if (colPos != col * colSize()) { colPos = col * colSize(); firstItem->setPosition(colPos, rowPos); - firstItem->item->setVisible(rowPos + rowSize() >= from && rowPos <= to); + firstItem->setVisible(firstItem->rowPos() + rowSize() >= from && firstItem->rowPos() <= to); } for (int i = 1; i < visibleItems.count(); ++i) { FxGridItemSG *item = static_cast(visibleItems.at(i)); @@ -578,12 +619,18 @@ void QQuickGridViewPrivate::layoutVisibleItems(int fromModelIndex) colPos = col * colSize(); if (item->index >= fromModelIndex) { item->setPosition(colPos, rowPos); - item->item->setVisible(rowPos + rowSize() >= from && rowPos <= to); + item->setVisible(item->rowPos() + rowSize() >= from && item->rowPos() <= to); } } } } +void QQuickGridViewPrivate::repositionItemAt(FxViewItem *item, int index, qreal sizeBuffer) +{ + int count = sizeBuffer / rowSize(); + static_cast(item)->setPosition(colPosAt(index + count), rowPosAt(index + count)); +} + void QQuickGridViewPrivate::repositionPackageItemAt(QQuickItem *item, int index) { Q_Q(QQuickGridView); @@ -668,8 +715,8 @@ void QQuickGridViewPrivate::updateHighlight() bool strictHighlight = haveHighlightRange && highlightRange == QQuickGridView::StrictlyEnforceRange; if (currentItem && autoHighlight && highlight && (!strictHighlight || !pressed)) { // auto-update highlight - highlightXAnimator->to = currentItem->item->x(); - highlightYAnimator->to = currentItem->item->y(); + highlightXAnimator->to = currentItem->itemX(); + highlightYAnimator->to = currentItem->itemY(); highlight->item->setWidth(currentItem->item->width()); highlight->item->setHeight(currentItem->item->height()); @@ -797,7 +844,11 @@ void QQuickGridViewPrivate::initializeCurrentItem() { if (currentItem && currentIndex >= 0) { FxGridItemSG *gridItem = static_cast(currentItem); - gridItem->setPosition(colPosAt(currentIndex), rowPosAt(currentIndex)); + FxViewItem *actualItem = visibleItem(currentIndex); + + // don't reposition the item if it's about to be transitioned to another position + if ((!actualItem || !actualItem->transitionScheduledOrRunning())) + gridItem->setPosition(colPosAt(currentIndex), rowPosAt(currentIndex)); } } @@ -1144,17 +1195,17 @@ void QQuickGridView::setHighlightFollowsCurrentItem(bool autoHighlight) /*! \qmlattachedproperty bool QtQuick2::GridView::delayRemove - This attached property holds whether the delegate may be destroyed. - - It is attached to each instance of the delegate. + This attached property holds whether the delegate may be destroyed. It + is attached to each instance of the delegate. The default value is false. It is sometimes necessary to delay the destruction of an item - until an animation completes. - - The example below ensures that the animation completes before - the item is removed from the grid. + until an animation completes. The example delegate below ensures that the + animation completes before the item is removed from the list. \snippet doc/src/snippets/declarative/gridview/gridview.qml delayRemove + + If a \l remove transition has been specified, it will not be applied until + delayRemove is returned to \c false. */ /*! @@ -1165,6 +1216,9 @@ void QQuickGridView::setHighlightFollowsCurrentItem(bool autoHighlight) /*! \qmlattachedsignal QtQuick2::GridView::onRemove() This attached handler is called immediately before an item is removed from the view. + + If a \l remove transition has been specified, it is applied after + this signal handler is called, providing that delayRemove is false. */ @@ -1530,6 +1584,228 @@ void QQuickGridView::setSnapMode(SnapMode mode) \sa footer, headerItem */ +/*! + \qmlproperty Transition QtQuick2::GridView::populate + This property holds the transition to apply to items that are initially created for a + view. + + This transition is applied to all the items that are created when: + + \list + \o The view is first created + \o The view's \l model changes + \o The view's \l model is \l {QAbstractItemModel::reset}{reset}, if the model is a QAbstractItemModel subclass + \endlist + + For example, here is a view that specifies such a transition: + + \code + GridView { + ... + populate: Transition { + NumberAnimation { properties: "x,y"; duration: 1000 } + } + } + \endcode + + When the view is initialized, the view will create all the necessary items for the view, + then animate them to their correct positions within the view over one second. + + For more details and examples on how to use view transitions, see the ViewTransition + documentation. + + \sa add, ViewTransition +*/ + +/*! + \qmlproperty Transition QtQuick2::GridView::add + This property holds the transition to apply to items that are added within the view. + + The transition is applied to items that have been added to the visible area of the view. For + example, here is a view that specifies such a transition: + + \code + GridView { + ... + add: Transition { + NumberAnimation { properties: "x,y"; from: 100; duration: 1000 } + } + } + \endcode + + Whenever an item is added to the above view, the item will be animated from the position (100,100) + to its final x,y position within the view, over one second. The transition only applies to + the new items that are added to the view; it does not apply to the items below that are + displaced by the addition of the new items. To animate the displaced items, set the \l + addDisplaced property. + + For more details and examples on how to use view transitions, see the ViewTransition + documentation. + + \note This transition is not applied to the items that are created when the view is initially + populated, or when the view's \l model changes. In those cases, the \l populate transition is + applied instead. + + \sa addDisplaced, populate, ViewTransition +*/ + +/*! + \qmlproperty Transition QtQuick2::GridView::addDisplaced + This property holds the transition to apply to items in the view that are displaced by other + items that have been added to the view. + + The transition is applied to items that are currently visible and have been displaced by newly + added items. For example, here is a view that specifies such a transition: + + \code + GridView { + ... + addDisplaced: Transition { + NumberAnimation { properties: "x,y"; duration: 1000 } + } + } + \endcode + + Whenever an item is added to the above view, all items beneath the new item are displaced, causing + them to move down (or sideways, if horizontally orientated) within the view. As this + displacement occurs, the items' movement to their new x,y positions within the view will be + animated by a NumberAnimation over one second, as specified. This transition is not applied to + the new item that has been added to the view; to animate the added items, set the \l add + property. + + For more details and examples on how to use view transitions, see the ViewTransition + documentation. + + \note This transition is not applied to the items that are created when the view is initially + populated, or when the view's \l model changes. In those cases, the \l populate transition is + applied instead. + + \sa add, populate, ViewTransition +*/ +/*! + \qmlproperty Transition QtQuick2::GridView::move + This property holds the transition to apply to items in the view that are moved by a move + operation. + + The transition is applied to items that are moving within the view or are moving + into the view as a result of a move operation in the view's model. For example: + + \code + GridView { + ... + move: Transition { + NumberAnimation { properties: "x,y"; duration: 1000 } + } + } + \endcode + + Whenever an item is moved within the above view, the item will be animated to its new position in + the view over one second. The transition only applies to the items that are the subject of the + move operation in the model; it does not apply to the items below them that are displaced by + the move operation. To animate the displaced items, set the \l moveDisplaced property. + + For more details and examples on how to use view transitions, see the ViewTransition + documentation. + + \sa moveDisplaced, ViewTransition +*/ + +/*! + \qmlproperty Transition QtQuick2::GridView::moveDisplaced + This property holds the transition to apply to items in the view that are displaced by a + move operation in the view. + + The transition is applied to items that are currently visible and have been displaced following + a move operation in the view's model. For example, here is a view that specifies such a transition: + + \code + GridView { + ... + moveDisplaced: Transition { + NumberAnimation { properties: "x,y"; duration: 1000 } + } + } + \endcode + + Whenever an item moves within (or moves into) the above view, all items beneath it are + displaced, causing them to move upwards (or sideways, if horizontally orientated) within the + view. As this displacement occurs, the items' movement to their new x,y positions within the + view will be animated by a NumberAnimation over one second, as specified. This transition is + not applied to the item that are actually the subject of the move operation; to animate the + moved items, set the \l move property. + + For more details and examples on how to use view transitions, see the ViewTransition + documentation. + + \sa move, ViewTransition +*/ + +/*! + \qmlproperty Transition QtQuick2::GridView::remove + This property holds the transition to apply to items that are removed from the view. + + The transition is applied to items that have been removed from the visible area of the view. For + example: + + \code + GridView { + ... + remove: Transition { + ParallelAnimation { + NumberAnimation { property: "opacity"; to: 0; duration: 1000 } + NumberAnimation { properties: "x,y"; to: 100; duration: 1000 } + } + } + } + \endcode + + Whenever an item is removed from the above view, the item will be animated to the position (100,100) + over one second, and in parallel will also change its opacity to 0. The transition + only applies to the items that are removed from the view; it does not apply to the items below + them that are displaced by the removal of the items. To animate the displaced items, set the \l + removeDisplaced property. + + Note that by the time the transition is applied, the item has already been removed from the + model; any references to the model data for the removed index will not be valid. + + Additionally, if the \l delayRemove attached property has been set for a delegate item, the + remove transition will not be applied until \l delayRemove becomes false again. + + For more details and examples on how to use view transitions, see the ViewTransition + documentation. + + \sa removeDisplaced, ViewTransition +*/ + +/*! + \qmlproperty Transition QtQuick2::GridView::removeDisplaced + This property holds the transition to apply to items in the view that are displaced by the + removal of other items in the view. + + The transition is applied to items that are currently visible and have been displaced by + the removal of items. For example, here is a view that specifies such a transition: + + \code + GridView { + ... + removeDisplaced: Transition { + NumberAnimation { properties: "x,y"; duration: 1000 } + } + } + \endcode + + Whenever an item is removed from the above view, all items beneath it are displaced, causing + them to move upwards (or sideways, if horizontally orientated) within the view. As this + displacement occurs, the items' movement to their new x,y positions within the view will be + animated by a NumberAnimation over one second, as specified. This transition is not applied to + the item that has actually been removed from the view; to animate the removed items, set the + \l remove property. + + For more details and examples on how to use view transitions, see the ViewTransition + documentation. + + \sa remove, ViewTransition +*/ void QQuickGridView::viewportMoved() { Q_D(QQuickGridView); @@ -1583,9 +1859,9 @@ void QQuickGridView::viewportMoved() d->updateCurrent(idx); if (d->currentItem && static_cast(d->currentItem)->colPos() != static_cast(d->highlight)->colPos() && d->autoHighlight) { if (d->flow == LeftToRight) - d->highlightXAnimator->to = d->currentItem->item->x(); + d->highlightXAnimator->to = d->currentItem->itemX(); else - d->highlightYAnimator->to = d->currentItem->item->y(); + d->highlightYAnimator->to = d->currentItem->itemY(); } } } @@ -1775,7 +2051,7 @@ void QQuickGridView::moveCurrentIndexRight() } } -bool QQuickGridViewPrivate::applyInsertionChange(const QDeclarativeChangeSet::Insert &change, ChangeResult *insertResult, QList *addedItems) +bool QQuickGridViewPrivate::applyInsertionChange(const QDeclarativeChangeSet::Insert &change, ChangeResult *insertResult, QList *addedItems, QList *movingIntoView) { Q_Q(QQuickGridView); @@ -1831,8 +2107,13 @@ bool QQuickGridViewPrivate::applyInsertionChange(const QDeclarativeChangeSet::In // Update the indexes of the following visible items. for (int i = 0; i < visibleItems.count(); ++i) { FxViewItem *item = visibleItems.at(i); - if (item->index != -1 && item->index >= modelIndex) + if (item->index != -1 && item->index >= modelIndex) { item->index += count; + if (change.isMove()) + transitionNextReposition(item, FxViewItemTransitionManager::MoveTransition, false); + else + transitionNextReposition(item, FxViewItemTransitionManager::AddTransition, false); + } } int prevVisibleCount = visibleItems.count(); @@ -1845,7 +2126,7 @@ bool QQuickGridViewPrivate::applyInsertionChange(const QDeclarativeChangeSet::In while (i >= 0) { if (rowPos > from && insertionIdx < visibleIndex) { // item won't be visible, just note the size for repositioning - insertResult->changeBeforeVisible++; + insertResult->countChangeBeforeVisible++; } else { // item is before first visible e.g. in cache buffer FxViewItem *item = 0; @@ -1858,8 +2139,12 @@ bool QQuickGridViewPrivate::applyInsertionChange(const QDeclarativeChangeSet::In item->item->setVisible(true); visibleItems.insert(insertionIdx, item); - if (!change.isMove()) + if (insertionIdx == 0) + insertResult->changedFirstItem = true; + if (!change.isMove()) { addedItems->append(item); + transitionNextReposition(item, FxViewItemTransitionManager::AddTransition, true); + } insertResult->sizeChangesBeforeVisiblePos += rowSize(); } @@ -1878,6 +2163,7 @@ bool QQuickGridViewPrivate::applyInsertionChange(const QDeclarativeChangeSet::In FxViewItem *item = 0; if (change.isMove() && (item = currentChanges.removedItems.take(change.moveKey(modelIndex + i)))) item->index = modelIndex + i; + bool newItem = !item; if (!item) item = createItem(modelIndex + i); if (!item) @@ -1887,8 +2173,15 @@ bool QQuickGridViewPrivate::applyInsertionChange(const QDeclarativeChangeSet::In visibleItems.insert(index, item); if (index == 0) insertResult->changedFirstItem = true; - if (!change.isMove()) + if (change.isMove()) { + // we know this is a move target, since move displaced items that are + // shuffled into view due to a move would be added in refill() + if (moveTransition && newItem) + movingIntoView->append(MovedItem(item, change.moveKey(item->index))); + } else { addedItems->append(item); + transitionNextReposition(item, FxViewItemTransitionManager::AddTransition, true); + } insertResult->sizeChangesAfterVisiblePos += rowSize(); if (++colNum >= columns) { @@ -1906,6 +2199,41 @@ bool QQuickGridViewPrivate::applyInsertionChange(const QDeclarativeChangeSet::In return visibleItems.count() > prevVisibleCount; } +void QQuickGridViewPrivate::translateAndTransitionItemsAfter(int afterModelIndex, const ChangeResult &insertionResult, const ChangeResult &removalResult) +{ + int markerItemIndex = -1; + for (int i=0; iindex == afterModelIndex) { + markerItemIndex = i; + break; + } + } + if (markerItemIndex < 0) + return; + + const qreal viewEndPos = isContentFlowReversed() ? -position() : position() + size(); + int countItemsRemoved = -(removalResult.sizeChangesAfterVisiblePos / rowSize()); + + // account for whether first item has changed if < 1 row was removed before visible + int changeBeforeVisible = insertionResult.countChangeBeforeVisible - removalResult.countChangeBeforeVisible; + if (changeBeforeVisible != 0) + countItemsRemoved += (changeBeforeVisible % columns) - (columns - 1); + + countItemsRemoved -= removalResult.countChangeAfterVisibleItems; + + for (int i=markerItemIndex+1; iposition() < viewEndPos; i++) { + FxGridItemSG *gridItem = static_cast(visibleItems[i]); + if (!gridItem->transitionScheduledOrRunning()) { + qreal origRowPos = gridItem->colPos(); + qreal origColPos = gridItem->rowPos(); + int indexDiff = gridItem->index - countItemsRemoved; + gridItem->setPosition((indexDiff % columns) * colSize(), (indexDiff / columns) * rowSize()); + transitionNextReposition(gridItem, FxViewItemTransitionManager::RemoveTransition, false); + gridItem->setPosition(origRowPos, origColPos); + } + } +} + bool QQuickGridViewPrivate::needsRefillForAddedOrRemovedIndex(int modelIndex) const { // If we add or remove items before visible items, a layout may be diff --git a/src/quick/items/qquickitemsmodule.cpp b/src/quick/items/qquickitemsmodule.cpp index 3d1e96da84..a4cfa26205 100644 --- a/src/quick/items/qquickitemsmodule.cpp +++ b/src/quick/items/qquickitemsmodule.cpp @@ -184,6 +184,7 @@ static void qt_quickitems_defineModule(const char *uri, int major, int minor) qmlRegisterUncreatableType(uri,major,minor,"KeyNavigation",QQuickKeyNavigationAttached::tr("KeyNavigation is only available via attached properties")); qmlRegisterUncreatableType(uri,major,minor,"Keys",QQuickKeysAttached::tr("Keys is only available via attached properties")); qmlRegisterUncreatableType(uri,major,minor,"LayoutMirroring", QQuickLayoutMirroringAttached::tr("LayoutMirroring is only available via attached properties")); + qmlRegisterUncreatableType(uri,major,minor,"ViewTransition",QQuickViewTransitionAttached::tr("ViewTransition is only available via attached properties")); qmlRegisterType(uri,major,minor,"PinchArea"); qmlRegisterType(uri,major,minor,"Pinch"); diff --git a/src/quick/items/qquickitemview.cpp b/src/quick/items/qquickitemview.cpp index f09be2b54f..87a428a87c 100644 --- a/src/quick/items/qquickitemview.cpp +++ b/src/quick/items/qquickitemview.cpp @@ -40,17 +40,26 @@ ****************************************************************************/ #include "qquickitemview_p_p.h" +#include QT_BEGIN_NAMESPACE FxViewItem::FxViewItem(QQuickItem *i, bool own) - : item(i), ownItem(own), index(-1) + : item(i), ownItem(own), index(-1), releaseAfterTransition(false) + , transition(0) + , nextTransitionType(FxViewItemTransitionManager::NoTransition) + , isTransitionTarget(false) + , nextTransitionToSet(false) { } FxViewItem::~FxViewItem() { + if (transition) + transition->m_item = 0; + delete transition; + if (ownItem && item) { item->setParentItem(0); item->deleteLater(); @@ -58,6 +67,273 @@ FxViewItem::~FxViewItem() } } +qreal FxViewItem::itemX() const +{ + if (nextTransitionType != FxViewItemTransitionManager::NoTransition) + return nextTransitionToSet ? nextTransitionTo.x() : item->x(); + else if (transition && transition->isActive()) + return transition->m_toPos.x(); + else + return item->x(); +} + +qreal FxViewItem::itemY() const +{ + // If item is transitioning to some pos, return that dest pos. + // If item was redirected to some new pos before the current transition finished, + // return that new pos. + if (nextTransitionType != FxViewItemTransitionManager::NoTransition) + return nextTransitionToSet ? nextTransitionTo.y() : item->y(); + else if (transition && transition->isActive()) + return transition->m_toPos.y(); + else + return item->y(); +} + +void FxViewItem::setVisible(bool visible) +{ + if (!visible && transitionScheduledOrRunning()) + return; + item->setVisible(visible); +} + +void FxViewItem::setNextTransition(FxViewItemTransitionManager::TransitionType type, bool isTargetItem) +{ + // Don't reset nextTransitionToSet - once it is set, it cannot be changed + // until the animation finishes since the itemX() and itemY() may be used + // to calculate positions for transitions for other items in the view. + nextTransitionType = type; + isTransitionTarget = isTargetItem; +} + +bool FxViewItem::transitionScheduledOrRunning() const +{ + return (transition && transition->isActive()) + || nextTransitionType != FxViewItemTransitionManager::NoTransition; +} + +bool FxViewItem::prepareTransition(const QRectF &viewBounds) +{ + bool doTransition = false; + + switch (nextTransitionType) { + case FxViewItemTransitionManager::NoTransition: + { + return false; + } + case FxViewItemTransitionManager::PopulateTransition: + { + return true; + } + case FxViewItemTransitionManager::AddTransition: + case FxViewItemTransitionManager::RemoveTransition: + // For Add targets, do transition if item is moving into visible area + // For Remove targets, do transition if item is currently in visible area + if (isTransitionTarget) { + doTransition = (nextTransitionType == FxViewItemTransitionManager::AddTransition) + ? viewBounds.intersects(QRectF(nextTransitionTo.x(), nextTransitionTo.y(), item->width(), item->height())) + : viewBounds.intersects(QRectF(item->x(), item->y(), item->width(), item->height())); + if (!doTransition) + item->setPos(nextTransitionTo); + } else { + if (viewBounds.intersects(QRectF(item->x(), item->y(), item->width(), item->height())) + || viewBounds.intersects(QRectF(nextTransitionTo.x(), nextTransitionTo.y(), item->width(), item->height()))) { + doTransition = (nextTransitionTo != item->pos()); + } else { + item->setPos(nextTransitionTo); + } + } + break; + case FxViewItemTransitionManager::MoveTransition: + // do transition if moving from or into visible area + if (nextTransitionTo != item->pos()) { + doTransition = viewBounds.intersects(QRectF(item->x(), item->y(), item->width(), item->height())) + || viewBounds.intersects(QRectF(nextTransitionTo.x(), nextTransitionTo.y(), item->width(), item->height())); + if (!doTransition) + item->setPos(nextTransitionTo); + } + break; + } + + if (!doTransition) + resetTransitionData(); + return doTransition; +} + +void FxViewItem::startTransition() +{ + if (nextTransitionType == FxViewItemTransitionManager::NoTransition) + return; + + if (!transition || transition->m_type != nextTransitionType || transition->m_type != isTransitionTarget) { + delete transition; + transition = new FxViewItemTransitionManager; + } + + // if item is not already moving somewhere, set it to not move anywhere + // so that removed items do not move to the default (0,0) + if (!nextTransitionToSet) + moveTo(item->pos()); + + transition->startTransition(this, nextTransitionType, nextTransitionTo, isTransitionTarget); + nextTransitionType = FxViewItemTransitionManager::NoTransition; +} + +void FxViewItem::stopTransition() +{ + if (transition) { + transition->cancel(); + delete transition; + transition = 0; + } + resetTransitionData(); + finishedTransition(); +} + +void FxViewItem::finishedTransition() +{ + nextTransitionToSet = false; + nextTransitionTo = QPointF(); + + if (releaseAfterTransition) { + QQuickItemViewPrivate *vp = static_cast(QObjectPrivate::get(itemView())); + vp->releasePendingTransition.removeOne(this); + vp->releaseItem(this); + } +} + +void FxViewItem::resetTransitionData() +{ + nextTransitionType = FxViewItemTransitionManager::NoTransition; + isTransitionTarget = false; + nextTransitionTo = QPointF(); + nextTransitionToSet = false; +} + +bool FxViewItem::isPendingRemoval() const +{ + if (nextTransitionType == FxViewItemTransitionManager::RemoveTransition) + return isTransitionTarget; + if (transition && transition->isActive() && transition->m_type == FxViewItemTransitionManager::RemoveTransition) + return transition->m_isTarget; + return false; +} + +void FxViewItem::moveTo(const QPointF &pos) +{ + if (transitionScheduledOrRunning()) { + nextTransitionTo = pos; + nextTransitionToSet = true; + } else { + item->setPos(pos); + } +} + + +FxViewItemTransitionManager::FxViewItemTransitionManager() + : m_active(false), m_item(0), m_type(FxViewItemTransitionManager::NoTransition), m_isTarget(false) +{ +} + +FxViewItemTransitionManager::~FxViewItemTransitionManager() +{ +} + +bool FxViewItemTransitionManager::isActive() const +{ + return m_active; +} + +void FxViewItemTransitionManager::startTransition(FxViewItem *item, FxViewItemTransitionManager::TransitionType type, const QPointF &to, bool isTargetItem) +{ + if (!item) { + qWarning("startTransition(): invalid item"); + return; + } + + QQuickItemViewPrivate *vp = static_cast(QObjectPrivate::get(item->itemView())); + + QDeclarativeTransition *trans = 0; + switch (type) { + case NoTransition: + break; + case PopulateTransition: + trans = vp->populateTransition; + break; + case AddTransition: + trans = isTargetItem ? vp->addTransition : vp->addDisplacedTransition; + break; + case MoveTransition: + trans = isTargetItem ? vp->moveTransition : vp->moveDisplacedTransition; + break; + case RemoveTransition: + trans = isTargetItem ? vp->removeTransition : vp->removeDisplacedTransition; + break; + } + + if (!trans) { + qWarning("QQuickItemView: invalid view transition!"); + return; + } + + m_active = true; + m_item = item; + m_toPos = to; + m_type = type; + m_isTarget = isTargetItem; + + QQuickViewTransitionAttached *attached = + static_cast(qmlAttachedPropertiesObject(trans)); + if (attached) { + attached->m_index = item->index; + attached->m_item = item->item; + attached->m_destination = to; + switch (type) { + case NoTransition: + break; + case PopulateTransition: + case AddTransition: + attached->m_targetIndexes = vp->addTransitionIndexes; + attached->m_targetItems = vp->addTransitionTargets; + break; + case MoveTransition: + attached->m_targetIndexes = vp->moveTransitionIndexes; + attached->m_targetItems = vp->moveTransitionTargets; + break; + case RemoveTransition: + attached->m_targetIndexes = vp->removeTransitionIndexes; + attached->m_targetItems = vp->removeTransitionTargets; + break; + } + emit attached->indexChanged(); + emit attached->itemChanged(); + emit attached->destinationChanged(); + emit attached->targetIndexesChanged(); + emit attached->targetItemsChanged(); + } + + QDeclarativeStateOperation::ActionList actions; + actions << QDeclarativeAction(item->item, QLatin1String("x"), QVariant(to.x())); + actions << QDeclarativeAction(item->item, QLatin1String("y"), QVariant(to.y())); + + QDeclarativeTransitionManager::transition(actions, trans, item->item); +} + +void FxViewItemTransitionManager::finished() +{ + QDeclarativeTransitionManager::finished(); + + m_active = false; + + if (m_item) + m_item->finishedTransition(); + m_item = 0; + m_toPos.setX(0); + m_toPos.setY(0); + m_type = NoTransition; + m_isTarget = false; +} + QQuickItemViewChangeSet::QQuickItemViewChangeSet() : active(false) @@ -137,6 +413,364 @@ void QQuickItemViewChangeSet::reset() } +QQuickViewTransitionAttached::QQuickViewTransitionAttached(QObject *parent) + : QObject(parent), m_index(-1), m_item(0) +{ +} +/*! + \qmlclass ViewTransition QQuickViewTransitionAttached + \inqmlmodule QtQuick 2 + \ingroup qml-view-elements + \brief The ViewTransition attached property provides details on items under transition in a view. + + With ListView and GridView, it is possible to specify transitions that should be applied whenever + the items in the view change as a result of modifications to the view's model. They both have the + following properties that can be set to the appropriate transitions to be run for various + operations: + + \list + \o \c add and \c addDisplaced - the transitions to run when items are added to the view + \o \c remove and \c removeDisplaced - the transitions to run when items are removed from the view + \o \c move and \c moveDisplaced - the transitions to run when items are moved within the view + (i.e. as a result of a move operation in the model) + \o \c populate - the transition to run when a view is created, or when the model changes + \endlist + + Such view transitions additionally have access to a ViewTransition attached property that + provides details of the items that are under transition and the operation that triggered the + transition. Since view transitions are run once per item, these details can be used to customise + each transition for each individual item. + + The ViewTransition attached property provides the following properties specific to the item to + which the transition is applied: + + \list + \o ViewTransition.item - the item that is under transition + \o ViewTransition.index - the index of this item + \o ViewTransition.destination - the (x,y) point to which this item is moving for the relevant view operation + \endlist + + In addition, ViewTransition provides properties specific to the items which are the target + of the operation that triggered the transition: + + \list + \o ViewTransition.targetIndexes - the indexes of the target items + \o ViewTransition.targetItems - the target items themselves + \endlist + + View transitions can be written without referring to any of the attributes listed + above. These attributes merely provide extra details that are useful for customising view + transitions. + + Following is an introduction to view transitions and the ways in which the ViewTransition + attached property can be used to augment view transitions. + + + \section2 View transitions: a simple example + + Here is a basic example of the use of view transitions. The view below specifies transitions for + the \c add and \c addDisplaced properties, which will be run when items are added to the view: + + \snippet doc/src/snippets/declarative/viewtransitions/viewtransitions-basic.qml 0 + + When the space key is pressed, adding an item to the model, the new item will fade in and + increase in scale over 400 milliseconds as it is added to the view. Also, any item that is + displaced by the addition of a new item will animate to its new position in the view over + 400 milliseconds, as specified by the \c addDisplaced transition. + + If five items were inserted in succession at index 0, the effect would be this: + + \image viewtransitions-basic.gif + + Notice that the NumberAnimation objects above do not need to specify a \c target to animate + the appropriate item. Also, the NumberAnimation in the \c addTransition does not need to specify + the \c to value to move the item to its correct position in the view. This is because the view + implicitly sets the \c target and \c to values with the correct item and final item position + values if these properties are not explicitly defined. + + At its simplest, a view transition may just animate an item to its new position following a + view operation, just as the \c addDisplaced transition does above, or animate some item properties, + as in the \c add transition above. Additionally, a view transition may make use of the + ViewTransition attached property to customise animation behavior for different items. Following + are some examples of how this can be achieved. + + + \section2 Using the ViewTransition attached property + + As stated, the various ViewTransition properties provide details specific to the individual item + being transitioned as well as the operation that triggered the transition. In the animation above, + five items are inserted in succession at index 0. When the fifth and final insertion takes place, + adding "Item 4" to the view, the \c add transition is run once (for the inserted item) and the + \c addDisplaced transition is run four times (once for each of the four existing items in the view). + + At this point, if we examined the \c addDisplaced transition that was run for the bottom displaced + item ("Item 0"), the ViewTransition property values provided to this transition would be as follows: + + \table + \header + \o Property + \o Value + \o Explanation + \row + \o ViewTransition.item + \o "Item 0" delegate instance + \o The "Item 0" \l Rectangle object itself + \row + \o ViewTransition.index + \o \c int value of 4 + \o The index of "Item 0" within the model following the add operation + \row + \o ViewTransition.destination + \o \l point value of (0, 120) + \o The position that "Item 0" is moving to + \row + \o ViewTransition.targetIndexes + \o \c int array, just contains the integer "0" (zero) + \o The index of "Item 4", the new item added to the view + \row + \o ViewTransition.targetItems + \o object array, just contains the "Item 4" delegate instance + \o The "Item 4" \l Rectangle object - the new item added to the view + \endtable + + The ViewTransition.targetIndexes and ViewTransition.targetItems lists provide the items and + indexes of all delegate instances that are the targets of the relevant operation. For an add + operation, these are all the items that are added into the view; for a remove, these are all + the items removed from the view, and so on. (Note these lists will only contain references to + items that have been created within the view or its cached items; targets that are not within + the visible area of the view or within the item cache will not be accessible.) + + So, while the ViewTransition.item, ViewTransition.index and ViewTransition.destination values + vary for each individual transition that is run, the ViewTransition.targetIndexes and + ViewTransition.targetItems values are the same for every \c add and \c addDisplaced transition + that is triggered by a particular add operation. + + + \section3 Delaying animations based on index + + Since each view transition is run once for each item affected by the transition, the ViewTransition + properties can be used within a transition to define custom behavior for each item's transition. + For example, the ListView in the previous example could use this information to create a ripple-type + effect on the movement of the displaced items. + + This can be achieved by modifying the \c addDisplaced transition so that it delays the animation of + each displaced item based on the difference between its index (provided by ViewTransition.index) + and the first removed index (provided by ViewTransition.targetIndexes): + + \snippet doc/src/snippets/declarative/viewtransitions/viewtransitions-delayedbyindex.qml 0 + + Each displaced item delays its animation by an additional 100 milliseconds, producing a subtle + ripple-type effect when items are displaced by the add, like this: + + \image viewtransitions-delayedbyindex.gif + + + \section3 Animating items to intermediate positions + + The ViewTransition.item property gives a reference to the item to which the transition is being + applied. This can be used to access any of the item's attributes, custom \c property values, + and so on. + + Below is a modification of the \c addDisplaced transition from the previous example. It adds a + ParallelAnimation with nested NumberAnimation objects that reference ViewTransition.item to access + each item's \c x and \c y values at the start of their transitions. This allows each item to + animate to an intermediate position relative to its starting point for the transition, before + animating to its final position in the view: + + \snippet doc/src/snippets/declarative/viewtransitions/viewtransitions-intermediatemove.qml 0 + + Now, a displaced item will first move to a position of (20, 50) relative to its starting + position, and then to its final, correct position in the view: + + \image viewtransitions-intermediatemove.gif + + Since the final NumberAnimation does not specify a \c to value, the view implicitly sets this + value to the item's final position in the view, and so this last animation will move this item + to the correct place. If the transition requires the final position of the item for some calculation, + this is accessible through ViewTransition.destination. + + Instead of using multiple NumberAnimations, you could use a PathAnimation to animate an item over + a curved path. For example, the \c add transition in the previous example could be augmented with + a PathAnimation as follows: to animate newly added items along a path: + + \snippet doc/src/snippets/declarative/viewtransitions/viewtransitions-pathanim.qml 0 + + This animates newly added items along a path. Notice that each path is specified relative to + each item's final destination point, so that items inserted at different indexes start their + paths from different positions: + + \image viewtransitions-pathanim.gif + + + \section2 Handling interrupted animations + + A view transition may be interrupted at any time if a different view transition needs to be + applied while the original transition is in progress. For example, say Item A is inserted at index 0 + and undergoes an "add" transition; then, Item B is inserted at index 0 in quick succession before + Item A's transition has finished. Since Item B is inserted before Item A, it will displace Item + A, causing the view to interrupt Item A's "add" transition mid-way and start an "addDisplaced" + transition on Item A instead. + + For simple animations that simply animate an item's movement to its final destination, this + interruption is unlikely to require additional consideration. However, if a transition changes other + properties, this interruption may cause unwanted side effects. Consider the first example on this + page, repeated below for convenience: + + \snippet doc/src/snippets/declarative/viewtransitions/viewtransitions-basic.qml 0 + + If multiple items are added in rapid succession, without waiting for a previous transition + to finish, this is the result: + + \image viewtransitions-interruptedbad.gif + + Each newly added item undergoes an \c add transition, but before the transition can finish, + another item is added, displacing the previously added item. Because of this, the \c add + transition on the previously added item is interrupted and an \c addDisplaced transition is + started on the item instead. Due to the interruption, the \c opacity and \c scale animations + have not completed, thus producing items with opacity and scale that are below 1.0. + + To fix this, the \c addDisplaced transition should additionally ensure the item properties are + set to the end values specified in the \c add transition, effectively resetting these values + whenever an item is displaced. In this case, it means setting the item opacity and scale to 1.0: + + \snippet doc/src/snippets/declarative/viewtransitions/viewtransitions-interruptedgood.qml 0 + + Now, when an item's \c add transition is interrupted, its opacity and scale are animated to 1.0 + upon displacement, avoiding the erroneous visual effects from before: + + \image viewtransitions-interruptedgood.gif + + The same principle applies to any combination of view transitions. An added item may be moved + before its add transition finishes, or a moved item may be removed before its moved transition + finishes, and so on; so, the rule of thumb is that every transition should handle the same set of + properties. + + + \section2 Restrictions regarding ScriptAction + + When a view transition is initialized, any property bindings that refer to the ViewTransition + attached property are evaluated in preparation for the transition. Due to the nature of the + internal construction of a view transition, the attributes of the ViewTransition attached + property are only valid for the relevant item when the transition is initialized, and may not be + valid when the transition is actually run. + + Therefore, a ScriptAction within a view transition should not refer to the ViewTransition + attached property, as it may not refer to the expected values at the time that the ScriptAction + is actually invoked. Consider the following example: + + \snippet doc/src/snippets/declarative/viewtransitions/viewtransitions-scriptactionbad.qml 0 + + When the space key is pressed, three items are moved from index 5 to index 1. For each moved + item, the \c moveTransition sequence presumably animates the item's color to "yellow", then + animates it to its final position, then changes the item color back to "lightsteelblue" using a + ScriptAction. However, when run, the transition does not produce the intended result: + + \image viewtransitions-scriptactionbad.gif + + Only the last moved item is returned to the "lightsteelblue" color; the others remain yellow. This + is because the ScriptAction is not run until after the transition has already been initialized, by + which time the ViewTransition.item value has changed to refer to a different item; the item that + the script had intended to refer to is not the one held by ViewTransition.item at the time the + ScriptAction is actually invoked. + + In this instance, to avoid this issue, the view could set the property using a PropertyAction + instead: + + \snippet doc/src/snippets/declarative/viewtransitions/viewtransitions-scriptactiongood.qml 0 + + When the transition is initialized, the PropertyAction \c target will be set to the respective + ViewTransition.item for the transition and will later run with the correct item target as + expected. + */ + +/*! + \qmlattachedproperty list QtQuick2::ViewTransition::index + + This attached property holds the index of the item that is being + transitioned. + + Note that if the item is being moved, this property holds the index that + the item is moving to, not from. +*/ + +/*! + \qmlattachedproperty list QtQuick2::ViewTransition::item + + This attached property holds the the item that is being transitioned. + + \warning This item should not be kept and referred to outside of the transition + as it may become invalid as the view changes. +*/ + +/*! + \qmlattachedproperty list QtQuick2::ViewTransition::destination + + This attached property holds the final destination position for the transitioned + item within the view. + + This property value is a \l point with \c x and \c y properties. +*/ + +/*! + \qmlattachedproperty list QtQuick2::ViewTransition::targetIndexes + + This attached property holds a list of the indexes of the items in view + that are the target of the relevant operation. + + The targets are the items that are the subject of the operation. For + an add operation, these are the items being added; for a remove, these + are the items being removed; for a move, these are the items being + moved. + + For example, if the transition was triggered by an insert operation + that added two items at index 1 and 2, this targetIndexes list would + have the value [1,2]. + + \note The targetIndexes list only contains the indexes of items that are actually + in view, or will be in the view once the relevant operation completes. + + \sa QtQuick2::ViewTransition::targetIndexes +*/ + +/*! + \qmlattachedproperty list QtQuick2::ViewTransition::targetItems + + This attached property holds the list of items in view that are the + target of the relevant operation. + + The targets are the items that are the subject of the operation. For + an add operation, these are the items being added; for a remove, these + are the items being removed; for a move, these are the items being + moved. + + For example, if the transition was triggered by an insert operation + that added two items at index 1 and 2, this targetItems list would + contain these two items. + + \note The targetItems list only contains items that are actually + in view, or will be in the view once the relevant operation completes. + + \warning The objects in this list should not be kept and referred to + outside of the transition as the items may become invalid. The targetItems + are only valid when the Transition is initially created; this also means + they should not be used by ScriptAction objects in the Transition, which are + not evaluated until the transition is run. + + \sa QtQuick2::ViewTransition::targetIndexes +*/ +QDeclarativeListProperty QQuickViewTransitionAttached::targetItems() +{ + return QDeclarativeListProperty(this, m_targetItems); +} + +QQuickViewTransitionAttached *QQuickViewTransitionAttached::qmlAttachedProperties(QObject *obj) +{ + return new QQuickViewTransitionAttached(obj); +} + + +//----------------------------------- + QQuickItemView::QQuickItemView(QQuickFlickablePrivate &dd, QQuickItem *parent) : QQuickFlickable(dd, parent) { @@ -232,6 +866,12 @@ void QQuickItemView::setModel(const QVariant &model) d->moveReason = QQuickItemViewPrivate::Other; } d->updateViewport(); + + if (d->populateTransition) { + d->forceLayout = true; + d->usePopulateTransition = true; + polish(); + } } connect(d->model, SIGNAL(modelUpdated(QDeclarativeChangeSet,bool)), this, SLOT(modelUpdated(QDeclarativeChangeSet,bool))); @@ -585,6 +1225,111 @@ void QQuickItemView::setHighlightMoveDuration(int duration) } } +QDeclarativeTransition *QQuickItemView::populateTransition() const +{ + Q_D(const QQuickItemView); + return d->populateTransition; +} + +void QQuickItemView::setPopulateTransition(QDeclarativeTransition *transition) +{ + Q_D(QQuickItemView); + if (d->populateTransition != transition) { + d->populateTransition = transition; + emit populateTransitionChanged(); + } +} + +QDeclarativeTransition *QQuickItemView::addTransition() const +{ + Q_D(const QQuickItemView); + return d->addTransition; +} + +void QQuickItemView::setAddTransition(QDeclarativeTransition *transition) +{ + Q_D(QQuickItemView); + if (d->addTransition != transition) { + d->addTransition = transition; + emit addTransitionChanged(); + } +} + +QDeclarativeTransition *QQuickItemView::addDisplacedTransition() const +{ + Q_D(const QQuickItemView); + return d->addDisplacedTransition; +} + +void QQuickItemView::setAddDisplacedTransition(QDeclarativeTransition *transition) +{ + Q_D(QQuickItemView); + if (d->addDisplacedTransition != transition) { + d->addDisplacedTransition = transition; + emit addDisplacedTransitionChanged(); + } +} + +QDeclarativeTransition *QQuickItemView::moveTransition() const +{ + Q_D(const QQuickItemView); + return d->moveTransition; +} + +void QQuickItemView::setMoveTransition(QDeclarativeTransition *transition) +{ + Q_D(QQuickItemView); + if (d->moveTransition != transition) { + d->moveTransition = transition; + emit moveTransitionChanged(); + } +} + +QDeclarativeTransition *QQuickItemView::moveDisplacedTransition() const +{ + Q_D(const QQuickItemView); + return d->moveDisplacedTransition; +} + +void QQuickItemView::setMoveDisplacedTransition(QDeclarativeTransition *transition) +{ + Q_D(QQuickItemView); + if (d->moveDisplacedTransition != transition) { + d->moveDisplacedTransition = transition; + emit moveDisplacedTransitionChanged(); + } +} + +QDeclarativeTransition *QQuickItemView::removeTransition() const +{ + Q_D(const QQuickItemView); + return d->removeTransition; +} + +void QQuickItemView::setRemoveTransition(QDeclarativeTransition *transition) +{ + Q_D(QQuickItemView); + if (d->removeTransition != transition) { + d->removeTransition = transition; + emit removeTransitionChanged(); + } +} + +QDeclarativeTransition *QQuickItemView::removeDisplacedTransition() const +{ + Q_D(const QQuickItemView); + return d->removeDisplacedTransition; +} + +void QQuickItemView::setRemoveDisplacedTransition(QDeclarativeTransition *transition) +{ + Q_D(QQuickItemView); + if (d->removeDisplacedTransition != transition) { + d->removeDisplacedTransition = transition; + emit removeDisplacedTransitionChanged(); + } +} + void QQuickItemViewPrivate::positionViewAtIndex(int index, int mode) { Q_Q(QQuickItemView); @@ -719,6 +1464,64 @@ void QQuickItemViewPrivate::applyPendingChanges() layout(); } +bool QQuickItemViewPrivate::hasItemTransitions() const +{ + return populateTransition + || addTransition || addDisplacedTransition + || moveTransition || moveDisplacedTransition + || removeTransition || removeDisplacedTransition; +} + +void QQuickItemViewPrivate::transitionNextReposition(FxViewItem *item, FxViewItemTransitionManager::TransitionType type, bool isTarget) +{ + switch (type) { + case FxViewItemTransitionManager::NoTransition: + return; + case FxViewItemTransitionManager::PopulateTransition: + if (populateTransition) { + item->setNextTransition(FxViewItemTransitionManager::PopulateTransition, isTarget); + return; + } + break; + case FxViewItemTransitionManager::AddTransition: + if (!usePopulateTransition) { + if ((isTarget && addTransition) || (!isTarget && addDisplacedTransition)) { + item->setNextTransition(type, isTarget); + return; + } + } + break; + case FxViewItemTransitionManager::MoveTransition: + if ((isTarget && moveTransition) || (!isTarget && moveDisplacedTransition)) { + item->setNextTransition(type, isTarget); + return; + } + break; + case FxViewItemTransitionManager::RemoveTransition: + if ((isTarget && removeTransition) || (!isTarget && removeDisplacedTransition)) { + item->setNextTransition(type, isTarget); + return; + } + break; + } + + // the requested transition type is not valid, but the item is scheduled/in another + // transition, so cancel it to allow the item to move directly to the correct pos + if (item->transitionScheduledOrRunning()) + item->stopTransition(); +} + +int QQuickItemViewPrivate::findMoveKeyIndex(QDeclarativeChangeSet::MoveKey key, const QVector &changes) const +{ + for (int i=0; iindex + << visibleItems[i]->item->objectName() + << visibleItems[i]->position(); + } +} + void QQuickItemViewPrivate::itemGeometryChanged(QQuickItem *item, const QRectF &newGeometry, const QRectF &oldGeometry) { Q_Q(QQuickItemView); @@ -752,8 +1566,19 @@ void QQuickItemViewPrivate::itemGeometryChanged(QQuickItem *item, const QRectF & fixupPosition(); } - if (currentItem && currentItem->item == item) + if (currentItem && currentItem->item == item) { + // don't allow item movement transitions to trigger a re-layout and + // start new transitions + bool prevDisableLayout = disableLayout; + if (!disableLayout) { + FxViewItem *actualItem = hasItemTransitions() ? visibleItem(currentIndex) : 0; + if (actualItem && actualItem->transition && actualItem->transition->isRunning()) + disableLayout = true; + } updateHighlight(); + disableLayout = prevDisableLayout; + } + if (trackedItem && trackedItem->item == item) q->trackedPositionChanged(); } @@ -765,8 +1590,15 @@ void QQuickItemView::destroyRemoved() it != d->visibleItems.end();) { FxViewItem *item = *it; if (item->index == -1 && item->attached->delayRemove() == false) { - d->releaseItem(item); - it = d->visibleItems.erase(it); + if (d->removeTransition) { + // don't remove from visibleItems until next layout() + d->runDelayedRemoveTransition = true; + QObject::disconnect(item->attached, SIGNAL(delayRemoveChanged()), this, SLOT(destroyRemoved())); + ++it; + } else { + d->releaseItem(item); + it = d->visibleItems.erase(it); + } } else { ++it; } @@ -782,6 +1614,7 @@ void QQuickItemView::modelUpdated(const QDeclarativeChangeSet &changeSet, bool r { Q_D(QQuickItemView); if (reset) { + d->usePopulateTransition = true; d->moveReason = QQuickItemViewPrivate::SetIndex; d->regenerate(); if (d->highlight && d->currentItem) { @@ -790,8 +1623,11 @@ void QQuickItemView::modelUpdated(const QDeclarativeChangeSet &changeSet, bool r d->updateTrackedItem(); } d->moveReason = QQuickItemViewPrivate::Other; - emit countChanged(); + if (d->populateTransition) { + d->forceLayout = true; + polish(); + } } else { d->currentChanges.prepare(d->currentIndex, d->itemCount); d->currentChanges.applyChanges(changeSet); @@ -1088,6 +1924,8 @@ void QQuickItemView::componentComplete() d->updateFooter(); d->updateViewport(); d->setPosition(d->contentStartOffset()); + d->usePopulateTransition = true; + if (d->isValid()) { d->refill(); d->moveReason = QQuickItemViewPrivate::SetIndex; @@ -1122,11 +1960,16 @@ QQuickItemViewPrivate::QQuickItemViewPrivate() , highlightRangeStart(0), highlightRangeEnd(0) , highlightMoveDuration(150) , headerComponent(0), header(0), footerComponent(0), footer(0) + , populateTransition(0) + , addTransition(0), addDisplacedTransition(0) + , moveTransition(0), moveDisplacedTransition(0) + , removeTransition(0), removeDisplacedTransition(0) , minExtent(0), maxExtent(0) , ownModel(false), wrap(false) - , inApplyModelChanges(false), inViewportMoved(false), forceLayout(false), currentIndexCleared(false) + , disableLayout(false), inViewportMoved(false), forceLayout(false), currentIndexCleared(false) , haveHighlightRange(false), autoHighlight(true), highlightRangeStartValid(false), highlightRangeEndValid(false) , fillCacheBuffer(false), inRequest(false), requestedAsync(false) + , usePopulateTransition(false), runDelayedRemoveTransition(false) { } @@ -1193,6 +2036,8 @@ FxViewItem *QQuickItemViewPrivate::visibleItem(int modelIndex) const { return 0; } +// should rename to firstItemInView() to avoid confusion with other "*visible*" methods +// that don't look at the view position and size FxViewItem *QQuickItemViewPrivate::firstVisibleItem() const { const qreal pos = isContentFlowReversed() ? -position()-size() : position(); for (int i = 0; i < visibleItems.count(); ++i) { @@ -1203,6 +2048,16 @@ FxViewItem *QQuickItemViewPrivate::firstVisibleItem() const { return visibleItems.count() ? visibleItems.first() : 0; } +int QQuickItemViewPrivate::findLastIndexInView() const +{ + const qreal viewEndPos = isContentFlowReversed() ? -position() : position() + size(); + for (int i=visibleItems.count() - 1; i>=0; i--) { + if (visibleItems.at(i)->position() <= viewEndPos && visibleItems.at(i)->index != -1) + return visibleItems.at(i)->index; + } + return -1; +} + // Map a model index to visibleItems list index. // These may differ if removed items are still present in the visible list, // e.g. doing a removal animation @@ -1284,6 +2139,12 @@ void QQuickItemViewPrivate::clear() visibleItems.clear(); visibleIndex = 0; + for (int i = 0; i < releasePendingTransition.count(); ++i) { + releasePendingTransition.at(i)->releaseAfterTransition = false; + releaseItem(releasePendingTransition.at(i)); + } + releasePendingTransition.clear(); + releaseItem(currentItem); currentItem = 0; createHighlight(); @@ -1382,16 +2243,26 @@ void QQuickItemViewPrivate::updateViewport() void QQuickItemViewPrivate::layout() { Q_Q(QQuickItemView); - if (inApplyModelChanges) + if (disableLayout) return; if (!isValid() && !visibleItems.count()) { clear(); setPosition(contentStartOffset()); + usePopulateTransition = false; return; } - if (!applyModelChanges() && !forceLayout) { + if (runDelayedRemoveTransition && removeDisplacedTransition) { + // assume that any items moving now are moving due to the remove - if they schedule + // a different transition, that will override this one anyway + for (int i=0; iisMoving() && !q->isFlicking()) { @@ -1416,20 +2291,49 @@ void QQuickItemViewPrivate::layout() updateFooter(); updateViewport(); updateUnrequestedPositions(); + + if (hasItemTransitions()) { + // items added in the last refill() may need to be transitioned in - e.g. a remove + // causes items to slide up into view + if (moveDisplacedTransition || removeDisplacedTransition) + translateAndTransitionItemsAfter(lastIndexInView, insertionPosChanges, removalPosChanges); + + prepareVisibleItemTransitions(); + + QRectF viewBounds(0, position(), q->width(), q->height()); + for (QList::Iterator it = releasePendingTransition.begin(); + it != releasePendingTransition.end(); ) { + FxViewItem *item = *it; + if ( (item->transition && item->transition->isActive()) + || prepareNonVisibleItemTransition(item, viewBounds)) { + ++it; + } else { + releaseItem(item); + it = releasePendingTransition.erase(it); + } + } + + for (int i=0; istartTransition(); + for (int i=0; istartTransition(); + } + usePopulateTransition = false; + runDelayedRemoveTransition = false; } -bool QQuickItemViewPrivate::applyModelChanges() +bool QQuickItemViewPrivate::applyModelChanges(ChangeResult *totalInsertionResult, ChangeResult *totalRemovalResult) { Q_Q(QQuickItemView); - if (!q->isComponentComplete() || !currentChanges.hasPendingChanges() || inApplyModelChanges) + if (!q->isComponentComplete() || (!currentChanges.hasPendingChanges() && !runDelayedRemoveTransition) || disableLayout) return false; - inApplyModelChanges = true; + disableLayout = true; updateUnrequestedIndexes(); moveReason = QQuickItemViewPrivate::Other; - FxViewItem *prevVisibleItemsFirst = visibleItems.count() ? visibleItems.first() : 0; + FxViewItem *prevVisibleItemsFirst = visibleItems.count() ? *visibleItems.constBegin() : 0; int prevItemCount = itemCount; int prevVisibleItemsCount = visibleItems.count(); bool visibleAffected = false; @@ -1445,10 +2349,13 @@ bool QQuickItemViewPrivate::applyModelChanges() } qreal prevVisibleItemsFirstPos = visibleItems.count() ? visibleItems.first()->position() : 0.0; + totalInsertionResult->visiblePos = prevViewPos; + totalRemovalResult->visiblePos = prevViewPos; + const QVector &removals = currentChanges.pendingChanges.removes(); const QVector &insertions = currentChanges.pendingChanges.inserts(); - ChangeResult removalResult(prevViewPos); ChangeResult insertionResult(prevViewPos); + ChangeResult removalResult(prevViewPos); int removedCount = 0; for (int i=0; i= 0 && removals[i].index < prevFirstVisibleIndex) { if (removals[i].index + removals[i].count < prevFirstVisibleIndex) - removalResult.changeBeforeVisible -= removals[i].count; + removalResult.countChangeBeforeVisible += removals[i].count; else - removalResult.changeBeforeVisible -= (prevFirstVisibleIndex - removals[i].index); + removalResult.countChangeBeforeVisible += (prevFirstVisibleIndex - removals[i].index); } } + if (runDelayedRemoveTransition) { + QDeclarativeChangeSet::Remove removal; + for (QList::Iterator it = visibleItems.begin(); it != visibleItems.end();) { + FxViewItem *item = *it; + if (item->index == -1 && !item->attached->delayRemove()) { + removeItem(item, removal, &removalResult); + removedCount++; + it = visibleItems.erase(it); + } else { + ++it; + } + } + } + *totalRemovalResult += removalResult; if (!removals.isEmpty()) { updateVisibleIndex(); @@ -1475,31 +2396,50 @@ bool QQuickItemViewPrivate::applyModelChanges() } QList newItems; + QList movingIntoView; + for (int i=0; iattached->emitAdd(); + // for each item that was moved directly into the view as a result of a move(), + // find the index it was moved from in order to set its initial position, so that we + // can transition it from this "original" position to its new position in the view + if (moveTransition) { + for (int i=0; i= 0) { + if (prevFirstVisibleIndex >= 0 && fromIndex < prevFirstVisibleIndex) + repositionItemAt(movingIntoView[i].item, fromIndex, -totalInsertionResult->sizeChangesAfterVisiblePos); + else + repositionItemAt(movingIntoView[i].item, fromIndex, totalInsertionResult->sizeChangesAfterVisiblePos); + transitionNextReposition(movingIntoView[i].item, FxViewItemTransitionManager::MoveTransition, true); + } + } + } + // reposition visibleItems.first() correctly so that the content y doesn't jump if (removedCount != prevVisibleItemsCount) repositionFirstItem(prevVisibleItemsFirst, prevVisibleItemsFirstPos, prevFirstVisible, &insertionResult, &removalResult); // Whatever removed/moved items remain are no longer visible items. + prepareRemoveTransitions(¤tChanges.removedItems); for (QHash::Iterator it = currentChanges.removedItems.begin(); it != currentChanges.removedItems.end(); ++it) { releaseItem(it.value()); @@ -1526,7 +2466,7 @@ bool QQuickItemViewPrivate::applyModelChanges() if (!visibleAffected && viewportChanged) updateViewport(); - inApplyModelChanges = false; + disableLayout = false; return visibleAffected; } @@ -1535,6 +2475,13 @@ bool QQuickItemViewPrivate::applyRemovalChange(const QDeclarativeChangeSet::Remo Q_Q(QQuickItemView); bool visibleAffected = false; + if (visibleItems.count() && removal.index + removal.count > visibleItems.last()->index) { + if (removal.index > visibleItems.last()->index) + removeResult->countChangeAfterVisibleItems += removal.count; + else + removeResult->countChangeAfterVisibleItems += ((removal.index + removal.count - 1) - visibleItems.last()->index); + } + QList::Iterator it = visibleItems.begin(); while (it != visibleItems.end()) { FxViewItem *item = *it; @@ -1546,6 +2493,10 @@ bool QQuickItemViewPrivate::applyRemovalChange(const QDeclarativeChangeSet::Remo } else if (item->index >= removal.index + removal.count) { // after removed items item->index -= removal.count; + if (removal.isMove()) + transitionNextReposition(item, FxViewItemTransitionManager::MoveTransition, false); + else + transitionNextReposition(item, FxViewItemTransitionManager::RemoveTransition, false); ++it; } else { // removed item @@ -1558,21 +2509,9 @@ bool QQuickItemViewPrivate::applyRemovalChange(const QDeclarativeChangeSet::Remo QObject::connect(item->attached, SIGNAL(delayRemoveChanged()), q, SLOT(destroyRemoved()), Qt::QueuedConnection); ++it; } else { - if (removeResult->visiblePos.isValid()) { - if (item->position() < removeResult->visiblePos) - removeResult->sizeChangesBeforeVisiblePos += item->size(); - else - removeResult->sizeChangesAfterVisiblePos += item->size(); - } - if (removal.isMove()) { - currentChanges.removedItems.insert(removal.moveKey(item->index), item); - } else { - // track item so it is released later - currentChanges.removedItems.insertMulti(QDeclarativeChangeSet::MoveKey(), item); + removeItem(item, removal, removeResult); + if (!removal.isMove()) (*removedCount)++; - } - if (!removeResult->changedFirstItem && item == visibleItems.first()) - removeResult->changedFirstItem = true; it = visibleItems.erase(it); } } @@ -1581,6 +2520,25 @@ bool QQuickItemViewPrivate::applyRemovalChange(const QDeclarativeChangeSet::Remo return visibleAffected; } +void QQuickItemViewPrivate::removeItem(FxViewItem *item, const QDeclarativeChangeSet::Remove &removal, ChangeResult *removeResult) +{ + if (removeResult->visiblePos.isValid()) { + if (item->position() < removeResult->visiblePos) + removeResult->sizeChangesBeforeVisiblePos += item->size(); + else + removeResult->sizeChangesAfterVisiblePos += item->size(); + } + if (removal.isMove()) { + currentChanges.removedItems.insert(removal.moveKey(item->index), item); + transitionNextReposition(item, FxViewItemTransitionManager::MoveTransition, true); + } else { + // track item so it is released later + currentChanges.removedItems.insertMulti(QDeclarativeChangeSet::MoveKey(), item); + } + if (!removeResult->changedFirstItem && item == *visibleItems.constBegin()) + removeResult->changedFirstItem = true; +} + void QQuickItemViewPrivate::repositionFirstItem(FxViewItem *prevVisibleItemsFirst, qreal prevVisibleItemsFirstPos, FxViewItem *prevFirstVisible, @@ -1613,13 +2571,102 @@ void QQuickItemViewPrivate::repositionFirstItem(FxViewItem *prevVisibleItemsFirs moveForwardsBy = removalResult->sizeChangesBeforeVisiblePos; moveBackwardsBy = insertionResult->sizeChangesBeforeVisiblePos; } - adjustFirstItem(moveForwardsBy, moveBackwardsBy, insertionResult->changeBeforeVisible + removalResult->changeBeforeVisible); + adjustFirstItem(moveForwardsBy, moveBackwardsBy, insertionResult->countChangeBeforeVisible - removalResult->countChangeBeforeVisible); } insertionResult->reset(); removalResult->reset(); } } +void QQuickItemViewPrivate::prepareVisibleItemTransitions() +{ + Q_Q(QQuickItemView); + if (!hasItemTransitions()) + return; + + addTransitionIndexes.clear(); + addTransitionTargets.clear(); + moveTransitionIndexes.clear(); + moveTransitionTargets.clear(); + + QRectF viewBounds(0, position(), q->width(), q->height()); + for (int i=0; iprepareTransition(viewBounds)) + continue; + if (visibleItems[i]->isTransitionTarget) { + switch (visibleItems[i]->nextTransitionType) { + case FxViewItemTransitionManager::NoTransition: + break; + case FxViewItemTransitionManager::PopulateTransition: + case FxViewItemTransitionManager::AddTransition: + addTransitionIndexes.append(visibleItems[i]->index); + addTransitionTargets.append(visibleItems[i]->item); + break; + case FxViewItemTransitionManager::MoveTransition: + moveTransitionIndexes.append(visibleItems[i]->index); + moveTransitionTargets.append(visibleItems[i]->item); + break; + case FxViewItemTransitionManager::RemoveTransition: + // removed targets won't be in visibleItems, handle these + // in prepareNonVisibleItemTransition() + break; + } + } + } +} + +void QQuickItemViewPrivate::prepareRemoveTransitions(QHash *removedItems) +{ + if (!removeTransition && !removeDisplacedTransition) + return; + + removeTransitionIndexes.clear(); + removeTransitionTargets.clear(); + + if (removeTransition) { + for (QHash::Iterator it = removedItems->begin(); + it != removedItems->end(); ) { + bool isRemove = it.key().moveId < 0; + if (isRemove) { + FxViewItem *item = *it; + item->releaseAfterTransition = true; + releasePendingTransition.append(item); + transitionNextReposition(item, FxViewItemTransitionManager::RemoveTransition, true); + it = removedItems->erase(it); + } else { + ++it; + } + } + } +} + +bool QQuickItemViewPrivate::prepareNonVisibleItemTransition(FxViewItem *item, const QRectF &viewBounds) +{ + // Called for items that have been removed from visibleItems and may now be + // transitioned out of the view. This applies to items that are being directly + // removed, or moved to outside of the view, as well as those that are + // displaced to a position outside of the view due to an insert or move. + + if (item->nextTransitionType == FxViewItemTransitionManager::MoveTransition) + repositionItemAt(item, item->index, 0); + if (!item->prepareTransition(viewBounds)) + return false; + + if (item->isTransitionTarget) { + if (item->nextTransitionType == FxViewItemTransitionManager::MoveTransition) { + moveTransitionIndexes.append(item->index); + moveTransitionTargets.append(item->item); + } else if (item->nextTransitionType == FxViewItemTransitionManager::RemoveTransition) { + removeTransitionIndexes.append(item->index); + removeTransitionTargets.append(item->item); + } + } + + item->releaseAfterTransition = true; + return true; +} + /* This may return 0 if the item is being created asynchronously. When the item becomes available, refill() will be called and the item @@ -1628,6 +2675,7 @@ void QQuickItemViewPrivate::repositionFirstItem(FxViewItem *prevVisibleItemsFirs FxViewItem *QQuickItemViewPrivate::createItem(int modelIndex, bool asynchronous) { Q_Q(QQuickItemView); + if (requestedIndex == modelIndex && (asynchronous || requestedAsync == asynchronous)) return 0; @@ -1638,6 +2686,14 @@ FxViewItem *QQuickItemViewPrivate::createItem(int modelIndex, bool asynchronous) requestedItem = 0; } + for (int i=0; iindex == modelIndex + && !releasePendingTransition[i]->isPendingRemoval()) { + releasePendingTransition[i]->releaseAfterTransition = false; + return releasePendingTransition.takeAt(i); + } + } + requestedIndex = modelIndex; requestedAsync = asynchronous; inRequest = true; diff --git a/src/quick/items/qquickitemview_p.h b/src/quick/items/qquickitemview_p.h index e43f7c6d70..0d3cd1c3ce 100644 --- a/src/quick/items/qquickitemview_p.h +++ b/src/quick/items/qquickitemview_p.h @@ -48,6 +48,8 @@ QT_BEGIN_HEADER QT_BEGIN_NAMESPACE +QT_MODULE(Declarative) + class QDeclarativeChangeSet; class QQuickItemViewPrivate; @@ -74,6 +76,14 @@ class Q_AUTOTEST_EXPORT QQuickItemView : public QQuickFlickable Q_PROPERTY(QDeclarativeComponent *footer READ footer WRITE setFooter NOTIFY footerChanged) Q_PROPERTY(QQuickItem *footerItem READ footerItem NOTIFY footerItemChanged) + Q_PROPERTY(QDeclarativeTransition *populate READ populateTransition WRITE setPopulateTransition NOTIFY populateTransitionChanged) + Q_PROPERTY(QDeclarativeTransition *add READ addTransition WRITE setAddTransition NOTIFY addTransitionChanged) + Q_PROPERTY(QDeclarativeTransition *addDisplaced READ addDisplacedTransition WRITE setAddDisplacedTransition NOTIFY addDisplacedTransitionChanged) + Q_PROPERTY(QDeclarativeTransition *move READ moveTransition WRITE setMoveTransition NOTIFY moveTransitionChanged) + Q_PROPERTY(QDeclarativeTransition *moveDisplaced READ moveDisplacedTransition WRITE setMoveDisplacedTransition NOTIFY moveDisplacedTransitionChanged) + Q_PROPERTY(QDeclarativeTransition *remove READ removeTransition WRITE setRemoveTransition NOTIFY removeTransitionChanged) + Q_PROPERTY(QDeclarativeTransition *removeDisplaced READ removeDisplacedTransition WRITE setRemoveDisplacedTransition NOTIFY removeDisplacedTransitionChanged) + Q_PROPERTY(QDeclarativeComponent *highlight READ highlight WRITE setHighlight NOTIFY highlightChanged) Q_PROPERTY(QQuickItem *highlightItem READ highlightItem NOTIFY highlightItemChanged) Q_PROPERTY(bool highlightFollowsCurrentItem READ highlightFollowsCurrentItem WRITE setHighlightFollowsCurrentItem NOTIFY highlightFollowsCurrentItemChanged) @@ -120,6 +130,27 @@ public: void setHeader(QDeclarativeComponent *); QQuickItem *headerItem() const; + QDeclarativeTransition *populateTransition() const; + void setPopulateTransition(QDeclarativeTransition *transition); + + QDeclarativeTransition *addTransition() const; + void setAddTransition(QDeclarativeTransition *transition); + + QDeclarativeTransition *addDisplacedTransition() const; + void setAddDisplacedTransition(QDeclarativeTransition *transition); + + QDeclarativeTransition *moveTransition() const; + void setMoveTransition(QDeclarativeTransition *transition); + + QDeclarativeTransition *moveDisplacedTransition() const; + void setMoveDisplacedTransition(QDeclarativeTransition *transition); + + QDeclarativeTransition *removeTransition() const; + void setRemoveTransition(QDeclarativeTransition *transition); + + QDeclarativeTransition *removeDisplacedTransition() const; + void setRemoveDisplacedTransition(QDeclarativeTransition *transition); + QDeclarativeComponent *highlight() const; void setHighlight(QDeclarativeComponent *); @@ -173,6 +204,14 @@ signals: void headerItemChanged(); void footerItemChanged(); + void populateTransitionChanged(); + void addTransitionChanged(); + void addDisplacedTransitionChanged(); + void moveTransitionChanged(); + void moveDisplacedTransitionChanged(); + void removeTransitionChanged(); + void removeDisplacedTransitionChanged(); + void highlightChanged(); void highlightItemChanged(); void highlightFollowsCurrentItemChanged(); @@ -200,8 +239,6 @@ protected slots: void animStopped(); void trackedPositionChanged(); - - private: Q_DECLARE_PRIVATE(QQuickItemView) }; @@ -287,8 +324,53 @@ public: QString m_nextSection; }; +class QQuickViewTransitionAttached : public QObject +{ + Q_OBJECT + + Q_PROPERTY(int index READ index NOTIFY indexChanged) + Q_PROPERTY(QQuickItem* item READ item NOTIFY itemChanged) + Q_PROPERTY(QPointF destination READ destination NOTIFY destinationChanged) + + Q_PROPERTY(QList targetIndexes READ targetIndexes NOTIFY targetIndexesChanged) + Q_PROPERTY(QDeclarativeListProperty targetItems READ targetItems NOTIFY targetItemsChanged) + +public: + QQuickViewTransitionAttached(QObject *parent); + + int index() const { return m_index; } + QQuickItem *item() const { return m_item; } + QPointF destination() const { return m_destination; } + + QList targetIndexes() const { return m_targetIndexes; } + QDeclarativeListProperty targetItems(); + + static QQuickViewTransitionAttached *qmlAttachedProperties(QObject *); + +signals: + void indexChanged(); + void itemChanged(); + void destinationChanged(); + + void targetIndexesChanged(); + void targetItemsChanged(); + +private: + friend class FxViewItemTransitionManager; + int m_index; + QQuickItem *m_item; + QPointF m_destination; + + QList m_targetIndexes; + QList m_targetItems; +}; + QT_END_NAMESPACE + +QML_DECLARE_TYPE(QQuickViewTransitionAttached) +QML_DECLARE_TYPEINFO(QQuickViewTransitionAttached, QML_HAS_ATTACHED_PROPERTIES) + QT_END_HEADER #endif // QQUICKITEMVIEW_P_H diff --git a/src/quick/items/qquickitemview_p_p.h b/src/quick/items/qquickitemview_p_p.h index f191f0683c..6768149d74 100644 --- a/src/quick/items/qquickitemview_p_p.h +++ b/src/quick/items/qquickitemview_p_p.h @@ -45,6 +45,8 @@ #include "qquickitemview_p.h" #include "qquickflickable_p_p.h" #include "qquickvisualdatamodel_p.h" +#include "qquickvisualitemmodel_p.h" +#include #include @@ -52,12 +54,58 @@ QT_BEGIN_HEADER QT_BEGIN_NAMESPACE +QT_MODULE(Declarative) + + +class FxViewItem; +class FxViewItemTransitionManager : public QDeclarativeTransitionManager +{ +public: + enum TransitionType { + NoTransition, + PopulateTransition, + AddTransition, + MoveTransition, + RemoveTransition + }; + + FxViewItemTransitionManager(); + ~FxViewItemTransitionManager(); + + bool isActive() const; + void startTransition(FxViewItem *item, FxViewItemTransitionManager::TransitionType type, const QPointF &to, bool isTargetItem); + + bool m_active; + FxViewItem *m_item; + QPointF m_toPos; + FxViewItemTransitionManager::TransitionType m_type; + bool m_isTarget; + +protected: + virtual void finished(); +}; + + class FxViewItem { public: FxViewItem(QQuickItem *, bool own); virtual ~FxViewItem(); + qreal itemX() const; + qreal itemY() const; + + void setVisible(bool visible); + + void setNextTransition(FxViewItemTransitionManager::TransitionType, bool isTargetItem); + bool transitionScheduledOrRunning() const; + bool isPendingRemoval() const; + + bool prepareTransition(const QRectF &viewBounds); + void startTransition(); + void stopTransition(); + void finishedTransition(); + // these are positions and sizes along the current direction of scrolling/flicking virtual qreal position() const = 0; virtual qreal endPosition() const = 0; @@ -65,13 +113,26 @@ public: virtual qreal sectionSize() const = 0; virtual bool contains(qreal x, qreal y) const = 0; + virtual QQuickItemView *itemView() const = 0; QQuickItem *item; bool ownItem; int index; + bool releaseAfterTransition; QQuickItemViewAttached *attached; + + FxViewItemTransitionManager *transition; + QPointF nextTransitionTo; + FxViewItemTransitionManager::TransitionType nextTransitionType; + bool isTransitionTarget; + bool nextTransitionToSet; + +protected: + void moveTo(const QPointF &pos); + void resetTransitionData(); }; + class QQuickItemViewChangeSet { public: @@ -93,6 +154,7 @@ public: bool currentRemoved : 1; }; + class QQuickItemViewPrivate : public QQuickFlickablePrivate { Q_DECLARE_PUBLIC(QQuickItemView) @@ -101,20 +163,39 @@ public: struct ChangeResult { QDeclarativeNullableValue visiblePos; + bool changedFirstItem; qreal sizeChangesBeforeVisiblePos; qreal sizeChangesAfterVisiblePos; - bool changedFirstItem; - int changeBeforeVisible; + int countChangeBeforeVisible; + int countChangeAfterVisibleItems; + + ChangeResult() + : visiblePos(0), changedFirstItem(false), + sizeChangesBeforeVisiblePos(0), sizeChangesAfterVisiblePos(0), + countChangeBeforeVisible(0), countChangeAfterVisibleItems(0) {} ChangeResult(const QDeclarativeNullableValue &p) - : visiblePos(p), sizeChangesBeforeVisiblePos(0), sizeChangesAfterVisiblePos(0), - changedFirstItem(false), changeBeforeVisible(0) {} + : visiblePos(p), changedFirstItem(false), + sizeChangesBeforeVisiblePos(0), sizeChangesAfterVisiblePos(0), + countChangeBeforeVisible(0), countChangeAfterVisibleItems(0) {} + + ChangeResult &operator+=(const ChangeResult &other) { + if (&other == this) + return *this; + changedFirstItem &= other.changedFirstItem; + sizeChangesBeforeVisiblePos += other.sizeChangesBeforeVisiblePos; + sizeChangesAfterVisiblePos += other.sizeChangesAfterVisiblePos; + countChangeBeforeVisible += other.countChangeBeforeVisible; + countChangeAfterVisibleItems += other.countChangeAfterVisibleItems; + return *this; + } void reset() { + changedFirstItem = false; sizeChangesBeforeVisiblePos = 0.0; sizeChangesAfterVisiblePos = 0.0; - changedFirstItem = false; - changeBeforeVisible = 0; + countChangeBeforeVisible = 0; + countChangeAfterVisibleItems = 0; } }; @@ -130,6 +211,7 @@ public: int findLastVisibleIndex(int defaultValue = -1) const; FxViewItem *visibleItem(int modelIndex) const; FxViewItem *firstVisibleItem() const; + int findLastIndexInView() const; int mapFromModel(int modelIndex) const; virtual void init(); @@ -155,12 +237,22 @@ public: void updateVisibleIndex(); void positionViewAtIndex(int index, int mode); void applyPendingChanges(); - bool applyModelChanges(); + bool applyModelChanges(ChangeResult *insertionResult, ChangeResult *removalResult); bool applyRemovalChange(const QDeclarativeChangeSet::Remove &removal, ChangeResult *changeResult, int *removedCount); + void removeItem(FxViewItem *item, const QDeclarativeChangeSet::Remove &removal, ChangeResult *removeResult); void repositionFirstItem(FxViewItem *prevVisibleItemsFirst, qreal prevVisibleItemsFirstPos, FxViewItem *prevFirstVisible, ChangeResult *insertionResult, ChangeResult *removalResult); + void prepareVisibleItemTransitions(); + void prepareRemoveTransitions(QHash *removedItems); + bool prepareNonVisibleItemTransition(FxViewItem *item, const QRectF &viewBounds); + + bool hasItemTransitions() const; + void transitionNextReposition(FxViewItem *item, FxViewItemTransitionManager::TransitionType type, bool isTarget); + int findMoveKeyIndex(QDeclarativeChangeSet::MoveKey key, const QVector &changes) const; + void checkVisible() const; + void showVisibleItems() const; void markExtentsDirty() { if (layoutOrientation() == Qt::Vertical) @@ -201,12 +293,35 @@ public: QDeclarativeComponent *footerComponent; FxViewItem *footer; + QDeclarativeTransition *populateTransition; + QDeclarativeTransition *addTransition; + QDeclarativeTransition *addDisplacedTransition; + QDeclarativeTransition *moveTransition; + QDeclarativeTransition *moveDisplacedTransition; + QDeclarativeTransition *removeTransition; + QDeclarativeTransition *removeDisplacedTransition; + + QList addTransitionIndexes; + QList moveTransitionIndexes; + QList removeTransitionIndexes; + QList addTransitionTargets; + QList moveTransitionTargets; + QList removeTransitionTargets; + + struct MovedItem { + FxViewItem *item; + QDeclarativeChangeSet::MoveKey moveKey; + MovedItem(FxViewItem *i, QDeclarativeChangeSet::MoveKey k) + : item(i), moveKey(k) {} + }; + QList releasePendingTransition; + mutable qreal minExtent; mutable qreal maxExtent; bool ownModel : 1; bool wrap : 1; - bool inApplyModelChanges : 1; + bool disableLayout : 1; bool inViewportMoved : 1; bool forceLayout : 1; bool currentIndexCleared : 1; @@ -217,6 +332,8 @@ public: bool fillCacheBuffer : 1; bool inRequest : 1; bool requestedAsync : 1; + bool usePopulateTransition : 1; + bool runDelayedRemoveTransition : 1; protected: virtual Qt::Orientation layoutOrientation() const = 0; @@ -246,15 +363,19 @@ protected: virtual void visibleItemsChanged() {} virtual FxViewItem *newViewItem(int index, QQuickItem *item) = 0; + virtual void repositionItemAt(FxViewItem *item, int index, qreal sizeBuffer) = 0; virtual void repositionPackageItemAt(QQuickItem *item, int index) = 0; virtual void resetFirstItemPosition(qreal pos = 0.0) = 0; virtual void adjustFirstItem(qreal forwards, qreal backwards, int changeBeforeVisible) = 0; virtual void layoutVisibleItems(int fromModelIndex = 0) = 0; virtual void changedVisibleIndex(int newIndex) = 0; - virtual bool applyInsertionChange(const QDeclarativeChangeSet::Insert &insert, ChangeResult *changeResult, QList *newItems) = 0; + + virtual bool applyInsertionChange(const QDeclarativeChangeSet::Insert &insert, ChangeResult *changeResult, + QList *newItems, QList *movingIntoView) = 0; virtual bool needsRefillForAddedOrRemovedIndex(int) const { return false; } + virtual void translateAndTransitionItemsAfter(int afterIndex, const ChangeResult &insertionResult, const ChangeResult &removalResult) = 0; virtual void initializeViewItem(FxViewItem *) {} virtual void initializeCurrentItem() {} diff --git a/src/quick/items/qquicklistview.cpp b/src/quick/items/qquicklistview.cpp index 424edc5843..03be177e2c 100644 --- a/src/quick/items/qquicklistview.cpp +++ b/src/quick/items/qquicklistview.cpp @@ -94,6 +94,7 @@ public: virtual FxViewItem *newViewItem(int index, QQuickItem *item); virtual void initializeViewItem(FxViewItem *item); virtual void releaseItem(FxViewItem *item); + virtual void repositionItemAt(FxViewItem *item, int index, qreal sizeBuffer); virtual void repositionPackageItemAt(QQuickItem *item, int index); virtual void resetFirstItemPosition(qreal pos = 0.0); virtual void adjustFirstItem(qreal forwards, qreal backwards, int); @@ -104,7 +105,9 @@ public: virtual void setPosition(qreal pos); virtual void layoutVisibleItems(int fromModelIndex = 0); - virtual bool applyInsertionChange(const QDeclarativeChangeSet::Insert &insert, ChangeResult *changeResult, QList *addedItems); + + virtual bool applyInsertionChange(const QDeclarativeChangeSet::Insert &insert, ChangeResult *changeResult, QList *addedItems, QList *movingIntoView); + virtual void translateAndTransitionItemsAfter(int afterIndex, const ChangeResult &insertionResult, const ChangeResult &removalResult); virtual void updateSections(); QQuickItem *getSectionItem(const QString §ion); @@ -253,9 +256,9 @@ public: } qreal itemPosition() const { if (view->orientation() == QQuickListView::Vertical) - return item->y(); + return itemY(); else - return (view->effectiveLayoutDirection() == Qt::RightToLeft ? -item->width()-item->x() : item->x()); + return (view->effectiveLayoutDirection() == Qt::RightToLeft ? -item->width()-itemX() : itemX()); } qreal size() const { if (section) @@ -273,35 +276,26 @@ public: } qreal endPosition() const { if (view->orientation() == QQuickListView::Vertical) { - return item->y() + item->height(); + return itemY() + item->height(); } else { return (view->effectiveLayoutDirection() == Qt::RightToLeft - ? -item->x() - : item->x() + item->width()); + ? -itemX() + : itemX() + item->width()); } } void setPosition(qreal pos) { - if (view->orientation() == QQuickListView::Vertical) { - if (section) { + // position the section immediately even if there is a transition + if (section) { + if (view->orientation() == QQuickListView::Vertical) { section->setY(pos); - pos += section->height(); - } - item->setY(pos); - } else { - if (view->effectiveLayoutDirection() == Qt::RightToLeft) { - if (section) { - section->setX(-section->width()-pos); - pos += section->width(); - } - item->setX(-item->width()-pos); } else { - if (section) { + if (view->effectiveLayoutDirection() == Qt::RightToLeft) + section->setX(-section->width()-pos); + else section->setX(pos); - pos += section->width(); - } - item->setX(pos); } } + moveTo(pointForPosition(pos)); } void setSize(qreal size) { if (view->orientation() == QQuickListView::Vertical) @@ -310,12 +304,34 @@ public: item->setWidth(size); } bool contains(qreal x, qreal y) const { - return (x >= item->x() && x < item->x() + item->width() && - y >= item->y() && y < item->y() + item->height()); + return (x >= itemX() && x < itemX() + item->width() && + y >= itemY() && y < itemY() + item->height()); + } + QQuickItemView *itemView() const { + return view; } QQuickItem *section; QQuickListView *view; + +private: + QPointF pointForPosition(qreal pos) const { + if (view->orientation() == QQuickListView::Vertical) { + if (section) + pos += section->height(); + return QPointF(itemX(), pos); + } else { + if (view->effectiveLayoutDirection() == Qt::RightToLeft) { + if (section) + pos += section->width(); + return QPointF(-item->width() - pos, itemY()); + } else { + if (section) + pos += section->width(); + return QPointF(pos, itemY()); + } + } + } }; //---------------------------------------------------------------------------- @@ -401,8 +417,9 @@ qreal QQuickListViewPrivate::lastPosition() const qreal QQuickListViewPrivate::positionAt(int modelIndex) const { - if (FxViewItem *item = visibleItem(modelIndex)) + if (FxViewItem *item = visibleItem(modelIndex)) { return item->position(); + } if (!visibleItems.isEmpty()) { if (modelIndex < visibleIndex) { int count = visibleIndex - modelIndex; @@ -612,7 +629,8 @@ bool QQuickListViewPrivate::addVisibleItems(qreal fillFrom, qreal fillTo, bool d #endif if (!(item = static_cast(createItem(modelIndex, doBuffer)))) break; - item->setPosition(pos); + if (!(usePopulateTransition && populateTransition)) // pos will be set by layoutVisibleItems() + item->setPosition(pos); item->item->setVisible(!doBuffer); pos += item->size() + spacing; visibleItems.append(item); @@ -631,7 +649,8 @@ bool QQuickListViewPrivate::addVisibleItems(qreal fillFrom, qreal fillTo, bool d break; --visibleIndex; visiblePos -= item->size() + spacing; - item->setPosition(visiblePos); + if (!(usePopulateTransition && populateTransition)) // pos will be set by layoutVisibleItems() + item->setPosition(visiblePos); item->item->setVisible(!doBuffer); visibleItems.prepend(item); changed = true; @@ -654,6 +673,7 @@ bool QQuickListViewPrivate::removeNonVisibleItems(qreal bufferFrom, qreal buffer && (item = visibleItems.at(index)) && item->endPosition() < bufferFrom) { if (item->attached->delayRemove()) break; + if (item->size() > 0) { #ifdef DEBUG_DELEGATE_LIFECYCLE qDebug() << "refill: remove first" << visibleIndex << "top end pos" << item->endPosition(); @@ -663,7 +683,15 @@ bool QQuickListViewPrivate::removeNonVisibleItems(qreal bufferFrom, qreal buffer if (item->index != -1) visibleIndex++; visibleItems.removeAt(index); - releaseItem(item); + if (item->transitionScheduledOrRunning()) { +#ifdef DEBUG_DELEGATE_LIFECYCLE + qDebug() << "refill not releasing animating item" << item->index << item->item->objectName(); +#endif + item->releaseAfterTransition = true; + releasePendingTransition.append(item); + } else { + releaseItem(item); + } if (index == 0) break; item = visibleItems.at(--index); @@ -681,7 +709,15 @@ bool QQuickListViewPrivate::removeNonVisibleItems(qreal bufferFrom, qreal buffer qDebug() << "refill: remove last" << visibleIndex+visibleItems.count()-1 << item->position(); #endif visibleItems.removeLast(); - releaseItem(item); + if (item->transitionScheduledOrRunning()) { +#ifdef DEBUG_DELEGATE_LIFECYCLE + qDebug() << "refill not releasing animating item" << item->index << item->item->objectName(); +#endif + item->releaseAfterTransition = true; + releasePendingTransition.append(item); + } else { + releaseItem(item); + } changed = true; } @@ -713,11 +749,12 @@ void QQuickListViewPrivate::layoutVisibleItems(int fromModelIndex) qreal sum = firstItem->size(); qreal pos = firstItem->position() + firstItem->size() + spacing; firstItem->item->setVisible(firstItem->endPosition() >= from && firstItem->position() <= to); + for (int i=1; i < visibleItems.count(); ++i) { FxListItemSG *item = static_cast(visibleItems.at(i)); if (item->index >= fromModelIndex) { item->setPosition(pos); - item->item->setVisible(item->endPosition() >= from && item->position() <= to); + item->setVisible(item->endPosition() >= from && item->position() <= to); } pos += item->size() + spacing; sum += item->size(); @@ -726,12 +763,16 @@ void QQuickListViewPrivate::layoutVisibleItems(int fromModelIndex) averageSize = qRound(sum / visibleItems.count()); // move current item if it is not a visible item. - if (currentIndex >= 0 && currentItem && !fixedCurrent) { + if (currentIndex >= 0 && currentItem && !fixedCurrent) static_cast(currentItem)->setPosition(positionAt(currentIndex)); - } } } +void QQuickListViewPrivate::repositionItemAt(FxViewItem *item, int index, qreal sizeBuffer) +{ + static_cast(item)->setPosition(positionAt(index) + sizeBuffer); +} + void QQuickListViewPrivate::repositionPackageItemAt(QQuickItem *item, int index) { Q_Q(QQuickListView); @@ -1132,13 +1173,17 @@ void QQuickListViewPrivate::initializeCurrentItem() if (currentItem) { FxListItemSG *listItem = static_cast(currentItem); - if (currentIndex == visibleIndex - 1 && visibleItems.count()) { - // We can calculate exact postion in this case - listItem->setPosition(visibleItems.first()->position() - currentItem->size() - spacing); - } else { - // Create current item now and position as best we can. - // Its position will be corrected when it becomes visible. - listItem->setPosition(positionAt(currentIndex)); + // don't reposition the item if it's about to be transitioned to another position + FxViewItem *actualItem = visibleItem(currentIndex); + if ((!actualItem || !actualItem->transitionScheduledOrRunning())) { + if (currentIndex == visibleIndex - 1 && visibleItems.count()) { + // We can calculate exact postion in this case + listItem->setPosition(visibleItems.first()->position() - currentItem->size() - spacing); + } else { + // Create current item now and position as best we can. + // Its position will be corrected when it becomes visible. + listItem->setPosition(positionAt(currentIndex)); + } } // Avoid showing section delegate twice. We still need the section heading so that @@ -1653,27 +1698,34 @@ QQuickListView::~QQuickListView() /*! \qmlattachedproperty bool QtQuick2::ListView::delayRemove - This attached property holds whether the delegate may be destroyed. - It is attached to each instance of the delegate. + This attached property holds whether the delegate may be destroyed. It + is attached to each instance of the delegate. The default value is false. It is sometimes necessary to delay the destruction of an item - until an animation completes. - - The example delegate below ensures that the animation completes before - the item is removed from the list. + until an animation completes. The example delegate below ensures that the + animation completes before the item is removed from the list. \snippet doc/src/snippets/declarative/listview/listview.qml delayRemove + + If a \l remove transition has been specified, it will not be applied until + delayRemove is returned to \c false. */ /*! \qmlattachedsignal QtQuick2::ListView::onAdd() - This attached handler is called immediately after an item is added to the view. + This attached signal handler is called immediately after an item is added to the view. + + If an \l add transition is specified, it is applied immediately after + this signal handler is called. */ /*! \qmlattachedsignal QtQuick2::ListView::onRemove() This attached handler is called immediately before an item is removed from the view. + + If a \l remove transition has been specified, it is applied after + this signal handler is called, providing that delayRemove is false. */ /*! @@ -2191,6 +2243,231 @@ void QQuickListView::setSnapMode(SnapMode mode) \sa footer, headerItem */ +/*! + \qmlproperty Transition QtQuick2::ListView::populate + This property holds the transition to apply to items that are initially created for a + view. + + This transition is applied to all the items that are created when: + + \list + \o The view is first created + \o The view's \l model changes + \o The view's \l model is \l {QAbstractItemModel::reset}{reset}, if the model is a QAbstractItemModel subclass + \endlist + + For example, here is a view that specifies such a transition: + + \code + ListView { + ... + populate: Transition { + NumberAnimation { properties: "x,y"; duration: 1000 } + } + } + \endcode + + When the view is initialized, the view will create all the necessary items for the view, + then animate them to their correct positions within the view over one second. + + For more details and examples on how to use view transitions, see the ViewTransition + documentation. + + \sa add, ViewTransition +*/ + +/*! + \qmlproperty Transition QtQuick2::ListView::add + This property holds the transition to apply to items that are added within the view. + + The transition is applied to items that have been added to the visible area of the view. For + example, here is a view that specifies such a transition: + + \code + ListView { + ... + add: Transition { + NumberAnimation { properties: "x,y"; from: 100; duration: 1000 } + } + } + \endcode + + Whenever an item is added to the above view, the item will be animated from the position (100,100) + to its final x,y position within the view, over one second. The transition only applies to + the new items that are added to the view; it does not apply to the items below that are + displaced by the addition of the new items. To animate the displaced items, set the \l + addDisplaced property. + + For more details and examples on how to use view transitions, see the ViewTransition + documentation. + + \note This transition is not applied to the items that are created when the view is initially + populated, or when the view's \l model changes. In those cases, the \l populate transition is + applied instead. + + \sa addDisplaced, populate, ViewTransition +*/ + +/*! + \qmlproperty Transition QtQuick2::ListView::addDisplaced + This property holds the transition to apply to items in the view that are displaced by other + items that have been added to the view. + + The transition is applied to items that are currently visible and have been displaced by newly + added items. For example, here is a view that specifies such a transition: + + \code + ListView { + ... + addDisplaced: Transition { + NumberAnimation { properties: "x,y"; duration: 1000 } + } + } + \endcode + + Whenever an item is added to the above view, all items beneath the new item are displaced, causing + them to move down (or sideways, if horizontally orientated) within the view. As this + displacement occurs, the items' movement to their new x,y positions within the view will be + animated by a NumberAnimation over one second, as specified. This transition is not applied to + the new item that has been added to the view; to animate the added items, set the \l add + property. + + For more details and examples on how to use view transitions, see the ViewTransition + documentation. + + \note This transition is not applied to the items that are created when the view is initially + populated, or when the view's \l model changes. In those cases, the \l populate transition is + applied instead. + + \sa add, populate, ViewTransition +*/ + +/*! + \qmlproperty Transition QtQuick2::ListView::move + This property holds the transition to apply to items in the view that are moved by a move + operation. + + The transition is applied to items that are moving within the view or are moving + into the view as a result of a move operation in the view's model. For example: + + \code + ListView { + ... + move: Transition { + NumberAnimation { properties: "x,y"; duration: 1000 } + } + } + \endcode + + Whenever an item is moved within the above view, the item will be animated to its new position in + the view over one second. The transition only applies to the items that are the subject of the + move operation in the model; it does not apply to the items below them that are displaced by + the move operation. To animate the displaced items, set the \l moveDisplaced property. + + For more details and examples on how to use view transitions, see the ViewTransition + documentation. + + \sa moveDisplaced, ViewTransition +*/ + +/*! + \qmlproperty Transition QtQuick2::ListView::moveDisplaced + This property holds the transition to apply to items in the view that are displaced by a + move operation in the view. + + The transition is applied to items that are currently visible and have been displaced following + a move operation in the view's model. For example, here is a view that specifies such a transition: + + \code + ListView { + ... + moveDisplaced: Transition { + NumberAnimation { properties: "x,y"; duration: 1000 } + } + } + \endcode + + Whenever an item moves within (or moves into) the above view, all items beneath it are + displaced, causing them to move upwards (or sideways, if horizontally orientated) within the + view. As this displacement occurs, the items' movement to their new x,y positions within the + view will be animated by a NumberAnimation over one second, as specified. This transition is + not applied to the item that are actually the subject of the move operation; to animate the + moved items, set the \l move property. + + For more details and examples on how to use view transitions, see the ViewTransition + documentation. + + \sa move, ViewTransition +*/ + +/*! + \qmlproperty Transition QtQuick2::ListView::remove + This property holds the transition to apply to items that are removed from the view. + + The transition is applied to items that have been removed from the visible area of the view. For + example: + + \code + ListView { + ... + remove: Transition { + ParallelAnimation { + NumberAnimation { property: "opacity"; to: 0; duration: 1000 } + NumberAnimation { properties: "x,y"; to: 100; duration: 1000 } + } + } + } + \endcode + + Whenever an item is removed from the above view, the item will be animated to the position (100,100) + over one second, and in parallel will also change its opacity to 0. The transition + only applies to the items that are removed from the view; it does not apply to the items below + them that are displaced by the removal of the items. To animate the displaced items, set the \l + removeDisplaced property. + + Note that by the time the transition is applied, the item has already been removed from the + model; any references to the model data for the removed index will not be valid. + + Additionally, if the \l delayRemove attached property has been set for a delegate item, the + remove transition will not be applied until \l delayRemove becomes false again. + + For more details and examples on how to use view transitions, see the ViewTransition + documentation. + + \sa removeDisplaced, ViewTransition +*/ + +/*! + \qmlproperty Transition QtQuick2::ListView::removeDisplaced + This property holds the transition to apply to items in the view that are displaced by the + removal of other items in the view. + + The transition is applied to items that are currently visible and have been displaced by + the removal of items. For example, here is a view that specifies such a transition: + + \code + ListView { + ... + removeDisplaced: Transition { + NumberAnimation { properties: "x,y"; duration: 1000 } + } + } + \endcode + + Whenever an item is removed from the above view, all items beneath it are displaced, causing + them to move upwards (or sideways, if horizontally orientated) within the view. As this + displacement occurs, the items' movement to their new x,y positions within the view will be + animated by a NumberAnimation over one second, as specified. This transition is not applied to + the item that has actually been removed from the view; to animate the removed items, set the + \l remove property. + + For more details and examples on how to use view transitions, see the ViewTransition + documentation. + + \sa remove, ViewTransition +*/ + + void QQuickListView::viewportMoved() { Q_D(QQuickListView); @@ -2385,7 +2662,7 @@ void QQuickListView::updateSections() } } -bool QQuickListViewPrivate::applyInsertionChange(const QDeclarativeChangeSet::Insert &change, ChangeResult *insertResult, QList *addedItems) +bool QQuickListViewPrivate::applyInsertionChange(const QDeclarativeChangeSet::Insert &change, ChangeResult *insertResult, QList *addedItems, QList *movingIntoView) { int modelIndex = change.index; int count = change.count; @@ -2450,8 +2727,10 @@ bool QQuickListViewPrivate::applyInsertionChange(const QDeclarativeChangeSet::In visibleItems.insert(insertionIdx, item); if (insertionIdx == 0) insertResult->changedFirstItem = true; - if (!change.isMove()) + if (!change.isMove()) { addedItems->append(item); + transitionNextReposition(item, FxViewItemTransitionManager::AddTransition, true); + } insertResult->sizeChangesBeforeVisiblePos += item->size() + spacing; pos -= item->size() + spacing; } @@ -2464,6 +2743,7 @@ bool QQuickListViewPrivate::applyInsertionChange(const QDeclarativeChangeSet::In FxViewItem *item = 0; if (change.isMove() && (item = currentChanges.removedItems.take(change.moveKey(modelIndex + i)))) item->index = modelIndex + i; + bool newItem = !item; if (!item) item = createItem(modelIndex + i); if (!item) @@ -2472,8 +2752,15 @@ bool QQuickListViewPrivate::applyInsertionChange(const QDeclarativeChangeSet::In visibleItems.insert(index, item); if (index == 0) insertResult->changedFirstItem = true; - if (!change.isMove()) + if (change.isMove()) { + // we know this is a move target, since move displaced items that are + // shuffled into view due to a move would be added in refill() + if (moveTransition && newItem) + movingIntoView->append(MovedItem(item, change.moveKey(item->index))); + } else { addedItems->append(item); + transitionNextReposition(item, FxViewItemTransitionManager::AddTransition, true); + } insertResult->sizeChangesAfterVisiblePos += item->size() + spacing; pos += item->size() + spacing; ++index; @@ -2484,6 +2771,10 @@ bool QQuickListViewPrivate::applyInsertionChange(const QDeclarativeChangeSet::In FxViewItem *item = visibleItems.at(index); if (item->index != -1) item->index += count; + if (change.isMove()) + transitionNextReposition(item, FxViewItemTransitionManager::MoveTransition, false); + else + transitionNextReposition(item, FxViewItemTransitionManager::AddTransition, false); } updateVisibleIndex(); @@ -2491,6 +2782,34 @@ bool QQuickListViewPrivate::applyInsertionChange(const QDeclarativeChangeSet::In return visibleItems.count() > prevVisibleCount; } +void QQuickListViewPrivate::translateAndTransitionItemsAfter(int afterModelIndex, const ChangeResult &insertionResult, const ChangeResult &removalResult) +{ + Q_UNUSED(insertionResult); + + int markerItemIndex = -1; + for (int i=0; iindex == afterModelIndex) { + markerItemIndex = i; + break; + } + } + if (markerItemIndex < 0) + return; + + const qreal viewEndPos = isContentFlowReversed() ? -position() : position() + size(); + qreal sizeRemoved = -removalResult.sizeChangesAfterVisiblePos + - (removalResult.countChangeAfterVisibleItems * (averageSize + spacing)); + + for (int i=markerItemIndex+1; iposition() < viewEndPos; i++) { + FxListItemSG *listItem = static_cast(visibleItems[i]); + if (!listItem->transitionScheduledOrRunning()) { + qreal pos = listItem->position(); + listItem->setPosition(pos - sizeRemoved); + transitionNextReposition(listItem, FxViewItemTransitionManager::RemoveTransition, false); + listItem->setPosition(pos); + } + } +} /*! \qmlmethod QtQuick2::ListView::positionViewAtIndex(int index, PositionMode mode) diff --git a/tests/auto/qtquick2/qquickgridview/data/addTransitions.qml b/tests/auto/qtquick2/qquickgridview/data/addTransitions.qml new file mode 100644 index 0000000000..faea02a50d --- /dev/null +++ b/tests/auto/qtquick2/qquickgridview/data/addTransitions.qml @@ -0,0 +1,129 @@ +import QtQuick 2.0 + +Rectangle { + id: root + width: 550 + height: 600 + + property int duration: 10 + property int count: grid.count + + Component { + id: myDelegate + + Rectangle { + id: wrapper + + property string nameData: name + + objectName: "wrapper" + width: 80 + height: 60 + border.width: 1 + Column { + Text { text: index } + Text { + text: wrapper.x + ", " + wrapper.y + } + Text { + id: textName + objectName: "textName" + text: name + } + } + color: GridView.isCurrentItem ? "lightsteelblue" : "white" + + onXChanged: checkPos() + onYChanged: checkPos() + + function checkPos() { + if (Qt.point(x, y) == targetItems_transitionFrom) + model_targetItems_transitionFrom.addItem(name, "") + if (Qt.point(x, y) == displacedItems_transitionVia) + model_displacedItems_transitionVia.addItem(name, "") + } + } + } + + GridView { + id: grid + + property int targetTransitionsDone + property int displaceTransitionsDone + + property var targetTrans_items: new Object() + property var targetTrans_targetIndexes: new Array() + property var targetTrans_targetItems: new Array() + + property var displacedTrans_items: new Object() + property var displacedTrans_targetIndexes: new Array() + property var displacedTrans_targetItems: new Array() + + objectName: "grid" + width: 240 + height: 320 + cellWidth: 80 + cellHeight: 60 + anchors.centerIn: parent + model: testModel + delegate: myDelegate + + // for QDeclarativeListProperty types + function copyList(propList) { + var temp = new Array() + for (var i=0; i #include #include +#include #include +#include #include "../../shared/util.h" #include "../shared/viewtestutil.h" #include "../shared/visualtestutil.h" @@ -127,6 +129,23 @@ private slots: void cacheBuffer(); void asynchronous(); void unrequestedVisibility(); + + void populateTransitions(); + void populateTransitions_data(); + void addTransitions(); + void addTransitions_data(); + void moveTransitions(); + void moveTransitions_data(); + void removeTransitions(); + void removeTransitions_data(); + void multipleTransitions(); + void multipleTransitions_data(); + +private: + QList toIntList(const QVariantList &list); + void matchIndexLists(const QVariantList &indexLists, const QList &expectedIndexes); + void matchItemsAndIndexes(const QVariantMap &items, const QaimModel &model, const QList &expectedIndexes); + void matchItemLists(const QVariantList &itemLists, const QList &expectedItems); }; tst_QQuickGridView::tst_QQuickGridView() @@ -500,13 +519,15 @@ void tst_QQuickGridView::insertBeforeVisible() QTRY_VERIFY(contentItem != 0); gridview->setCacheBuffer(cacheBuffer); + QTRY_COMPARE(QQuickItemPrivate::get(gridview)->polishScheduled, false); // trigger a refill (not just setting contentY) so that the visibleItems grid is updated int firstVisibleIndex = 12; // move to an index where the top item is not visible gridview->setContentY(firstVisibleIndex/3 * 60.0); gridview->setCurrentIndex(firstVisibleIndex); - qApp->processEvents(); + QTRY_COMPARE(QQuickItemPrivate::get(gridview)->polishScheduled, false); + QTRY_COMPARE(gridview->currentIndex(), firstVisibleIndex); QQuickItem *item = findItem(contentItem, "wrapper", firstVisibleIndex); QVERIFY(item); @@ -521,6 +542,7 @@ void tst_QQuickGridView::insertBeforeVisible() // now, moving to the top of the view should position the inserted items correctly int itemsOffsetAfterMove = (insertCount / 3) * -60.0; gridview->setCurrentIndex(0); + QTRY_COMPARE(QQuickItemPrivate::get(gridview)->polishScheduled, false); QTRY_COMPARE(gridview->currentIndex(), 0); QTRY_COMPARE(gridview->contentY(), 0.0 + itemsOffsetAfterMove); @@ -1283,12 +1305,19 @@ void tst_QQuickGridView::multipleChanges() } case ListChange::Removed: model.removeItems(changes[i].index, changes[i].count); + QTRY_COMPARE(QQuickItemPrivate::get(gridview)->polishScheduled, false); break; case ListChange::Moved: model.moveItems(changes[i].index, changes[i].to, changes[i].count); + QTRY_COMPARE(QQuickItemPrivate::get(gridview)->polishScheduled, false); break; case ListChange::SetCurrent: gridview->setCurrentIndex(changes[i].index); + QTRY_COMPARE(QQuickItemPrivate::get(gridview)->polishScheduled, false); + break; + case ListChange::SetContentY: + gridview->setContentY(changes[i].pos); + QTRY_COMPARE(QQuickItemPrivate::get(gridview)->polishScheduled, false); break; } } @@ -2005,10 +2034,10 @@ void tst_QQuickGridView::componentChanges() QTRY_VERIFY(gridView); QDeclarativeComponent component(canvas->engine()); - component.setData("import QtQuick 2.0; Rectangle { color: \"blue\"; }", QUrl::fromLocalFile("")); + component.setData("import QtQuick 1.0; Rectangle { color: \"blue\"; }", QUrl::fromLocalFile("")); QDeclarativeComponent delegateComponent(canvas->engine()); - delegateComponent.setData("import QtQuick 2.0; Text { text: 'Name: ' + name }", QUrl::fromLocalFile("")); + delegateComponent.setData("import QtQuick 1.0; Text { text: 'Name: ' + name }", QUrl::fromLocalFile("")); QSignalSpy highlightSpy(gridView, SIGNAL(highlightChanged())); QSignalSpy delegateSpy(gridView, SIGNAL(delegateChanged())); @@ -3769,6 +3798,898 @@ void tst_QQuickGridView::unaligned() delete canvas; } +void tst_QQuickGridView::populateTransitions() +{ + QFETCH(bool, staticallyPopulate); + QFETCH(bool, dynamicallyPopulate); + QFETCH(bool, usePopulateTransition); + + QPointF transitionFrom(-50, -50); + QPointF transitionVia(100, 100); + QaimModel model_transitionFrom; + QaimModel model_transitionVia; + + QaimModel model; + if (staticallyPopulate) { + for (int i = 0; i < 30; i++) + model.addItem("item" + QString::number(i), ""); + } + + QQuickView *canvas = createView(); + canvas->rootContext()->setContextProperty("testModel", &model); + canvas->rootContext()->setContextProperty("usePopulateTransition", usePopulateTransition); + canvas->rootContext()->setContextProperty("dynamicallyPopulate", dynamicallyPopulate); + canvas->rootContext()->setContextProperty("transitionFrom", transitionFrom); + canvas->rootContext()->setContextProperty("transitionVia", transitionVia); + canvas->rootContext()->setContextProperty("model_transitionFrom", &model_transitionFrom); + canvas->rootContext()->setContextProperty("model_transitionVia", &model_transitionVia); + canvas->setSource(testFileUrl("populateTransitions.qml")); + canvas->show(); + + QQuickGridView *gridview = findItem(canvas->rootObject(), "grid"); + QVERIFY(gridview); + QQuickItem *contentItem = gridview->contentItem(); + QVERIFY(contentItem); + + if (staticallyPopulate || dynamicallyPopulate) { + // check the populate transition is run + if (usePopulateTransition) { + QTRY_COMPARE(gridview->property("countPopulateTransitions").toInt(), 19); + } else { + QTRY_COMPARE(QQuickItemPrivate::get(gridview)->polishScheduled, false); + QTRY_COMPARE(gridview->property("countPopulateTransitions").toInt(), 0); + } + QTRY_COMPARE(gridview->property("countAddTransitions").toInt(), 0); + } else { + QTRY_COMPARE(QQuickItemPrivate::get(gridview)->polishScheduled, false); + } + + int itemCount = findItems(contentItem, "wrapper").count(); + if (usePopulateTransition) + QCOMPARE(itemCount, gridview->property("countPopulateTransitions").toInt()); + for (int i=0; i < model.count() && i < itemCount; ++i) { + QQuickItem *item = findItem(contentItem, "wrapper", i); + QVERIFY2(item, QTest::toString(QString("Item %1 not found").arg(i))); + QCOMPARE(item->x(), (i%3)*80.0); + QCOMPARE(item->y(), (i/3)*60.0); + QQuickText *name = findItem(contentItem, "textName", i); + QVERIFY(name != 0); + QTRY_COMPARE(name->text(), model.name(i)); + } + + // add an item and check this is done with add transition, not populate + model.insertItem(0, "another item", ""); + QTRY_COMPARE(gridview->property("countAddTransitions").toInt(), 1); + QTRY_COMPARE(gridview->property("countPopulateTransitions").toInt(), + (usePopulateTransition && (staticallyPopulate || dynamicallyPopulate)) ? 19 : 0); + + // clear the model + canvas->rootContext()->setContextProperty("testModel", QVariant()); + QTRY_COMPARE(gridview->count(), 0); + QTRY_COMPARE(findItems(contentItem, "wrapper").count(), 0); + gridview->setProperty("countPopulateTransitions", 0); + gridview->setProperty("countAddTransitions", 0); + + // set to a valid model and check populate transition is run a second time + model.clear(); + for (int i = 0; i < 30; i++) + model.addItem("item" + QString::number(i), ""); + canvas->rootContext()->setContextProperty("testModel", &model); + QTRY_COMPARE(QQuickItemPrivate::get(gridview)->polishScheduled, false); + + QTRY_COMPARE(gridview->property("countPopulateTransitions").toInt(), usePopulateTransition ? 19 : 0); + QTRY_COMPARE(gridview->property("countAddTransitions").toInt(), 0); + + itemCount = findItems(contentItem, "wrapper").count(); + if (usePopulateTransition) + QCOMPARE(itemCount, gridview->property("countPopulateTransitions").toInt()); + for (int i=0; i < model.count() && i < itemCount; ++i) { + QQuickItem *item = findItem(contentItem, "wrapper", i); + QVERIFY2(item, QTest::toString(QString("Item %1 not found").arg(i))); + QCOMPARE(item->x(), (i%3)*80.0); + QCOMPARE(item->y(), (i/3)*60.0); + QQuickText *name = findItem(contentItem, "textName", i); + QVERIFY(name != 0); + QTRY_COMPARE(name->text(), model.name(i)); + } + + // reset model and check populate transition is run again + gridview->setProperty("countPopulateTransitions", 0); + gridview->setProperty("countAddTransitions", 0); + model.reset(); + QTRY_COMPARE(gridview->property("countPopulateTransitions").toInt(), usePopulateTransition ? 19 : 0); + QTRY_COMPARE(gridview->property("countAddTransitions").toInt(), 0); + + itemCount = findItems(contentItem, "wrapper").count(); + if (usePopulateTransition) + QCOMPARE(itemCount, gridview->property("countPopulateTransitions").toInt()); + for (int i=0; i < model.count() && i < itemCount; ++i) { + QQuickItem *item = findItem(contentItem, "wrapper", i); + QVERIFY2(item, QTest::toString(QString("Item %1 not found").arg(i))); + QCOMPARE(item->x(), (i%3)*80.0); + QCOMPARE(item->y(), (i/3)*60.0); + QQuickText *name = findItem(contentItem, "textName", i); + QVERIFY(name != 0); + QTRY_COMPARE(name->text(), model.name(i)); + } + + delete canvas; +} + +void tst_QQuickGridView::populateTransitions_data() +{ + QTest::addColumn("staticallyPopulate"); + QTest::addColumn("dynamicallyPopulate"); + QTest::addColumn("usePopulateTransition"); + + QTest::newRow("static") << true << false << true; + QTest::newRow("static, no populate") << true << false << false; + + QTest::newRow("dynamic") << false << true << true; + QTest::newRow("dynamic, no populate") << false << true << false; + + QTest::newRow("empty to start with") << false << false << true; + QTest::newRow("empty to start with, no populate") << false << false << false; +} + +void tst_QQuickGridView::addTransitions() +{ + QFETCH(int, initialItemCount); + QFETCH(bool, shouldAnimateTargets); + QFETCH(qreal, contentY); + QFETCH(int, insertionIndex); + QFETCH(int, insertionCount); + QFETCH(ListRange, expectedDisplacedIndexes); + + // added items should start here + QPointF targetItems_transitionFrom(-50, -50); + + // displaced items should pass through this point + QPointF displacedItems_transitionVia(100, 100); + + QaimModel model; + for (int i = 0; i < initialItemCount; i++) + model.addItem("Original item" + QString::number(i), ""); + QaimModel model_targetItems_transitionFrom; + QaimModel model_displacedItems_transitionVia; + + QQuickView *canvas = createView(); + QDeclarativeContext *ctxt = canvas->rootContext(); + ctxt->setContextProperty("testModel", &model); + ctxt->setContextProperty("model_targetItems_transitionFrom", &model_targetItems_transitionFrom); + ctxt->setContextProperty("model_displacedItems_transitionVia", &model_displacedItems_transitionVia); + ctxt->setContextProperty("targetItems_transitionFrom", targetItems_transitionFrom); + ctxt->setContextProperty("displacedItems_transitionVia", displacedItems_transitionVia); + canvas->setSource(testFileUrl("addTransitions.qml")); + canvas->show(); + + QQuickGridView *gridview = findItem(canvas->rootObject(), "grid"); + QTRY_VERIFY(gridview != 0); + QQuickItem *contentItem = gridview->contentItem(); + QVERIFY(contentItem != 0); + QTRY_COMPARE(QQuickItemPrivate::get(gridview)->polishScheduled, false); + + if (contentY != 0) { + gridview->setContentY(contentY); + QTRY_COMPARE(QQuickItemPrivate::get(gridview)->polishScheduled, false); + } + + QList > expectedDisplacedValues = expectedDisplacedIndexes.getModelDataValues(model); + + // only target items that will become visible should be animated + QList > newData; + QList > expectedTargetData; + QList targetIndexes; + if (shouldAnimateTargets) { + for (int i=insertionIndex; i= (contentY / 60)*3 && i < qCeil((contentY + gridview->height()) / 60.0)*3) { + expectedTargetData << newData.last(); + targetIndexes << i; + } + } + QVERIFY(expectedTargetData.count() > 0); + } + + // start animation + if (!newData.isEmpty()) { + model.insertItems(insertionIndex, newData); + QTRY_COMPARE(model.count(), gridview->count()); + } + + QList targetItems = findItems(contentItem, "wrapper", targetIndexes); + + if (shouldAnimateTargets) { + QTRY_COMPARE(gridview->property("targetTransitionsDone").toInt(), expectedTargetData.count()); + QTRY_COMPARE(gridview->property("displaceTransitionsDone").toInt(), + expectedDisplacedIndexes.isValid() ? expectedDisplacedIndexes.count() : 0); + + // check the target and displaced items were animated + model_targetItems_transitionFrom.matchAgainst(expectedTargetData, "wasn't animated from target 'from' pos", "shouldn't have been animated from target 'from' pos"); + model_displacedItems_transitionVia.matchAgainst(expectedDisplacedValues, "wasn't animated with displaced anim", "shouldn't have been animated with displaced anim"); + + // check attached properties + matchItemsAndIndexes(gridview->property("targetTrans_items").toMap(), model, targetIndexes); + matchIndexLists(gridview->property("targetTrans_targetIndexes").toList(), targetIndexes); + matchItemLists(gridview->property("targetTrans_targetItems").toList(), targetItems); + if (expectedDisplacedIndexes.isValid()) { + // adjust expectedDisplacedIndexes to their final values after the move + QList displacedIndexes = adjustIndexesForAddDisplaced(expectedDisplacedIndexes.indexes, insertionIndex, insertionCount); + matchItemsAndIndexes(gridview->property("displacedTrans_items").toMap(), model, displacedIndexes); + matchIndexLists(gridview->property("displacedTrans_targetIndexes").toList(), targetIndexes); + matchItemLists(gridview->property("displacedTrans_targetItems").toList(), targetItems); + } + } else { + QTRY_COMPARE(model_targetItems_transitionFrom.count(), 0); + QTRY_COMPARE(model_displacedItems_transitionVia.count(), 0); + } + + QList items = findItems(contentItem, "wrapper"); + int firstVisibleIndex = -1; + for (int i=0; iy() >= contentY) { + QDeclarativeExpression e(qmlContext(items[i]), items[i], "index"); + firstVisibleIndex = e.evaluate().toInt(); + break; + } + } + QVERIFY2(firstVisibleIndex >= 0, QTest::toString(firstVisibleIndex)); + + // verify all items moved to the correct final positions + int itemCount = findItems(contentItem, "wrapper").count(); + for (int i = firstVisibleIndex; i < model.count() && i < itemCount; ++i) { + QQuickItem *item = findItem(contentItem, "wrapper", i); + QVERIFY2(item, QTest::toString(QString("Item %1 not found").arg(i))); + QCOMPARE(item->x(), (i%3)*80.0); + QCOMPARE(item->y(), (i/3)*60.0); + QQuickText *name = findItem(contentItem, "textName", i); + QVERIFY(name != 0); + QCOMPARE(name->text(), model.name(i)); + } + + delete canvas; +} + +void tst_QQuickGridView::addTransitions_data() +{ + QTest::addColumn("initialItemCount"); + QTest::addColumn("contentY"); + QTest::addColumn("shouldAnimateTargets"); + QTest::addColumn("insertionIndex"); + QTest::addColumn("insertionCount"); + QTest::addColumn("expectedDisplacedIndexes"); + + // if inserting a full row before visible index, items don't appear or animate in, even if there are > 1 new items + QTest::newRow("insert 1, just before start") + << 30 << 20.0 << false + << 0 << 1 << ListRange(); + QTest::newRow("insert 1, way before start") + << 30 << 20.0 << false + << 0 << 1 << ListRange(); + QTest::newRow("insert multiple, just before start") + << 30 << 100.0 << false + << 0 << 3 << ListRange(); + QTest::newRow("insert multiple (< 1 row), just before start") + << 30 << 100.0 << false + << 0 << 2 << ListRange(); + QTest::newRow("insert multiple, way before start") + << 30 << 100.0 << false + << 0 << 3 << ListRange(); + + QTest::newRow("insert 1 at start") + << 30 << 0.0 << true + << 0 << 1 << ListRange(0, 17); + QTest::newRow("insert multiple at start") + << 30 << 0.0 << true + << 0 << 3 << ListRange(0, 17); + QTest::newRow("insert multiple (> 1 row) at start") + << 30 << 0.0 << true + << 0 << 5 << ListRange(0, 17); + QTest::newRow("insert 1 at start, content y not 0") + << 30 << 60.0 << true // first visible is index 3 + << 3 << 1 << ListRange(0 + 3, 17 + 3); + QTest::newRow("insert multiple at start, content y not 0") + << 30 << 60.0 << true // first visible is index 3 + << 3 << 3 << ListRange(0 + 3, 17 + 3); + QTest::newRow("insert multiple (> 1 row) at start, content y not 0") + << 30 << 60.0 << true // first visible is index 3 + << 3 << 5 << ListRange(0 + 3, 17 + 3); + + QTest::newRow("insert 1 at start, to empty grid") + << 0 << 0.0 << true + << 0 << 1 << ListRange(); + QTest::newRow("insert multiple at start, to empty grid") + << 0 << 0.0 << true + << 0 << 3 << ListRange(); + + QTest::newRow("insert 1 at middle") + << 30 << 0.0 << true + << 7 << 1 << ListRange(7, 17); + QTest::newRow("insert multiple at middle") + << 30 << 0.0 << true + << 7 << 3 << ListRange(7, 17); + QTest::newRow("insert multiple (> 1 row) at middle") + << 30 << 0.0 << true + << 7 << 5 << ListRange(7, 17); + + QTest::newRow("insert 1 at bottom") + << 30 << 0.0 << true + << 17 << 1 << ListRange(17, 17); + QTest::newRow("insert multiple at bottom") + << 30 << 0.0 << true + << 17 << 3 << ListRange(17, 17); + QTest::newRow("insert 1 at bottom, content y not 0") + << 30 << 20.0 * 3 << true + << 17 + 3 << 1 << ListRange(17 + 3, 17 + 3); + QTest::newRow("insert multiple at bottom, content y not 0") + << 30 << 20.0 * 3 << true + << 17 + 3 << 3 << ListRange(17 + 3, 17 + 3); + + + // items added after the last visible will not be animated in, since they + // do not appear in the final view + QTest::newRow("insert 1 after end") + << 30 << 0.0 << false + << 18 << 1 << ListRange(); + QTest::newRow("insert multiple after end") + << 30 << 0.0 << false + << 18 << 3 << ListRange(); +} + +void tst_QQuickGridView::moveTransitions() +{ + QFETCH(int, initialItemCount); + QFETCH(qreal, contentY); + QFETCH(qreal, itemsOffsetAfterMove); + QFETCH(int, moveFrom); + QFETCH(int, moveTo); + QFETCH(int, moveCount); + QFETCH(ListRange, expectedDisplacedIndexes); + + // target and displaced items should pass through these points + QPointF targetItems_transitionVia(-50, 50); + QPointF displacedItems_transitionVia(100, 100); + + QaimModel model; + for (int i = 0; i < initialItemCount; i++) + model.addItem("Original item" + QString::number(i), ""); + QaimModel model_targetItems_transitionVia; + QaimModel model_displacedItems_transitionVia; + + QQuickView *canvas = createView(); + QDeclarativeContext *ctxt = canvas->rootContext(); + ctxt->setContextProperty("testModel", &model); + ctxt->setContextProperty("model_targetItems_transitionVia", &model_targetItems_transitionVia); + ctxt->setContextProperty("model_displacedItems_transitionVia", &model_displacedItems_transitionVia); + ctxt->setContextProperty("targetItems_transitionVia", targetItems_transitionVia); + ctxt->setContextProperty("displacedItems_transitionVia", displacedItems_transitionVia); + canvas->setSource(testFileUrl("moveTransitions.qml")); + canvas->show(); + + QQuickGridView *gridview = findItem(canvas->rootObject(), "grid"); + QTRY_VERIFY(gridview != 0); + QQuickItem *contentItem = gridview->contentItem(); + QVERIFY(contentItem != 0); + QQuickText *name; + + if (contentY != 0) { + gridview->setContentY(contentY); + QTRY_COMPARE(QQuickItemPrivate::get(gridview)->polishScheduled, false); + } + + QList > expectedDisplacedValues = expectedDisplacedIndexes.getModelDataValues(model); + + // Items moving to *or* from visible positions should be animated. + // Otherwise, they should not be animated. + QList > expectedTargetData; + QList targetIndexes; + for (int i=moveFrom; iheight()) / 60.0)*3) - 1; + if ((i >= firstVisibleIndex && i <= lastVisibleIndex) + || (toIndex >= firstVisibleIndex && toIndex <= lastVisibleIndex)) { + expectedTargetData << qMakePair(model.name(i), model.number(i)); + targetIndexes << i; + } + } + // ViewTransition.index provides the indices that items are moving to, not from + targetIndexes = adjustIndexesForMove(targetIndexes, moveFrom, moveTo, moveCount); + + // start animation + model.moveItems(moveFrom, moveTo, moveCount); + + QTRY_COMPARE(gridview->property("targetTransitionsDone").toInt(), expectedTargetData.count()); + QTRY_COMPARE(gridview->property("displaceTransitionsDone").toInt(), + expectedDisplacedIndexes.isValid() ? expectedDisplacedIndexes.count() : 0); + + QList targetItems = findItems(contentItem, "wrapper", targetIndexes); + + // check the target and displaced items were animated + model_targetItems_transitionVia.matchAgainst(expectedTargetData, "wasn't animated from target 'from' pos", "shouldn't have been animated from target 'from' pos"); + model_displacedItems_transitionVia.matchAgainst(expectedDisplacedValues, "wasn't animated with displaced anim", "shouldn't have been animated with displaced anim"); + + // check attached properties + matchItemsAndIndexes(gridview->property("targetTrans_items").toMap(), model, targetIndexes); + matchIndexLists(gridview->property("targetTrans_targetIndexes").toList(), targetIndexes); + matchItemLists(gridview->property("targetTrans_targetItems").toList(), targetItems); + if (expectedDisplacedIndexes.isValid()) { + // adjust expectedDisplacedIndexes to their final values after the move + QList displacedIndexes = adjustIndexesForMove(expectedDisplacedIndexes.indexes, moveFrom, moveTo, moveCount); + matchItemsAndIndexes(gridview->property("displacedTrans_items").toMap(), model, displacedIndexes); + matchIndexLists(gridview->property("displacedTrans_targetIndexes").toList(), targetIndexes); + matchItemLists(gridview->property("displacedTrans_targetItems").toList(), targetItems); + } + + QList items = findItems(contentItem, "wrapper"); + int firstVisibleIndex = -1; + for (int i=0; iy() >= contentY) { + QDeclarativeExpression e(qmlContext(items[i]), items[i], "index"); + firstVisibleIndex = e.evaluate().toInt(); + break; + } + } + QVERIFY2(firstVisibleIndex >= 0, QTest::toString(firstVisibleIndex)); + + // verify all items moved to the correct final positions + int itemCount = findItems(contentItem, "wrapper").count(); + for (int i=firstVisibleIndex; i < model.count() && i < itemCount; ++i) { + QQuickItem *item = findItem(contentItem, "wrapper", i); + QVERIFY2(item, QTest::toString(QString("Item %1 not found").arg(i))); + QCOMPARE(item->x(), (i%3)*80.0); + QCOMPARE(item->y(), (i/3)*60.0 + itemsOffsetAfterMove); + name = findItem(contentItem, "textName", i); + QVERIFY(name != 0); + QTRY_COMPARE(name->text(), model.name(i)); + } + + delete canvas; +} + +void tst_QQuickGridView::moveTransitions_data() +{ + QTest::addColumn("initialItemCount"); + QTest::addColumn("contentY"); + QTest::addColumn("itemsOffsetAfterMove"); + QTest::addColumn("moveFrom"); + QTest::addColumn("moveTo"); + QTest::addColumn("moveCount"); + QTest::addColumn("expectedDisplacedIndexes"); + + QTest::newRow("move from above view, outside visible items, move 1") << 30 << 120.0 << 0.0 + << 1 << 10 << 1 << ListRange(6, 10); + QTest::newRow("move from above view, outside visible items, move 1 (first item)") << 30 << 120.0 << 0.0 + << 0 << 10 << 1 << ListRange(6, 10); + QTest::newRow("move from above view, outside visible items, move multiple") << 30 << 120.0 << 60.0 + << 1 << 10 << 3 << ListRange(13, 23); + QTest::newRow("move from above view, mix of visible/non-visible") << 30 << 120.0 << 60.0 + << 1 << 10 << 6 << (ListRange(7, 15) + ListRange(16, 23)); + QTest::newRow("move from above view, mix of visible/non-visible (move first)") << 30 << 120.0 << 120.0 + << 0 << 10 << 6 << ListRange(16, 23); + + QTest::newRow("move within view, move 1 down") << 30 << 0.0 << 0.0 + << 1 << 10 << 1 << ListRange(2, 10); + QTest::newRow("move within view, move 1 down, move first item") << 30 << 0.0 << 0.0 + << 0 << 10 << 1 << ListRange(1, 10); + QTest::newRow("move within view, move 1 down, move first item, contentY not 0") << 30 << 120.0 << 0.0 + << 0+6 << 10+6 << 1 << ListRange(1+6, 10+6); + QTest::newRow("move within view, move 1 down, to last item") << 30 << 0.0 << 0.0 + << 10 << 17 << 1 << ListRange(11, 17); + QTest::newRow("move within view, move first->last") << 30 << 0.0 << 0.0 + << 0 << 17 << 1 << ListRange(1, 17); + + QTest::newRow("move within view, move multiple down") << 30 << 0.0 << 0.0 + << 1 << 10 << 3 << ListRange(4, 12); + QTest::newRow("move within view, move multiple down, move first item") << 30 << 0.0 << 0.0 + << 0 << 10 << 3 << ListRange(3, 12); + QTest::newRow("move within view, move multiple down, move first item, contentY not 0") << 30 << 60.0 << 0.0 + << 0+3 << 10+3 << 3 << ListRange(3+3, 12+3); + QTest::newRow("move within view, move multiple down, displace last item") << 30 << 0.0 << 0.0 + << 5 << 15 << 3 << ListRange(8, 17); + QTest::newRow("move within view, move multiple down, move first->last") << 30 << 0.0 << 0.0 + << 0 << 15 << 3 << ListRange(3, 17); + + QTest::newRow("move within view, move 1 up") << 30 << 0.0 << 0.0 + << 10 << 1 << 1 << ListRange(1, 9); + QTest::newRow("move within view, move 1 up, move to first index") << 30 << 0.0 << 0.0 + << 10 << 0 << 1 << ListRange(0, 9); + QTest::newRow("move within view, move 1 up, move to first index, contentY not 0") << 30 << 120.0 << 0.0 + << 10+6 << 0+6 << 1 << ListRange(0+6, 9+6); + QTest::newRow("move within view, move 1 up, move to first index, contentY not on item border") << 30 << 80.0 << 0.0 + << 10+3 << 0+3 << 1 << ListRange(0+3, 9+3); + QTest::newRow("move within view, move 1 up, move last item") << 30 << 0.0 << 0.0 + << 17 << 10 << 1 << ListRange(10, 16); + QTest::newRow("move within view, move 1 up, move last->first") << 30 << 0.0 << 0.0 + << 17 << 0 << 1 << ListRange(0, 16); + + QTest::newRow("move within view, move multiple up") << 30 << 0.0 << 0.0 + << 10 << 1 << 3 << ListRange(1, 9); + QTest::newRow("move within view, move multiple (> 1 row) up") << 30 << 0.0 << 0.0 + << 10 << 1 << 5 << ListRange(1, 9); + QTest::newRow("move within view, move multiple up, move to first index") << 30 << 0.0 << 0.0 + << 10 << 0 << 3 << ListRange(0, 9); + QTest::newRow("move within view, move multiple up, move to first index, contentY not 0") << 30 << 60.0 << 0.0 + << 10+3 << 0+3 << 3 << ListRange(0+3, 9+3); + QTest::newRow("move within view, move multiple up (> 1 row), move to first index, contentY not on border") << 30 << 80.0 << 0.0 + << 10+3 << 0+3 << 5 << ListRange(0+3, 9+3); + QTest::newRow("move within view, move multiple up, move last item") << 30 << 0.0 << 0.0 + << 15 << 5 << 3 << ListRange(5, 14); + QTest::newRow("move within view, move multiple up, move last->first") << 30 << 0.0 << 0.0 + << 15 << 0 << 3 << ListRange(0, 14); + + QTest::newRow("move from below view, move 1 up") << 30 << 0.0 << 0.0 + << 20 << 5 << 1 << ListRange(5, 17); + QTest::newRow("move from below view, move 1 up, move to top") << 30 << 0.0 << 0.0 + << 20 << 0 << 1 << ListRange(0, 17); + QTest::newRow("move from below view, move 1 up, move to top, contentY not 0") << 30 << 60.0 << 0.0 + << 25 << 3 << 1 << ListRange(0+3, 17+3); + QTest::newRow("move from below view, move multiple (> 1 row) up") << 30 << 0.0 << 0.0 + << 20 << 5 << 5 << ListRange(5, 17); + QTest::newRow("move from below view, move multiple up, move to top") << 30 << 0.0 << 0.0 + << 20 << 0 << 3 << ListRange(0, 17); + QTest::newRow("move from below view, move multiple up, move to top, contentY not 0") << 30 << 60.0 << 0.0 + << 25 << 3 << 3 << ListRange(0+3, 17+3); + + QTest::newRow("move from below view, move 1 up, move to bottom") << 30 << 0.0 << 0.0 + << 20 << 17 << 1 << ListRange(17, 17); + QTest::newRow("move from below view, move 1 up, move to bottom, contentY not 0") << 30 << 60.0 << 0.0 + << 25 << 17+3 << 1 << ListRange(17+3, 17+3); + QTest::newRow("move from below view, move multiple up, move to to bottom") << 30 << 0.0 << 0.0 + << 20 << 17 << 3 << ListRange(17, 17); + QTest::newRow("move from below view, move multiple up, move to bottom, contentY not 0") << 30 << 60.0 << 0.0 + << 25 << 17+3 << 3 << ListRange(17+3, 17+3); +} + +void tst_QQuickGridView::removeTransitions() +{ + QFETCH(int, initialItemCount); + QFETCH(bool, shouldAnimateTargets); + QFETCH(qreal, contentY); + QFETCH(int, removalIndex); + QFETCH(int, removalCount); + QFETCH(ListRange, expectedDisplacedIndexes); + + // added items should end here + QPointF targetItems_transitionTo(-50, -50); + + // displaced items should pass through this points + QPointF displacedItems_transitionVia(100, 100); + + QaimModel model; + for (int i = 0; i < initialItemCount; i++) + model.addItem("Original item" + QString::number(i), ""); + QaimModel model_targetItems_transitionTo; + QaimModel model_displacedItems_transitionVia; + + QQuickView *canvas = createView(); + QDeclarativeContext *ctxt = canvas->rootContext(); + ctxt->setContextProperty("testModel", &model); + ctxt->setContextProperty("model_targetItems_transitionTo", &model_targetItems_transitionTo); + ctxt->setContextProperty("model_displacedItems_transitionVia", &model_displacedItems_transitionVia); + ctxt->setContextProperty("targetItems_transitionTo", targetItems_transitionTo); + ctxt->setContextProperty("displacedItems_transitionVia", displacedItems_transitionVia); + canvas->setSource(testFileUrl("removeTransitions.qml")); + canvas->show(); + + QQuickGridView *gridview = findItem(canvas->rootObject(), "grid"); + QTRY_VERIFY(gridview != 0); + QQuickItem *contentItem = gridview->contentItem(); + QVERIFY(contentItem != 0); + QTRY_COMPARE(QQuickItemPrivate::get(gridview)->polishScheduled, false); + + if (contentY != 0) { + gridview->setContentY(contentY); + QTRY_COMPARE(QQuickItemPrivate::get(gridview)->polishScheduled, false); + } + + QList > expectedDisplacedValues = expectedDisplacedIndexes.getModelDataValues(model); + + // only target items that are visible should be animated + QList > expectedTargetData; + QList targetIndexes; + if (shouldAnimateTargets) { + for (int i=removalIndex; iheight()) / 60.0)*3) - 1; + if (i >= firstVisibleIndex && i <= lastVisibleIndex) { + expectedTargetData << qMakePair(model.name(i), model.number(i)); + targetIndexes << i; + } + } + QVERIFY(expectedTargetData.count() > 0); + } + + // calculate targetItems and expectedTargets before model changes + QList targetItems = findItems(contentItem, "wrapper", targetIndexes); + QVariantMap expectedTargets; + for (int i=0; icount()); + + if (shouldAnimateTargets || expectedDisplacedIndexes.isValid()) { + QTRY_COMPARE(gridview->property("targetTransitionsDone").toInt(), expectedTargetData.count()); + QTRY_COMPARE(gridview->property("displaceTransitionsDone").toInt(), + expectedDisplacedIndexes.isValid() ? expectedDisplacedIndexes.count() : 0); + + // check the target and displaced items were animated + model_targetItems_transitionTo.matchAgainst(expectedTargetData, "wasn't animated to target 'to' pos", "shouldn't have been animated to target 'to' pos"); + model_displacedItems_transitionVia.matchAgainst(expectedDisplacedValues, "wasn't animated with displaced anim", "shouldn't have been animated with displaced anim"); + + // check attached properties + QCOMPARE(gridview->property("targetTrans_items").toMap(), expectedTargets); + matchIndexLists(gridview->property("targetTrans_targetIndexes").toList(), targetIndexes); + matchItemLists(gridview->property("targetTrans_targetItems").toList(), targetItems); + if (expectedDisplacedIndexes.isValid()) { + // adjust expectedDisplacedIndexes to their final values after the move + QList displacedIndexes = adjustIndexesForRemoveDisplaced(expectedDisplacedIndexes.indexes, removalIndex, removalCount); + matchItemsAndIndexes(gridview->property("displacedTrans_items").toMap(), model, displacedIndexes); + matchIndexLists(gridview->property("displacedTrans_targetIndexes").toList(), targetIndexes); + matchItemLists(gridview->property("displacedTrans_targetItems").toList(), targetItems); + } + } else { + QTRY_COMPARE(model_targetItems_transitionTo.count(), 0); + QTRY_COMPARE(model_displacedItems_transitionVia.count(), 0); + } + + QList items = findItems(contentItem, "wrapper"); + int itemCount = items.count(); + int firstVisibleIndex = -1; + for (int i=0; iy() >= contentY) + firstVisibleIndex = index; + else if (index < 0) + itemCount--; // exclude deleted items + } + QVERIFY2(firstVisibleIndex >= 0, QTest::toString(firstVisibleIndex)); + + // verify all items moved to the correct final positions + for (int i=firstVisibleIndex; i < model.count() && i < itemCount; ++i) { + QQuickItem *item = findItem(contentItem, "wrapper", i); + QVERIFY2(item, QTest::toString(QString("Item %1 not found").arg(i))); + QCOMPARE(item->x(), (i%3)*80.0); + QCOMPARE(item->y(), contentY + ((i-firstVisibleIndex)/3) * 60.0); + QQuickText *name = findItem(contentItem, "textName", i); + QVERIFY(name != 0); + QTRY_COMPARE(name->text(), model.name(i)); + } + + delete canvas; +} + +void tst_QQuickGridView::removeTransitions_data() +{ + QTest::addColumn("initialItemCount"); + QTest::addColumn("contentY"); + QTest::addColumn("shouldAnimateTargets"); + QTest::addColumn("removalIndex"); + QTest::addColumn("removalCount"); + QTest::addColumn("expectedDisplacedIndexes"); + + // All items that are visible following the remove operation should be animated. + // Remove targets that are outside of the view should not be animated. + + // For a GridView, removing any number of items other than a full row before the start + // should displace all items in the view + QTest::newRow("remove 1 before start") + << 30 << 120.0 << false + << 2 << 1 << ListRange(6, 24); // 6-24 are displaced + QTest::newRow("remove 1 row, before start") + << 30 << 120.0 << false + << 3 << 3 << ListRange(); + QTest::newRow("remove between 1-2 rows, before start") + << 30 << 120.0 << false + << 0 << 5 << ListRange(6, 25); + QTest::newRow("remove 2 rows, before start") + << 30 << 120.0 << false + << 0 << 6 << ListRange(); + QTest::newRow("remove mix of before and after start") + << 30 << 60.0 << true + << 2 << 3 << ListRange(5, 23); // 5-23 are displaced into view + + + QTest::newRow("remove 1 from start") + << 30 << 0.0 << true + << 0 << 1 << ListRange(1, 18); // 1-18 are displaced into view + QTest::newRow("remove multiple from start") + << 30 << 0.0 << true + << 0 << 3 << ListRange(3, 20); // 3-18 are displaced into view + QTest::newRow("remove 1 from start, content y not 0") + << 30 << 60.0 << true + << 3 << 1 << ListRange(1 + 3, 18 + 3); + QTest::newRow("remove multiple from start, content y not 0") + << 30 << 60.0 << true + << 3 << 3 << ListRange(3 + 3, 20 + 3); + + + QTest::newRow("remove 1 from middle") + << 30 << 0.0 << true + << 5 << 1 << ListRange(6, 18); + QTest::newRow("remove multiple from middle") + << 30 << 0.0 << true + << 5 << 3 << ListRange(8, 20); + + + QTest::newRow("remove 1 from bottom") + << 30 << 0.0 << true + << 17 << 1 << ListRange(18, 18); + QTest::newRow("remove multiple (1 row) from bottom") + << 30 << 0.0 << true + << 15 << 3 << ListRange(18, 20); + QTest::newRow("remove multiple (> 1 row) from bottom") + << 30 << 0.0 << true + << 15 << 5 << ListRange(20, 22); + QTest::newRow("remove 1 from bottom, content y not 0") + << 30 << 60.0 << true + << 17 + 3 << 1 << ListRange(18 + 3, 18 + 3); + QTest::newRow("remove multiple (1 row) from bottom, content y not 0") + << 30 << 60.0 << true + << 15 + 3 << 3 << ListRange(18 + 3, 20 + 3); + + + QTest::newRow("remove 1 after end") + << 30 << 0.0 << false + << 18 << 1 << ListRange(); + QTest::newRow("remove multiple after end") + << 30 << 0.0 << false + << 18 << 3 << ListRange(); +} + +void tst_QQuickGridView::multipleTransitions() +{ + // Tests that if you interrupt a transition in progress with another action that + // cancels the previous transition, the resulting items are still placed correctly. + + QFETCH(int, initialCount); + QFETCH(qreal, contentY); + QFETCH(QList, changes); + + // add transitions on the left, moves on the right + QPointF addTargets_transitionFrom(-50, -50); + QPointF addDisplaced_transitionFrom(-50, 50); + QPointF moveTargets_transitionFrom(50, -50); + QPointF moveDisplaced_transitionFrom(50, 50); + + QmlListModel model; + for (int i = 0; i < initialCount; i++) + model.addItem("Original item" + QString::number(i), ""); + + QQuickView *canvas = createView(); + QDeclarativeContext *ctxt = canvas->rootContext(); + ctxt->setContextProperty("testModel", &model); + ctxt->setContextProperty("addTargets_transitionFrom", addTargets_transitionFrom); + ctxt->setContextProperty("addDisplaced_transitionFrom", addDisplaced_transitionFrom); + ctxt->setContextProperty("moveTargets_transitionFrom", moveTargets_transitionFrom); + ctxt->setContextProperty("moveDisplaced_transitionFrom", moveDisplaced_transitionFrom); + canvas->setSource(testFileUrl("multipleTransitions.qml")); + canvas->show(); + + QQuickGridView *gridview = findItem(canvas->rootObject(), "grid"); + QTRY_VERIFY(gridview != 0); + QQuickItem *contentItem = gridview->contentItem(); + QVERIFY(contentItem != 0); + QTRY_COMPARE(QQuickItemPrivate::get(gridview)->polishScheduled, false); + + int timeBetweenActions = canvas->rootObject()->property("timeBetweenActions").toInt(); + + QList > targetItems; + for (int i=0; icount()); + QTRY_VERIFY(gridview->property("runningAddTargets").toBool()); + QTRY_VERIFY(gridview->property("runningAddDisplaced").toBool()); + if (i == changes.count() - 1) { + QTRY_VERIFY(!gridview->property("runningAddTargets").toBool()); + QTRY_VERIFY(!gridview->property("runningAddDisplaced").toBool()); + } else { + QTest::qWait(timeBetweenActions); + } + break; + } + case ListChange::Removed: + for (int j=changes[i].index; jcount()); + QTRY_VERIFY(gridview->property("runningRemoveTargets").toBool()); + QTRY_VERIFY(gridview->property("runningRemoveDisplaced").toBool()); + if (i == changes.count() - 1) { + QTRY_VERIFY(!gridview->property("runningRemoveTargets").toBool()); + QTRY_VERIFY(!gridview->property("runningRemoveDisplaced").toBool()); + } else { + QTest::qWait(timeBetweenActions); + } + break; + case ListChange::Moved: + for (int j=changes[i].index; jproperty("runningMoveTargets").toBool()); + QTRY_VERIFY(gridview->property("runningMoveDisplaced").toBool()); + if (i == changes.count() - 1) { + QTRY_VERIFY(!gridview->property("runningMoveTargets").toBool()); + QTRY_VERIFY(!gridview->property("runningMoveDisplaced").toBool()); + } else { + QTest::qWait(timeBetweenActions); + } + break; + case ListChange::SetCurrent: + gridview->setCurrentIndex(changes[i].index); + QTRY_COMPARE(QQuickItemPrivate::get(gridview)->polishScheduled, false); + break; + case ListChange::SetContentY: + gridview->setContentY(changes[i].pos); + QTRY_COMPARE(QQuickItemPrivate::get(gridview)->polishScheduled, false); + break; + } + } + QCOMPARE(gridview->count(), model.count()); + + QList items = findItems(contentItem, "wrapper"); + int firstVisibleIndex = -1; + for (int i=0; iy() >= contentY) { + QDeclarativeExpression e(qmlContext(items[i]), items[i], "index"); + firstVisibleIndex = e.evaluate().toInt(); + break; + } + } + QVERIFY2(firstVisibleIndex >= 0, QTest::toString(firstVisibleIndex)); + + // verify all items moved to the correct final positions + int itemCount = findItems(contentItem, "wrapper").count(); + for (int i=firstVisibleIndex; i < model.count() && i < itemCount; ++i) { + QQuickItem *item = findItem(contentItem, "wrapper", i); + QVERIFY2(item, QTest::toString(QString("Item %1 not found").arg(i))); + QCOMPARE(item->x(), (i%3)*80.0); + QCOMPARE(item->y(), (i/3)*60.0); + QQuickText *name = findItem(contentItem, "textName", i); + QVERIFY(name != 0); + QTRY_COMPARE(name->text(), model.name(i)); + } + + delete canvas; +} + +void tst_QQuickGridView::multipleTransitions_data() +{ + QTest::addColumn("initialCount"); + QTest::addColumn("contentY"); + QTest::addColumn >("changes"); + + // the added item and displaced items should move to final dest correctly + QTest::newRow("add item, then move it immediately") << 10 << 0.0 << (QList() + << ListChange::insert(0, 1) + << ListChange::move(0, 3, 1) + ); + + // items affected by the add should change from move to add transition + QTest::newRow("move, then insert item before the moved item") << 20 << 0.0 << (QList() + << ListChange::move(1, 10, 3) + << ListChange::insert(0, 1) + ); + + // items should be placed correctly if you trigger a transition then refill for that index + QTest::newRow("add at 0, flick down, flick back to top and add at 0 again") << 20 << 0.0 << (QList() + << ListChange::insert(0, 1) + << ListChange::setContentY(160.0) + << ListChange::setContentY(0.0) + << ListChange::insert(0, 1) + ); +} + void tst_QQuickGridView::cacheBuffer() { QQuickView *canvas = createView(); @@ -4085,6 +5006,56 @@ void tst_QQuickGridView::unrequestedVisibility() delete canvas; } +QList tst_QQuickGridView::toIntList(const QVariantList &list) +{ + QList ret; + bool ok = true; + for (int i=0; i &expectedIndexes) +{ + for (int i=0; i current = indexLists[i].value >().toSet(); + if (current != expectedIndexes.toSet()) + qDebug() << "Cannot match actual targets" << current << "with expected" << expectedIndexes; + QCOMPARE(current, expectedIndexes.toSet()); + } +} + +void tst_QQuickGridView::matchItemsAndIndexes(const QVariantMap &items, const QaimModel &model, const QList &expectedIndexes) +{ + for (QVariantMap::const_iterator it = items.begin(); it != items.end(); ++it) { + QVERIFY(it.value().type() == QVariant::Int); + QString name = it.key(); + int itemIndex = it.value().toInt(); + QVERIFY2(expectedIndexes.contains(itemIndex), QTest::toString(QString("Index %1 not found in expectedIndexes").arg(itemIndex))); + if (model.name(itemIndex) != name) + qDebug() << itemIndex; + QCOMPARE(model.name(itemIndex), name); + } + QCOMPARE(items.count(), expectedIndexes.count()); +} + +void tst_QQuickGridView::matchItemLists(const QVariantList &itemLists, const QList &expectedItems) +{ + for (int i=0; i(current[j].value()); + QVERIFY2(o, QTest::toString(QString("Invalid actual item at %1").arg(j))); + QVERIFY2(expectedItems.contains(o), QTest::toString(QString("Cannot match item %1").arg(j))); + } + QCOMPARE(current.count(), expectedItems.count()); + } +} + QTEST_MAIN(tst_QQuickGridView) #include "tst_qquickgridview.moc" diff --git a/tests/auto/qtquick2/qquicklistview/data/addTransitions.qml b/tests/auto/qtquick2/qquicklistview/data/addTransitions.qml new file mode 100644 index 0000000000..ff90ead8a6 --- /dev/null +++ b/tests/auto/qtquick2/qquicklistview/data/addTransitions.qml @@ -0,0 +1,134 @@ +import QtQuick 2.0 + +Rectangle { + id: root + width: 500 + height: 600 + + property int duration: 10 + property int count: list.count + + Component { + id: myDelegate + Rectangle { + id: wrapper + + property string nameData: name + + objectName: "wrapper" + height: 20 + width: 240 + Text { text: index } + Text { + x: 30 + id: textName + objectName: "textName" + text: name + } + Text { + x: 200 + text: wrapper.y + } + color: ListView.isCurrentItem ? "lightsteelblue" : "white" + + onXChanged: checkPos() + onYChanged: checkPos() + + function checkPos() { + if (Qt.point(x, y) == targetItems_transitionFrom) + model_targetItems_transitionFrom.addItem(name, "") + if (Qt.point(x, y) == displacedItems_transitionVia) + model_displacedItems_transitionVia.addItem(name, "") + } + } + } + + ListView { + id: list + + property int targetTransitionsDone + property int displaceTransitionsDone + + property var targetTrans_items: new Object() + property var targetTrans_targetIndexes: new Array() + property var targetTrans_targetItems: new Array() + + property var displacedTrans_items: new Object() + property var displacedTrans_targetIndexes: new Array() + property var displacedTrans_targetItems: new Array() + + objectName: "list" + focus: true + anchors.centerIn: parent + width: 240 + height: 320 + model: testModel + delegate: myDelegate + + // for QDeclarativeListProperty types + function copyList(propList) { + var temp = new Array() + for (var i=0; i #include #include -#include #include "../../shared/util.h" #include "../shared/viewtestutil.h" #include "../shared/visualtestutil.h" @@ -171,6 +170,17 @@ private slots: void asynchronous(); void unrequestedVisibility(); + void populateTransitions(); + void populateTransitions_data(); + void addTransitions(); + void addTransitions_data(); + void moveTransitions(); + void moveTransitions_data(); + void removeTransitions(); + void removeTransitions_data(); + void multipleTransitions(); + void multipleTransitions_data(); + private: template void items(const QUrl &source, bool forceLayout); template void changed(const QUrl &source, bool forceLayout); @@ -182,6 +192,11 @@ private: template void clear(const QUrl &source); template void sections(const QUrl &source); + QList toIntList(const QVariantList &list); + void matchIndexLists(const QVariantList &indexLists, const QList &expectedIndexes); + void matchItemsAndIndexes(const QVariantMap &items, const QaimModel &model, const QList &expectedIndexes); + void matchItemLists(const QVariantList &itemLists, const QList &expectedItems); + void inserted_more_data(); void removed_more_data(); void moved_data(); @@ -1356,18 +1371,26 @@ void tst_QQuickListView::multipleChanges() { QList > items; for (int j=changes[i].index; jpolishScheduled, false); break; } case ListChange::Removed: model.removeItems(changes[i].index, changes[i].count); + QTRY_COMPARE(QQuickItemPrivate::get(listview)->polishScheduled, false); break; case ListChange::Moved: model.moveItems(changes[i].index, changes[i].to, changes[i].count); + QTRY_COMPARE(QQuickItemPrivate::get(listview)->polishScheduled, false); break; case ListChange::SetCurrent: listview->setCurrentIndex(changes[i].index); + QTRY_COMPARE(QQuickItemPrivate::get(listview)->polishScheduled, false); + break; + case ListChange::SetContentY: + listview->setContentY(changes[i].pos); + QTRY_COMPARE(QQuickItemPrivate::get(listview)->polishScheduled, false); break; } } @@ -4729,6 +4752,929 @@ void tst_QQuickListView::unrequestedVisibility() delete canvas; } +void tst_QQuickListView::populateTransitions() +{ + QFETCH(bool, staticallyPopulate); + QFETCH(bool, dynamicallyPopulate); + QFETCH(bool, usePopulateTransition); + + QPointF transitionFrom(-50, -50); + QPointF transitionVia(100, 100); + QaimModel model_transitionFrom; + QaimModel model_transitionVia; + + QaimModel model; + if (staticallyPopulate) { + for (int i = 0; i < 30; i++) + model.addItem("item" + QString::number(i), ""); + } + + QQuickView *canvas = createView(); + canvas->rootContext()->setContextProperty("testModel", &model); + canvas->rootContext()->setContextProperty("testObject", new TestObject(canvas->rootContext())); + canvas->rootContext()->setContextProperty("usePopulateTransition", usePopulateTransition); + canvas->rootContext()->setContextProperty("dynamicallyPopulate", dynamicallyPopulate); + canvas->rootContext()->setContextProperty("transitionFrom", transitionFrom); + canvas->rootContext()->setContextProperty("transitionVia", transitionVia); + canvas->rootContext()->setContextProperty("model_transitionFrom", &model_transitionFrom); + canvas->rootContext()->setContextProperty("model_transitionVia", &model_transitionVia); + canvas->setSource(testFileUrl("populateTransitions.qml")); + canvas->show(); + + QQuickListView *listview = findItem(canvas->rootObject(), "list"); + QVERIFY(listview); + QQuickItem *contentItem = listview->contentItem(); + QVERIFY(contentItem); + + if (staticallyPopulate || dynamicallyPopulate) { + // check the populate transition is run + if (usePopulateTransition) { + QTRY_COMPARE(listview->property("countPopulateTransitions").toInt(), 17); + } else { + QTRY_COMPARE(QQuickItemPrivate::get(listview)->polishScheduled, false); + QTRY_COMPARE(listview->property("countPopulateTransitions").toInt(), 0); + } + QTRY_COMPARE(listview->property("countAddTransitions").toInt(), 0); + } else { + QTRY_COMPARE(QQuickItemPrivate::get(listview)->polishScheduled, false); + } + + int itemCount = findItems(contentItem, "wrapper").count(); + if (usePopulateTransition) + QCOMPARE(itemCount, listview->property("countPopulateTransitions").toInt()); + for (int i=0; i < model.count() && i < itemCount; ++i) { + QQuickItem *item = findItem(contentItem, "wrapper", i); + QVERIFY2(item, QTest::toString(QString("Item %1 not found").arg(i))); + QTRY_COMPARE(item->x(), 0.0); + QTRY_COMPARE(item->y(), i*20.0); + QQuickText *name = findItem(contentItem, "textName", i); + QVERIFY(name != 0); + QTRY_COMPARE(name->text(), model.name(i)); + } + + // add an item and check this is done with add trantion, not populate + model.insertItem(0, "another item", ""); + QTRY_COMPARE(listview->property("countAddTransitions").toInt(), 1); + QTRY_COMPARE(listview->property("countPopulateTransitions").toInt(), + (usePopulateTransition && (staticallyPopulate || dynamicallyPopulate)) ? 17 : 0); + + // clear the model + canvas->rootContext()->setContextProperty("testModel", QVariant()); + QTRY_COMPARE(listview->count(), 0); + QTRY_COMPARE(findItems(contentItem, "wrapper").count(), 0); + listview->setProperty("countPopulateTransitions", 0); + listview->setProperty("countAddTransitions", 0); + + // set to a valid model and check populate transition is run a second time + model.clear(); + for (int i = 0; i < 30; i++) + model.addItem("item" + QString::number(i), ""); + canvas->rootContext()->setContextProperty("testModel", &model); + QTRY_COMPARE(listview->property("countPopulateTransitions").toInt(), usePopulateTransition ? 17 : 0); + QTRY_COMPARE(listview->property("countAddTransitions").toInt(), 0); + + itemCount = findItems(contentItem, "wrapper").count(); + if (usePopulateTransition) + QCOMPARE(itemCount, listview->property("countPopulateTransitions").toInt()); + for (int i=0; i < model.count() && i < itemCount; ++i) { + QQuickItem *item = findItem(contentItem, "wrapper", i); + QVERIFY2(item, QTest::toString(QString("Item %1 not found").arg(i))); + QTRY_COMPARE(item->x(), 0.0); + QTRY_COMPARE(item->y(), i*20.0); + QQuickText *name = findItem(contentItem, "textName", i); + QVERIFY(name != 0); + QTRY_COMPARE(name->text(), model.name(i)); + } + + // reset model and check populate transition is run again + listview->setProperty("countPopulateTransitions", 0); + listview->setProperty("countAddTransitions", 0); + model.reset(); + QTRY_COMPARE(listview->property("countPopulateTransitions").toInt(), usePopulateTransition ? 17 : 0); + QTRY_COMPARE(listview->property("countAddTransitions").toInt(), 0); + + itemCount = findItems(contentItem, "wrapper").count(); + if (usePopulateTransition) + QCOMPARE(itemCount, listview->property("countPopulateTransitions").toInt()); + for (int i=0; i < model.count() && i < itemCount; ++i) { + QQuickItem *item = findItem(contentItem, "wrapper", i); + QVERIFY2(item, QTest::toString(QString("Item %1 not found").arg(i))); + QTRY_COMPARE(item->x(), 0.0); + QTRY_COMPARE(item->y(), i*20.0); + QQuickText *name = findItem(contentItem, "textName", i); + QVERIFY(name != 0); + QTRY_COMPARE(name->text(), model.name(i)); + } + + delete canvas; +} + +void tst_QQuickListView::populateTransitions_data() +{ + QTest::addColumn("staticallyPopulate"); + QTest::addColumn("dynamicallyPopulate"); + QTest::addColumn("usePopulateTransition"); + + QTest::newRow("static") << true << false << true; + QTest::newRow("static, no populate") << true << false << false; + + QTest::newRow("dynamic") << false << true << true; + QTest::newRow("dynamic, no populate") << false << true << false; + + QTest::newRow("empty to start with") << false << false << true; + QTest::newRow("empty to start with, no populate") << false << false << false; +} + +void tst_QQuickListView::addTransitions() +{ + QFETCH(int, initialItemCount); + QFETCH(bool, shouldAnimateTargets); + QFETCH(qreal, contentY); + QFETCH(int, insertionIndex); + QFETCH(int, insertionCount); + QFETCH(ListRange, expectedDisplacedIndexes); + + // added items should start here + QPointF targetItems_transitionFrom(-50, -50); + + // displaced items should pass through this point + QPointF displacedItems_transitionVia(100, 100); + + QaimModel model; + for (int i = 0; i < initialItemCount; i++) + model.addItem("Original item" + QString::number(i), ""); + QaimModel model_targetItems_transitionFrom; + QaimModel model_displacedItems_transitionVia; + + QQuickView *canvas = createView(); + QDeclarativeContext *ctxt = canvas->rootContext(); + TestObject *testObject = new TestObject; + ctxt->setContextProperty("testModel", &model); + ctxt->setContextProperty("model_targetItems_transitionFrom", &model_targetItems_transitionFrom); + ctxt->setContextProperty("model_displacedItems_transitionVia", &model_displacedItems_transitionVia); + ctxt->setContextProperty("targetItems_transitionFrom", targetItems_transitionFrom); + ctxt->setContextProperty("displacedItems_transitionVia", displacedItems_transitionVia); + ctxt->setContextProperty("testObject", testObject); + canvas->setSource(testFileUrl("addTransitions.qml")); + canvas->show(); + + QQuickListView *listview = findItem(canvas->rootObject(), "list"); + QTRY_VERIFY(listview != 0); + QQuickItem *contentItem = listview->contentItem(); + QVERIFY(contentItem != 0); + QTRY_COMPARE(QQuickItemPrivate::get(listview)->polishScheduled, false); + + if (contentY != 0) { + listview->setContentY(contentY); + QTRY_COMPARE(QQuickItemPrivate::get(listview)->polishScheduled, false); + } + + QList > expectedDisplacedValues = expectedDisplacedIndexes.getModelDataValues(model); + + // only target items that will become visible should be animated + QList > newData; + QList > expectedTargetData; + QList targetIndexes; + if (shouldAnimateTargets) { + for (int i=insertionIndex; i= contentY / 20 && i < (contentY + listview->height()) / 20) { // only grab visible items + expectedTargetData << newData.last(); + targetIndexes << i; + } + } + QVERIFY(expectedTargetData.count() > 0); + } + + // start animation + if (!newData.isEmpty()) { + model.insertItems(insertionIndex, newData); + QTRY_COMPARE(model.count(), listview->count()); + } + + QList targetItems = findItems(contentItem, "wrapper", targetIndexes); + + if (shouldAnimateTargets) { + QTRY_COMPARE(listview->property("targetTransitionsDone").toInt(), expectedTargetData.count()); + QTRY_COMPARE(listview->property("displaceTransitionsDone").toInt(), + expectedDisplacedIndexes.isValid() ? expectedDisplacedIndexes.count() : 0); + + // check the target and displaced items were animated + model_targetItems_transitionFrom.matchAgainst(expectedTargetData, "wasn't animated from target 'from' pos", "shouldn't have been animated from target 'from' pos"); + model_displacedItems_transitionVia.matchAgainst(expectedDisplacedValues, "wasn't animated with displaced anim", "shouldn't have been animated with displaced anim"); + + // check attached properties + matchItemsAndIndexes(listview->property("targetTrans_items").toMap(), model, targetIndexes); + matchIndexLists(listview->property("targetTrans_targetIndexes").toList(), targetIndexes); + matchItemLists(listview->property("targetTrans_targetItems").toList(), targetItems); + if (expectedDisplacedIndexes.isValid()) { + // adjust expectedDisplacedIndexes to their final values after the move + QList displacedIndexes = adjustIndexesForAddDisplaced(expectedDisplacedIndexes.indexes, insertionIndex, insertionCount); + matchItemsAndIndexes(listview->property("displacedTrans_items").toMap(), model, displacedIndexes); + matchIndexLists(listview->property("displacedTrans_targetIndexes").toList(), targetIndexes); + matchItemLists(listview->property("displacedTrans_targetItems").toList(), targetItems); + } + + } else { + QTRY_COMPARE(model_targetItems_transitionFrom.count(), 0); + QTRY_COMPARE(model_displacedItems_transitionVia.count(), 0); + } + + QList items = findItems(contentItem, "wrapper"); + int firstVisibleIndex = -1; + int itemCount = items.count(); + for (int i=0; iy() >= contentY) { + QDeclarativeExpression e(qmlContext(items[i]), items[i], "index"); + firstVisibleIndex = e.evaluate().toInt(); + break; + } + } + QVERIFY2(firstVisibleIndex >= 0, QTest::toString(firstVisibleIndex)); + + // verify all items moved to the correct final positions + for (int i=firstVisibleIndex; i < model.count() && i < itemCount; ++i) { + QQuickItem *item = findItem(contentItem, "wrapper", i); + QVERIFY2(item, QTest::toString(QString("Item %1 not found").arg(i))); + QTRY_COMPARE(item->y(), i*20.0); + QQuickText *name = findItem(contentItem, "textName", i); + QVERIFY(name != 0); + QTRY_COMPARE(name->text(), model.name(i)); + } + + delete canvas; + delete testObject; +} + +void tst_QQuickListView::addTransitions_data() +{ + QTest::addColumn("initialItemCount"); + QTest::addColumn("contentY"); + QTest::addColumn("shouldAnimateTargets"); + QTest::addColumn("insertionIndex"); + QTest::addColumn("insertionCount"); + QTest::addColumn("expectedDisplacedIndexes"); + + // if inserting before visible index, items should not appear or animate in, even if there are > 1 new items + QTest::newRow("insert 1, just before start") + << 30 << 20.0 << false + << 0 << 1 << ListRange(); + QTest::newRow("insert 1, way before start") + << 30 << 20.0 << false + << 0 << 1 << ListRange(); + QTest::newRow("insert multiple, just before start") + << 30 << 100.0 << false + << 0 << 3 << ListRange(); + QTest::newRow("insert multiple, way before start") + << 30 << 100.0 << false + << 0 << 3 << ListRange(); + + QTest::newRow("insert 1 at start") + << 30 << 0.0 << true + << 0 << 1 << ListRange(0, 15); + QTest::newRow("insert multiple at start") + << 30 << 0.0 << true + << 0 << 3 << ListRange(0, 15); + QTest::newRow("insert 1 at start, content y not 0") + << 30 << 40.0 << true // first visible is index 2, so translate the displaced indexes by 2 + << 2 << 1 << ListRange(0 + 2, 15 + 2); + QTest::newRow("insert multiple at start, content y not 0") + << 30 << 40.0 << true // first visible is index 2 + << 2 << 3 << ListRange(0 + 2, 15 + 2); + + QTest::newRow("insert 1 at start, to empty list") + << 0 << 0.0 << true + << 0 << 1 << ListRange(); + QTest::newRow("insert multiple at start, to empty list") + << 0 << 0.0 << true + << 0 << 3 << ListRange(); + + QTest::newRow("insert 1 at middle") + << 30 << 0.0 << true + << 5 << 1 << ListRange(5, 15); + QTest::newRow("insert multiple at middle") + << 30 << 0.0 << true + << 5 << 3 << ListRange(5, 15); + + QTest::newRow("insert 1 at bottom") + << 30 << 0.0 << true + << 15 << 1 << ListRange(15, 15); + QTest::newRow("insert multiple at bottom") + << 30 << 0.0 << true + << 15 << 3 << ListRange(15, 15); + QTest::newRow("insert 1 at bottom, content y not 0") + << 30 << 20.0 * 3 << true + << 15 + 3 << 1 << ListRange(15 + 3, 15 + 3); + QTest::newRow("insert multiple at bottom, content y not 0") + << 30 << 20.0 * 3 << true + << 15 + 3 << 3 << ListRange(15 + 3, 15 + 3); + + // items added after the last visible will not be animated in, since they + // do not appear in the final view + QTest::newRow("insert 1 after end") + << 30 << 0.0 << false + << 17 << 1 << ListRange(); + QTest::newRow("insert multiple after end") + << 30 << 0.0 << false + << 17 << 3 << ListRange(); +} + +void tst_QQuickListView::moveTransitions() +{ + QFETCH(int, initialItemCount); + QFETCH(qreal, contentY); + QFETCH(qreal, itemsOffsetAfterMove); + QFETCH(int, moveFrom); + QFETCH(int, moveTo); + QFETCH(int, moveCount); + QFETCH(ListRange, expectedDisplacedIndexes); + + // target and displaced items should pass through these points + QPointF targetItems_transitionVia(-50, 50); + QPointF displacedItems_transitionVia(100, 100); + + QaimModel model; + for (int i = 0; i < initialItemCount; i++) + model.addItem("Original item" + QString::number(i), ""); + QaimModel model_targetItems_transitionVia; + QaimModel model_displacedItems_transitionVia; + + QQuickView *canvas = createView(); + QDeclarativeContext *ctxt = canvas->rootContext(); + TestObject *testObject = new TestObject; + ctxt->setContextProperty("testModel", &model); + ctxt->setContextProperty("model_targetItems_transitionVia", &model_targetItems_transitionVia); + ctxt->setContextProperty("model_displacedItems_transitionVia", &model_displacedItems_transitionVia); + ctxt->setContextProperty("targetItems_transitionVia", targetItems_transitionVia); + ctxt->setContextProperty("displacedItems_transitionVia", displacedItems_transitionVia); + ctxt->setContextProperty("testObject", testObject); + canvas->setSource(testFileUrl("moveTransitions.qml")); + canvas->show(); + + QQuickListView *listview = findItem(canvas->rootObject(), "list"); + QTRY_VERIFY(listview != 0); + QQuickItem *contentItem = listview->contentItem(); + QVERIFY(contentItem != 0); + QQuickText *name; + + if (contentY != 0) { + listview->setContentY(contentY); + QTRY_COMPARE(QQuickItemPrivate::get(listview)->polishScheduled, false); + } + + QList > expectedDisplacedValues = expectedDisplacedIndexes.getModelDataValues(model); + + // Items moving to *or* from visible positions should be animated. + // Otherwise, they should not be animated. + QList > expectedTargetData; + QList targetIndexes; + for (int i=moveFrom; iheight()) / 20 + || toIndex < (contentY + listview->height()) / 20) { + expectedTargetData << qMakePair(model.name(i), model.number(i)); + targetIndexes << i; + } + } + // ViewTransition.index provides the indices that items are moving to, not from + targetIndexes = adjustIndexesForMove(targetIndexes, moveFrom, moveTo, moveCount); + + // start animation + model.moveItems(moveFrom, moveTo, moveCount); + + QTRY_COMPARE(listview->property("targetTransitionsDone").toInt(), expectedTargetData.count()); + QTRY_COMPARE(listview->property("displaceTransitionsDone").toInt(), + expectedDisplacedIndexes.isValid() ? expectedDisplacedIndexes.count() : 0); + + QList targetItems = findItems(contentItem, "wrapper", targetIndexes); + + // check the target and displaced items were animated + model_targetItems_transitionVia.matchAgainst(expectedTargetData, "wasn't animated from target 'from' pos", "shouldn't have been animated from target 'from' pos"); + model_displacedItems_transitionVia.matchAgainst(expectedDisplacedValues, "wasn't animated with displaced anim", "shouldn't have been animated with displaced anim"); + + // check attached properties + matchItemsAndIndexes(listview->property("targetTrans_items").toMap(), model, targetIndexes); + matchIndexLists(listview->property("targetTrans_targetIndexes").toList(), targetIndexes); + matchItemLists(listview->property("targetTrans_targetItems").toList(), targetItems); + if (expectedDisplacedIndexes.isValid()) { + // adjust expectedDisplacedIndexes to their final values after the move + QList displacedIndexes = adjustIndexesForMove(expectedDisplacedIndexes.indexes, moveFrom, moveTo, moveCount); + matchItemsAndIndexes(listview->property("displacedTrans_items").toMap(), model, displacedIndexes); + matchIndexLists(listview->property("displacedTrans_targetIndexes").toList(), targetIndexes); + matchItemLists(listview->property("displacedTrans_targetItems").toList(), targetItems); + } + + QList items = findItems(contentItem, "wrapper"); + int firstVisibleIndex = -1; + for (int i=0; iy() >= contentY) { + QDeclarativeExpression e(qmlContext(items[i]), items[i], "index"); + firstVisibleIndex = e.evaluate().toInt(); + break; + } + } + QVERIFY2(firstVisibleIndex >= 0, QTest::toString(firstVisibleIndex)); + + // verify all items moved to the correct final positions + int itemCount = findItems(contentItem, "wrapper").count(); + for (int i=firstVisibleIndex; i < model.count() && i < itemCount; ++i) { + QQuickItem *item = findItem(contentItem, "wrapper", i); + QVERIFY2(item, QTest::toString(QString("Item %1 not found").arg(i))); + QTRY_COMPARE(item->y(), i*20.0 + itemsOffsetAfterMove); + name = findItem(contentItem, "textName", i); + QVERIFY(name != 0); + QTRY_COMPARE(name->text(), model.name(i)); + } + + delete canvas; + delete testObject; +} + +void tst_QQuickListView::moveTransitions_data() +{ + QTest::addColumn("initialItemCount"); + QTest::addColumn("contentY"); + QTest::addColumn("itemsOffsetAfterMove"); + QTest::addColumn("moveFrom"); + QTest::addColumn("moveTo"); + QTest::addColumn("moveCount"); + QTest::addColumn("expectedDisplacedIndexes"); + + // when removing from above the visible, all items shift down depending on how many + // items have been removed from above the visible + QTest::newRow("move from above view, outside visible items, move 1") << 30 << 4*20.0 << 20.0 + << 1 << 10 << 1 << ListRange(11, 15+4); + QTest::newRow("move from above view, outside visible items, move 1 (first item)") << 30 << 4*20.0 << 20.0 + << 0 << 10 << 1 << ListRange(11, 15+4); + QTest::newRow("move from above view, outside visible items, move multiple") << 30 << 4*20.0 << 2*20.0 + << 1 << 10 << 2 << ListRange(12, 15+4); + QTest::newRow("move from above view, outside visible items, move multiple (first item)") << 30 << 4*20.0 << 3*20.0 + << 0 << 10 << 3 << ListRange(13, 15+4); + QTest::newRow("move from above view, mix of visible/non-visible") << 30 << 4*20.0 << 3*20.0 + << 1 << 10 << 5 << ListRange(6, 14) + ListRange(15, 15+4); + QTest::newRow("move from above view, mix of visible/non-visible (move first)") << 30 << 4*20.0 << 4*20.0 + << 0 << 10 << 5 << ListRange(5, 14) + ListRange(15, 15+4); + + QTest::newRow("move within view, move 1 down") << 30 << 0.0 << 0.0 + << 1 << 10 << 1 << ListRange(2, 10); + QTest::newRow("move within view, move 1 down, move first item") << 30 << 0.0 << 0.0 + << 0 << 10 << 1 << ListRange(1, 10); + QTest::newRow("move within view, move 1 down, move first item, contentY not 0") << 30 << 4*20.0 << 0.0 + << 0+4 << 10+4 << 1 << ListRange(1+4, 10+4); + QTest::newRow("move within view, move 1 down, to last item") << 30 << 0.0 << 0.0 + << 10 << 15 << 1 << ListRange(11, 15); + QTest::newRow("move within view, move first->last") << 30 << 0.0 << 0.0 + << 0 << 15 << 1 << ListRange(1, 15); + + QTest::newRow("move within view, move multiple down") << 30 << 0.0 << 0.0 + << 1 << 10 << 3 << ListRange(4, 12); + QTest::newRow("move within view, move multiple down, move first item") << 30 << 0.0 << 0.0 + << 0 << 10 << 3 << ListRange(3, 12); + QTest::newRow("move within view, move multiple down, move first item, contentY not 0") << 30 << 4*20.0 << 0.0 + << 0+4 << 10+4 << 3 << ListRange(3+4, 12+4); + QTest::newRow("move within view, move multiple down, displace last item") << 30 << 0.0 << 0.0 + << 5 << 13 << 3 << ListRange(8, 15); + QTest::newRow("move within view, move multiple down, move first->last") << 30 << 0.0 << 0.0 + << 0 << 13 << 3 << ListRange(3, 15); + + QTest::newRow("move within view, move 1 up") << 30 << 0.0 << 0.0 + << 10 << 1 << 1 << ListRange(1, 9); + QTest::newRow("move within view, move 1 up, move to first index") << 30 << 0.0 << 0.0 + << 10 << 0 << 1 << ListRange(0, 9); + QTest::newRow("move within view, move 1 up, move to first index, contentY not 0") << 30 << 4*20.0 << 0.0 + << 10+4 << 0+4 << 1 << ListRange(0+4, 9+4); + QTest::newRow("move within view, move 1 up, move to first index, contentY not on item border") << 30 << 4*20.0 - 10 << 0.0 + << 10+4 << 0+4 << 1 << ListRange(0+4, 9+4); + QTest::newRow("move within view, move 1 up, move last item") << 30 << 0.0 << 0.0 + << 15 << 10 << 1 << ListRange(10, 14); + QTest::newRow("move within view, move 1 up, move last->first") << 30 << 0.0 << 0.0 + << 15 << 0 << 1 << ListRange(0, 14); + + QTest::newRow("move within view, move multiple up") << 30 << 0.0 << 0.0 + << 10 << 1 << 3 << ListRange(1, 9); + QTest::newRow("move within view, move multiple up, move to first index") << 30 << 0.0 << 0.0 + << 10 << 0 << 3 << ListRange(0, 9); + QTest::newRow("move within view, move multiple up, move to first index, contentY not 0") << 30 << 4*20.0 << 0.0 + << 10+4 << 0+4 << 3 << ListRange(0+4, 9+4); + QTest::newRow("move within view, move multiple up, move last item") << 30 << 0.0 << 0.0 + << 13 << 5 << 3 << ListRange(5, 12); + QTest::newRow("move within view, move multiple up, move last->first") << 30 << 0.0 << 0.0 + << 13 << 0 << 3 << ListRange(0, 12); + + QTest::newRow("move from below view, move 1 up, move to top") << 30 << 0.0 << 0.0 + << 20 << 0 << 1 << ListRange(0, 15); + QTest::newRow("move from below view, move 1 up, move to top, contentY not 0") << 30 << 4*20.0 << 0.0 + << 25 << 4 << 1 << ListRange(0+4, 15+4); + QTest::newRow("move from below view, move multiple up, move to top") << 30 << 0.0 << 0.0 + << 20 << 0 << 3 << ListRange(0, 15); + QTest::newRow("move from below view, move multiple up, move to top, contentY not 0") << 30 << 4*20.0 << 0.0 + << 25 << 4 << 3 << ListRange(0+4, 15+4); + + QTest::newRow("move from below view, move 1 up, move to bottom") << 30 << 0.0 << 0.0 + << 20 << 15 << 1 << ListRange(15, 15); + QTest::newRow("move from below view, move 1 up, move to bottom, contentY not 0") << 30 << 4*20.0 << 0.0 + << 25 << 15+4 << 1 << ListRange(15+4, 15+4); + QTest::newRow("move from below view, move multiple up, move to to bottom") << 30 << 0.0 << 0.0 + << 20 << 15 << 3 << ListRange(15, 15); + QTest::newRow("move from below view, move multiple up, move to bottom, contentY not 0") << 30 << 4*20.0 << 0.0 + << 25 << 15+4 << 3 << ListRange(15+4, 15+4); +} + +void tst_QQuickListView::removeTransitions() +{ + QFETCH(int, initialItemCount); + QFETCH(bool, shouldAnimateTargets); + QFETCH(qreal, contentY); + QFETCH(int, removalIndex); + QFETCH(int, removalCount); + QFETCH(ListRange, expectedDisplacedIndexes); + + // added items should end here + QPointF targetItems_transitionTo(-50, -50); + + // displaced items should pass through this points + QPointF displacedItems_transitionVia(100, 100); + + QaimModel model; + for (int i = 0; i < initialItemCount; i++) + model.addItem("Original item" + QString::number(i), ""); + QaimModel model_targetItems_transitionTo; + QaimModel model_displacedItems_transitionVia; + + QQuickView *canvas = createView(); + QDeclarativeContext *ctxt = canvas->rootContext(); + TestObject *testObject = new TestObject; + ctxt->setContextProperty("testModel", &model); + ctxt->setContextProperty("model_targetItems_transitionTo", &model_targetItems_transitionTo); + ctxt->setContextProperty("model_displacedItems_transitionVia", &model_displacedItems_transitionVia); + ctxt->setContextProperty("targetItems_transitionTo", targetItems_transitionTo); + ctxt->setContextProperty("displacedItems_transitionVia", displacedItems_transitionVia); + ctxt->setContextProperty("testObject", testObject); + canvas->setSource(testFileUrl("removeTransitions.qml")); + canvas->show(); + + QQuickListView *listview = findItem(canvas->rootObject(), "list"); + QTRY_VERIFY(listview != 0); + QQuickItem *contentItem = listview->contentItem(); + QVERIFY(contentItem != 0); + QTRY_COMPARE(QQuickItemPrivate::get(listview)->polishScheduled, false); + + if (contentY != 0) { + listview->setContentY(contentY); + QTRY_COMPARE(QQuickItemPrivate::get(listview)->polishScheduled, false); + } + + QList > expectedDisplacedValues = expectedDisplacedIndexes.getModelDataValues(model); + + // only target items that are visible should be animated + QList > expectedTargetData; + QList targetIndexes; + if (shouldAnimateTargets) { + for (int i=removalIndex; i= contentY / 20 && i < (contentY + listview->height()) / 20) { + expectedTargetData << qMakePair(model.name(i), model.number(i)); + targetIndexes << i; + } + } + QVERIFY(expectedTargetData.count() > 0); + } + + // calculate targetItems and expectedTargets before model changes + QList targetItems = findItems(contentItem, "wrapper", targetIndexes); + QVariantMap expectedTargets; + for (int i=0; icount()); + + if (shouldAnimateTargets) { + QTRY_COMPARE(listview->property("targetTransitionsDone").toInt(), expectedTargetData.count()); + QTRY_COMPARE(listview->property("displaceTransitionsDone").toInt(), + expectedDisplacedIndexes.isValid() ? expectedDisplacedIndexes.count() : 0); + + // check the target and displaced items were animated + model_targetItems_transitionTo.matchAgainst(expectedTargetData, "wasn't animated to target 'to' pos", "shouldn't have been animated to target 'to' pos"); + model_displacedItems_transitionVia.matchAgainst(expectedDisplacedValues, "wasn't animated with displaced anim", "shouldn't have been animated with displaced anim"); + + // check attached properties + QCOMPARE(listview->property("targetTrans_items").toMap(), expectedTargets); + matchIndexLists(listview->property("targetTrans_targetIndexes").toList(), targetIndexes); + matchItemLists(listview->property("targetTrans_targetItems").toList(), targetItems); + if (expectedDisplacedIndexes.isValid()) { + // adjust expectedDisplacedIndexes to their final values after the move + QList displacedIndexes = adjustIndexesForRemoveDisplaced(expectedDisplacedIndexes.indexes, removalIndex, removalCount); + matchItemsAndIndexes(listview->property("displacedTrans_items").toMap(), model, displacedIndexes); + matchIndexLists(listview->property("displacedTrans_targetIndexes").toList(), targetIndexes); + matchItemLists(listview->property("displacedTrans_targetItems").toList(), targetItems); + } + } else { + QTRY_COMPARE(model_targetItems_transitionTo.count(), 0); + QTRY_COMPARE(model_displacedItems_transitionVia.count(), 0); + } + + QList items = findItems(contentItem, "wrapper"); + int firstVisibleIndex = -1; + int itemCount = items.count(); + + for (int i=0; iy() >= contentY) + firstVisibleIndex = index; + if (index < 0) + itemCount--; // exclude deleted items + } + QVERIFY2(firstVisibleIndex >= 0, QTest::toString(firstVisibleIndex)); + + // verify all items moved to the correct final positions + for (int i=firstVisibleIndex; i < model.count() && i < itemCount; ++i) { + QQuickItem *item = findItem(contentItem, "wrapper", i); + QVERIFY2(item, QTest::toString(QString("Item %1 not found").arg(i))); + QCOMPARE(item->x(), 0.0); + QCOMPARE(item->y(), contentY + (i-firstVisibleIndex) * 20.0); + QQuickText *name = findItem(contentItem, "textName", i); + QVERIFY(name != 0); + QTRY_COMPARE(name->text(), model.name(i)); + } + + delete canvas; + delete testObject; +} + +void tst_QQuickListView::removeTransitions_data() +{ + QTest::addColumn("initialItemCount"); + QTest::addColumn("contentY"); + QTest::addColumn("shouldAnimateTargets"); + QTest::addColumn("removalIndex"); + QTest::addColumn("removalCount"); + QTest::addColumn("expectedDisplacedIndexes"); + + // All items that are visible following the remove operation should be animated. + // Remove targets that are outside of the view should not be animated. + + QTest::newRow("remove 1 before start") + << 30 << 20.0 * 3 << false + << 2 << 1 << ListRange(); + QTest::newRow("remove multiple, all before start") + << 30 << 20.0 * 3 << false + << 0 << 3 << ListRange(); + QTest::newRow("remove mix of before and after start") + << 30 << 20.0 * 3 << true + << 2 << 3 << ListRange(5, 20); // 5-20 are visible after the remove + + QTest::newRow("remove 1 from start") + << 30 << 0.0 << true + << 0 << 1 << ListRange(1, 16); // 1-16 are visible after the remove + QTest::newRow("remove multiple from start") + << 30 << 0.0 << true + << 0 << 3 << ListRange(3, 18); // 3-18 are visible after the remove + QTest::newRow("remove 1 from start, content y not 0") + << 30 << 20.0 * 2 << true // first visible is index 2, so translate the displaced indexes by 2 + << 2 << 1 << ListRange(1 + 2, 16 + 2); + QTest::newRow("remove multiple from start, content y not 0") + << 30 << 20.0 * 2 << true // first visible is index 2 + << 2 << 3 << ListRange(3 + 2, 18 + 2); + + QTest::newRow("remove 1 from middle") + << 30 << 0.0 << true + << 5 << 1 << ListRange(6, 16); + QTest::newRow("remove multiple from middle") + << 30 << 0.0 << true + << 5 << 3 << ListRange(8, 18); + + + QTest::newRow("remove 1 from bottom") + << 30 << 0.0 << true + << 15 << 1 << ListRange(16, 16); + + // remove 15, 16, 17 + // 15 will animate as the target item, 16 & 17 won't be animated since they are outside + // the view, and 18 will be animated as the displaced item to replace the last item + QTest::newRow("remove multiple from bottom") + << 30 << 0.0 << true + << 15 << 3 << ListRange(18, 18); + + QTest::newRow("remove 1 from bottom, content y not 0") + << 30 << 20.0 * 2 << true + << 15 + 2 << 1 << ListRange(16 + 2, 16 + 2); + QTest::newRow("remove multiple from bottom, content y not 0") + << 30 << 20.0 * 2 << true + << 15 + 2 << 3 << ListRange(18 + 2, 18 + 2); + + + QTest::newRow("remove 1 after end") + << 30 << 0.0 << false + << 17 << 1 << ListRange(); + QTest::newRow("remove multiple after end") + << 30 << 0.0 << false + << 17 << 3 << ListRange(); +} + +void tst_QQuickListView::multipleTransitions() +{ + // Tests that if you interrupt a transition in progress with another action that + // cancels the previous transition, the resulting items are still placed correctly. + + QFETCH(int, initialCount); + QFETCH(qreal, contentY); + QFETCH(QList, changes); + + // add transitions on the left, moves on the right + QPointF addTargets_transitionFrom(-50, -50); + QPointF addDisplaced_transitionFrom(-50, 50); + QPointF moveTargets_transitionFrom(50, -50); + QPointF moveDisplaced_transitionFrom(50, 50); + + QmlListModel model; + for (int i = 0; i < initialCount; i++) + model.addItem("Original item" + QString::number(i), ""); + + QQuickView *canvas = createView(); + QDeclarativeContext *ctxt = canvas->rootContext(); + TestObject *testObject = new TestObject; + ctxt->setContextProperty("testModel", &model); + ctxt->setContextProperty("testObject", testObject); + ctxt->setContextProperty("addTargets_transitionFrom", addTargets_transitionFrom); + ctxt->setContextProperty("addDisplaced_transitionFrom", addDisplaced_transitionFrom); + ctxt->setContextProperty("moveTargets_transitionFrom", moveTargets_transitionFrom); + ctxt->setContextProperty("moveDisplaced_transitionFrom", moveDisplaced_transitionFrom); + canvas->setSource(testFileUrl("multipleTransitions.qml")); + canvas->show(); + + QQuickListView *listview = findItem(canvas->rootObject(), "list"); + QTRY_VERIFY(listview != 0); + QQuickItem *contentItem = listview->contentItem(); + QVERIFY(contentItem != 0); + QTRY_COMPARE(QQuickItemPrivate::get(listview)->polishScheduled, false); + + int timeBetweenActions = canvas->rootObject()->property("timeBetweenActions").toInt(); + + QList > targetItems; + for (int i=0; icount()); + QTRY_VERIFY(listview->property("runningAddTargets").toBool()); + QTRY_VERIFY(listview->property("runningAddDisplaced").toBool()); + if (i == changes.count() - 1) { + QTRY_VERIFY(!listview->property("runningAddTargets").toBool()); + QTRY_VERIFY(!listview->property("runningAddDisplaced").toBool()); + } else { + QTest::qWait(timeBetweenActions); + } + break; + } + case ListChange::Removed: + for (int j=changes[i].index; jcount()); + QTRY_VERIFY(listview->property("runningRemoveTargets").toBool()); + QTRY_VERIFY(listview->property("runningRemoveDisplaced").toBool()); + if (i == changes.count() - 1) { + QTRY_VERIFY(!listview->property("runningRemoveTargets").toBool()); + QTRY_VERIFY(!listview->property("runningRemoveDisplaced").toBool()); + } else { + QTest::qWait(timeBetweenActions); + } + break; + case ListChange::Moved: + for (int j=changes[i].index; jproperty("runningMoveTargets").toBool()); + QTRY_VERIFY(listview->property("runningMoveDisplaced").toBool()); + if (i == changes.count() - 1) { + QTRY_VERIFY(!listview->property("runningMoveTargets").toBool()); + QTRY_VERIFY(!listview->property("runningMoveDisplaced").toBool()); + } else { + QTest::qWait(timeBetweenActions); + } + break; + case ListChange::SetCurrent: + listview->setCurrentIndex(changes[i].index); + break; + case ListChange::SetContentY: + listview->setContentY(changes[i].pos); + QTRY_COMPARE(QQuickItemPrivate::get(listview)->polishScheduled, false); + break; + } + } + QCOMPARE(listview->count(), model.count()); + + QList items = findItems(contentItem, "wrapper"); + int firstVisibleIndex = -1; + for (int i=0; iy() >= contentY) { + QDeclarativeExpression e(qmlContext(items[i]), items[i], "index"); + firstVisibleIndex = e.evaluate().toInt(); + break; + } + } + QVERIFY2(firstVisibleIndex >= 0, QTest::toString(firstVisibleIndex)); + + // verify all items moved to the correct final positions + int itemCount = findItems(contentItem, "wrapper").count(); + for (int i=firstVisibleIndex; i < model.count() && i < itemCount; ++i) { + QQuickItem *item = findItem(contentItem, "wrapper", i); + QVERIFY2(item, QTest::toString(QString("Item %1 not found").arg(i))); + QTRY_COMPARE(item->x(), 0.0); + QTRY_COMPARE(item->y(), i*20.0); + QQuickText *name = findItem(contentItem, "textName", i); + QVERIFY(name != 0); + QTRY_COMPARE(name->text(), model.name(i)); + } + + delete canvas; + delete testObject; +} + +void tst_QQuickListView::multipleTransitions_data() +{ + QTest::addColumn("initialCount"); + QTest::addColumn("contentY"); + QTest::addColumn >("changes"); + + // the added item and displaced items should move to final dest correctly + QTest::newRow("add item, then move it immediately") << 10 << 0.0 << (QList() + << ListChange::insert(0, 1) + << ListChange::move(0, 3, 1) + ); + + // items affected by the add should change from move to add transition + QTest::newRow("move, then insert item before the moved item") << 20 << 0.0 << (QList() + << ListChange::move(1, 10, 3) + << ListChange::insert(0, 1) + ); + + // items should be placed correctly if you trigger a transition then refill for that index + QTest::newRow("add at 0, flick down, flick back to top and add at 0 again") << 20 << 0.0 << (QList() + << ListChange::insert(0, 1) + << ListChange::setContentY(80.0) + << ListChange::setContentY(0.0) + << ListChange::insert(0, 1) + ); +} + +QList tst_QQuickListView::toIntList(const QVariantList &list) +{ + QList ret; + bool ok = true; + for (int i=0; i &expectedIndexes) +{ + for (int i=0; i current = indexLists[i].value >().toSet(); + if (current != expectedIndexes.toSet()) + qDebug() << "Cannot match actual targets" << current << "with expected" << expectedIndexes; + QCOMPARE(current, expectedIndexes.toSet()); + } +} + +void tst_QQuickListView::matchItemsAndIndexes(const QVariantMap &items, const QaimModel &model, const QList &expectedIndexes) +{ + for (QVariantMap::const_iterator it = items.begin(); it != items.end(); ++it) { + QVERIFY(it.value().type() == QVariant::Int); + QString name = it.key(); + int itemIndex = it.value().toInt(); + QVERIFY2(expectedIndexes.contains(itemIndex), QTest::toString(QString("Index %1 not found in expectedIndexes").arg(itemIndex))); + if (model.name(itemIndex) != name) + qDebug() << itemIndex; + QCOMPARE(model.name(itemIndex), name); + } + QCOMPARE(items.count(), expectedIndexes.count()); +} + +void tst_QQuickListView::matchItemLists(const QVariantList &itemLists, const QList &expectedItems) +{ + for (int i=0; i(current[j].value()); + QVERIFY2(o, QTest::toString(QString("Invalid actual item at %1").arg(j))); + QVERIFY2(expectedItems.contains(o), QTest::toString(QString("Cannot match item %1").arg(j))); + } + QCOMPARE(current.count(), expectedItems.count()); + } +} + QTEST_MAIN(tst_QQuickListView) diff --git a/tests/auto/qtquick2/shared/viewtestutil.cpp b/tests/auto/qtquick2/shared/viewtestutil.cpp index ed2066d6aa..eeef23001e 100644 --- a/tests/auto/qtquick2/shared/viewtestutil.cpp +++ b/tests/auto/qtquick2/shared/viewtestutil.cpp @@ -99,6 +99,53 @@ void QQuickViewTestUtil::flick(QQuickView *canvas, const QPoint &from, const QPo QTest::qWait(50); } +QList QQuickViewTestUtil::adjustIndexesForAddDisplaced(const QList &indexes, int index, int count) +{ + QList result; + for (int i=0; i= index) { + num += count; + } + result << num; + } + return result; +} + +QList QQuickViewTestUtil::adjustIndexesForMove(const QList &indexes, int from, int to, int count) +{ + QList result; + for (int i=0; i= from && num < from + count) + num += (to - from); // target + else if (num >= from && num < to + count) + num -= count; // displaced + } else if (from > to) { + if (num >= from && num < from + count) + num -= (from - to); // target + else if (num >= to && num < from + count) + num += count; // displaced + } + result << num; + } + return result; +} + +QList QQuickViewTestUtil::adjustIndexesForRemoveDisplaced(const QList &indexes, int index, int count) +{ + QList result; + for (int i=0; i= index) + num -= count; + result << num; + } + return result; +} + + QQuickViewTestUtil::QmlListModel::QmlListModel(QObject *parent) : QListModelInterface(parent) { @@ -228,6 +275,17 @@ void QQuickViewTestUtil::QmlListModel::clear() { emit itemsRemoved(0, count); } +void QQuickViewTestUtil::QmlListModel::matchAgainst(const QList > &other, const QString &error1, const QString &error2) { + for (int i=0; i > &other, const QString &error1, const QString &error2) { + for (int i=0; i > QQuickViewTestUtil::ListRange::getModelDataValues(const QmlListModel &model) +{ + QList > data; + if (!valid) + return data; + for (int i=0; i > QQuickViewTestUtil::ListRange::getModelDataValues(const QaimModel &model) +{ + QList > data; + if (!valid) + return data; + for (int i=0; i adjustIndexesForAddDisplaced(const QList &indexes, int index, int count); + QList adjustIndexesForMove(const QList &indexes, int from, int to, int count); + QList adjustIndexesForRemoveDisplaced(const QList &indexes, int index, int count); + struct ListChange { - enum { Inserted, Removed, Moved, SetCurrent } type; + enum { Inserted, Removed, Moved, SetCurrent, SetContentY } type; int index; int count; int to; // Move + qreal pos; // setContentY - static ListChange insert(int index, int count = 1) { ListChange c = { Inserted, index, count, -1 }; return c; } - static ListChange remove(int index, int count = 1) { ListChange c = { Removed, index, count, -1 }; return c; } - static ListChange move(int index, int to, int count) { ListChange c = { Moved, index, count, to }; return c; } - static ListChange setCurrent(int index) { ListChange c = { SetCurrent, index, -1, -1 }; return c; } + static ListChange insert(int index, int count = 1) { ListChange c = { Inserted, index, count, -1, 0.0 }; return c; } + static ListChange remove(int index, int count = 1) { ListChange c = { Removed, index, count, -1, 0.0 }; return c; } + static ListChange move(int index, int to, int count) { ListChange c = { Moved, index, count, to, 0.0 }; return c; } + static ListChange setCurrent(int index) { ListChange c = { SetCurrent, index, -1, -1, 0.0 }; return c; } + static ListChange setContentY(qreal pos) { ListChange c = { SetContentY, -1, -1, -1, pos }; return c; } }; class QmlListModel : public QListModelInterface @@ -87,7 +93,7 @@ namespace QQuickViewTestUtil QVariant data(int index, int role) const; QHash data(int index, const QList &roles) const; - void addItem(const QString &name, const QString &number); + Q_INVOKABLE void addItem(const QString &name, const QString &number); void insertItem(int index, const QString &name, const QString &number); void insertItems(int index, const QList > &items); @@ -101,6 +107,8 @@ namespace QQuickViewTestUtil void clear(); + void matchAgainst(const QList > &other, const QString &error1, const QString &error2); + private: QList > list; }; @@ -120,7 +128,7 @@ namespace QQuickViewTestUtil QString name(int index) const; QString number(int index) const; - void addItem(const QString &name, const QString &number); + Q_INVOKABLE void addItem(const QString &name, const QString &number); void addItems(const QList > &items); void insertItem(int index, const QString &name, const QString &number); void insertItems(int index, const QList > &items); @@ -134,13 +142,39 @@ namespace QQuickViewTestUtil void modifyItem(int idx, const QString &name, const QString &number); void clear(); + void reset(); + + void matchAgainst(const QList > &other, const QString &error1, const QString &error2); private: QList > list; }; + class ListRange + { + public: + ListRange(); + ListRange(const ListRange &other); + ListRange(int start, int end); + + ~ListRange(); + + ListRange operator+(const ListRange &other) const; + bool operator==(const ListRange &other) const; + bool operator!=(const ListRange &other) const; + + bool isValid() const; + int count() const; + + QList > getModelDataValues(const QmlListModel &model); + QList > getModelDataValues(const QaimModel &model); + + QList indexes; + bool valid; + }; } Q_DECLARE_METATYPE(QList) +Q_DECLARE_METATYPE(QQuickViewTestUtil::ListRange) #endif // QQUICKVIEWTESTUTIL_H diff --git a/tests/auto/qtquick2/shared/visualtestutil.h b/tests/auto/qtquick2/shared/visualtestutil.h index ceed4b04a7..09bb03c002 100644 --- a/tests/auto/qtquick2/shared/visualtestutil.h +++ b/tests/auto/qtquick2/shared/visualtestutil.h @@ -97,6 +97,16 @@ namespace QQuickVisualTestUtil return items; } + + template + QList findItems(QQuickItem *parent, const QString &objectName, const QList &indexes) + { + QList items; + for (int i=0; i(findItem(parent, objectName, indexes[i])); + return items; + } + } #endif // QQUICKVISUALTESTUTIL_H -- cgit v1.2.3