aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--src/imports/controls/Tumbler.qml27
-rw-r--r--src/imports/controls/doc/qtquickcontrols2.qdocconf1
-rw-r--r--src/imports/controls/doc/snippets/qtquickcontrols2-tumbler-listView.qml46
-rw-r--r--src/imports/controls/doc/snippets/qtquickcontrols2-tumbler-pathView.qml57
-rw-r--r--src/imports/controls/doc/snippets/qtquickcontrols2-tumbler-timePicker.qml103
-rw-r--r--src/imports/controls/doc/src/qtquickcontrols2-customize.qdoc9
-rw-r--r--src/imports/controls/material/Tumbler.qml23
-rw-r--r--src/imports/controls/plugins.qmltypes3
-rw-r--r--src/imports/controls/qtquickcontrols2plugin.cpp3
-rw-r--r--src/imports/controls/universal/Tumbler.qml23
-rw-r--r--src/imports/templates/qtquicktemplates2plugin.cpp1
-rw-r--r--src/quickcontrols2/qquicktumblerview.cpp238
-rw-r--r--src/quickcontrols2/qquicktumblerview_p.h108
-rw-r--r--src/quickcontrols2/quickcontrols2.pri6
-rw-r--r--src/quicktemplates2/qquicktumbler.cpp388
-rw-r--r--src/quicktemplates2/qquicktumbler_p.h6
-rw-r--r--tests/auto/controls/data/TumblerDatePicker.qml2
-rw-r--r--tests/auto/controls/data/TumblerListView.qml53
-rw-r--r--tests/auto/controls/data/TumblerPathView.qml63
-rw-r--r--tests/auto/controls/data/tst_tumbler.qml321
-rw-r--r--tests/manual/gifs/data/qtquickcontrols2-tumbler-wrap.qml79
-rw-r--r--tests/manual/gifs/tst_gifs.cpp65
22 files changed, 1307 insertions, 318 deletions
diff --git a/src/imports/controls/Tumbler.qml b/src/imports/controls/Tumbler.qml
index 13bc0859..371c705f 100644
--- a/src/imports/controls/Tumbler.qml
+++ b/src/imports/controls/Tumbler.qml
@@ -35,15 +35,15 @@
****************************************************************************/
import QtQuick 2.6
-import QtQuick.Controls 2.0
-import QtQuick.Templates 2.0 as T
+import QtQuick.Controls 2.1
+import QtQuick.Controls.impl 2.1
+import QtQuick.Templates 2.1 as T
T.Tumbler {
id: control
implicitWidth: 60
implicitHeight: 200
- //! [delegate]
delegate: Text {
id: label
text: modelData
@@ -53,29 +53,20 @@ T.Tumbler {
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
}
- //! [delegate]
- //! [contentItem]
- contentItem: PathView {
- id: pathView
+ contentItem: TumblerView {
+ id: tumblerView
model: control.model
delegate: control.delegate
- clip: true
- pathItemCount: control.visibleItemCount + 1
- preferredHighlightBegin: 0.5
- preferredHighlightEnd: 0.5
- dragMargin: width / 2
-
path: Path {
- startX: pathView.width / 2
- startY: -pathView.delegateHeight / 2
+ startX: tumblerView.width / 2
+ startY: -tumblerView.delegateHeight / 2
PathLine {
- x: pathView.width / 2
- y: pathView.pathItemCount * pathView.delegateHeight - pathView.delegateHeight / 2
+ x: tumblerView.width / 2
+ y: (control.visibleItemCount + 1) * tumblerView.delegateHeight - tumblerView.delegateHeight / 2
}
}
property real delegateHeight: control.availableHeight / control.visibleItemCount
}
- //! [contentItem]
}
diff --git a/src/imports/controls/doc/qtquickcontrols2.qdocconf b/src/imports/controls/doc/qtquickcontrols2.qdocconf
index 95c438a6..73f9bdfc 100644
--- a/src/imports/controls/doc/qtquickcontrols2.qdocconf
+++ b/src/imports/controls/doc/qtquickcontrols2.qdocconf
@@ -38,7 +38,6 @@ exampledirs += ../../../../examples/quickcontrols2 \
../ \
../../../quicktemplates2 \
../../calendar \
- ../../../../tests/auto/controls/data \
snippets
examplesinstallpath = quickcontrols2
diff --git a/src/imports/controls/doc/snippets/qtquickcontrols2-tumbler-listView.qml b/src/imports/controls/doc/snippets/qtquickcontrols2-tumbler-listView.qml
new file mode 100644
index 00000000..ff3ecd81
--- /dev/null
+++ b/src/imports/controls/doc/snippets/qtquickcontrols2-tumbler-listView.qml
@@ -0,0 +1,46 @@
+/****************************************************************************
+**
+** Copyright (C) 2016 The Qt Company Ltd.
+** Contact: http://www.qt.io/licensing/
+**
+** This file is part of the documentation of the Qt Toolkit.
+**
+** $QT_BEGIN_LICENSE:FDL$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see http://www.qt.io/terms-conditions. For further
+** information use the contact form at http://www.qt.io/contact-us.
+**
+** GNU Free Documentation License Usage
+** Alternatively, this file may be used under the terms of the GNU Free
+** Documentation License version 1.3 as published by the Free Software
+** Foundation and appearing in the file included in the packaging of
+** this file. Please review the following information to ensure
+** the GNU Free Documentation License version 1.3 requirements
+** will be met: http://www.gnu.org/copyleft/fdl.html.
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+import QtQuick 2.8
+import QtQuick.Controls 2.1
+
+//! [contentItem]
+Tumbler {
+ id: tumbler
+
+ contentItem: ListView {
+ model: tumbler.model
+ delegate: tumbler.delegate
+
+ snapMode: ListView.SnapToItem
+ highlightRangeMode: ListView.StrictlyEnforceRange
+ preferredHighlightBegin: height / 2 - (height / tumbler.visibleItemCount / 2)
+ preferredHighlightEnd: height / 2 + (height / tumbler.visibleItemCount / 2)
+ clip: true
+ }
+}
+//! [contentItem]
diff --git a/src/imports/controls/doc/snippets/qtquickcontrols2-tumbler-pathView.qml b/src/imports/controls/doc/snippets/qtquickcontrols2-tumbler-pathView.qml
new file mode 100644
index 00000000..957112e0
--- /dev/null
+++ b/src/imports/controls/doc/snippets/qtquickcontrols2-tumbler-pathView.qml
@@ -0,0 +1,57 @@
+/****************************************************************************
+**
+** Copyright (C) 2016 The Qt Company Ltd.
+** Contact: http://www.qt.io/licensing/
+**
+** This file is part of the documentation of the Qt Toolkit.
+**
+** $QT_BEGIN_LICENSE:FDL$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see http://www.qt.io/terms-conditions. For further
+** information use the contact form at http://www.qt.io/contact-us.
+**
+** GNU Free Documentation License Usage
+** Alternatively, this file may be used under the terms of the GNU Free
+** Documentation License version 1.3 as published by the Free Software
+** Foundation and appearing in the file included in the packaging of
+** this file. Please review the following information to ensure
+** the GNU Free Documentation License version 1.3 requirements
+** will be met: http://www.gnu.org/copyleft/fdl.html.
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+import QtQuick 2.8
+import QtQuick.Controls 2.1
+
+//! [contentItem]
+Tumbler {
+ id: tumbler
+
+ contentItem: PathView {
+ id: pathView
+ model: tumbler.model
+ delegate: tumbler.delegate
+ clip: true
+ pathItemCount: tumbler.visibleItemCount + 1
+ preferredHighlightBegin: 0.5
+ preferredHighlightEnd: 0.5
+ dragMargin: width / 2
+
+ path: Path {
+ startX: pathView.width / 2
+ startY: -pathView.delegateHeight / 2
+ PathLine {
+ x: pathView.width / 2
+ y: pathView.pathItemCount * pathView.delegateHeight - pathView.delegateHeight / 2
+ }
+ }
+
+ property real delegateHeight: tumbler.availableHeight / tumbler.visibleItemCount
+ }
+}
+//! [contentItem]
diff --git a/src/imports/controls/doc/snippets/qtquickcontrols2-tumbler-timePicker.qml b/src/imports/controls/doc/snippets/qtquickcontrols2-tumbler-timePicker.qml
new file mode 100644
index 00000000..e62238ca
--- /dev/null
+++ b/src/imports/controls/doc/snippets/qtquickcontrols2-tumbler-timePicker.qml
@@ -0,0 +1,103 @@
+/****************************************************************************
+**
+** Copyright (C) 2016 The Qt Company Ltd.
+** Contact: http://www.qt.io/licensing/
+**
+** This file is part of the test suite 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 The Qt Company Ltd nor the names of its
+** contributors may be used to endorse or promote products derived
+** from this software without specific prior written permission.
+**
+**
+** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE."
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+//! [tumbler]
+import QtQuick 2.8
+import QtQuick.Window 2.2
+import QtQuick.Controls 2.1
+
+Rectangle {
+ width: frame.implicitWidth + 10
+ height: frame.implicitHeight + 10
+
+ function formatText(count, modelData) {
+ var data = count === 12 ? modelData + 1 : modelData;
+ return data.toString().length < 2 ? "0" + data : data;
+ }
+
+ FontMetrics {
+ id: fontMetrics
+ }
+
+ Component {
+ id: delegateComponent
+
+ Label {
+ text: formatText(Tumbler.tumbler.count, modelData)
+ opacity: 1.0 - Math.abs(Tumbler.displacement) / (Tumbler.tumbler.visibleItemCount / 2)
+ horizontalAlignment: Text.AlignHCenter
+ verticalAlignment: Text.AlignVCenter
+ font.pixelSize: fontMetrics.font.pixelSize * 1.25
+ }
+ }
+
+ Frame {
+ id: frame
+ padding: 0
+ anchors.centerIn: parent
+
+ Row {
+ id: row
+
+ Tumbler {
+ id: hoursTumbler
+ visibleItemCount: 5
+ model: 12
+ delegate: delegateComponent
+ }
+
+ Tumbler {
+ id: minutesTumbler
+ visibleItemCount: 5
+ model: 60
+ delegate: delegateComponent
+ }
+
+ Tumbler {
+ id: amPmTumbler
+ visibleItemCount: 5
+ wrap: false
+ model: ["AM", "PM"]
+ delegate: delegateComponent
+ }
+ }
+ }
+}
+//! [tumbler]
diff --git a/src/imports/controls/doc/src/qtquickcontrols2-customize.qdoc b/src/imports/controls/doc/src/qtquickcontrols2-customize.qdoc
index 559c89f0..a60969c0 100644
--- a/src/imports/controls/doc/src/qtquickcontrols2-customize.qdoc
+++ b/src/imports/controls/doc/src/qtquickcontrols2-customize.qdoc
@@ -697,4 +697,13 @@
\image qtquickcontrols2-tumbler-custom.png
\snippet qtquickcontrols2-tumbler-custom.qml file
+
+ If you want to define your own contentItem, use either a \l ListView or
+ \l PathView as the root item. For a wrapping Tumbler, use PathView:
+
+ \snippet qtquickcontrols2-tumbler-pathView.qml contentItem
+
+ For a non-wrapping Tumbler, use ListView:
+
+ \snippet qtquickcontrols2-tumbler-listView.qml contentItem
*/
diff --git a/src/imports/controls/material/Tumbler.qml b/src/imports/controls/material/Tumbler.qml
index 7e914319..3587ea42 100644
--- a/src/imports/controls/material/Tumbler.qml
+++ b/src/imports/controls/material/Tumbler.qml
@@ -35,8 +35,9 @@
****************************************************************************/
import QtQuick 2.6
-import QtQuick.Controls 2.0
-import QtQuick.Templates 2.0 as T
+import QtQuick.Controls 2.1
+import QtQuick.Controls.impl 2.1
+import QtQuick.Templates 2.1 as T
import QtQuick.Controls.Material 2.0
T.Tumbler {
@@ -54,22 +55,16 @@ T.Tumbler {
verticalAlignment: Text.AlignVCenter
}
- contentItem: PathView {
- id: pathView
+ contentItem: TumblerView {
+ id: tumblerView
model: control.model
delegate: control.delegate
- clip: true
- pathItemCount: control.visibleItemCount + 1
- preferredHighlightBegin: 0.5
- preferredHighlightEnd: 0.5
- dragMargin: width / 2
-
path: Path {
- startX: pathView.width / 2
- startY: -pathView.delegateHeight / 2
+ startX: tumblerView.width / 2
+ startY: -tumblerView.delegateHeight / 2
PathLine {
- x: pathView.width / 2
- y: pathView.pathItemCount * pathView.delegateHeight - pathView.delegateHeight / 2
+ x: tumblerView.width / 2
+ y: (control.visibleItemCount + 1) * tumblerView.delegateHeight - tumblerView.delegateHeight / 2
}
}
diff --git a/src/imports/controls/plugins.qmltypes b/src/imports/controls/plugins.qmltypes
index e661056c..c8cde812 100644
--- a/src/imports/controls/plugins.qmltypes
+++ b/src/imports/controls/plugins.qmltypes
@@ -2055,7 +2055,7 @@ Module {
name: "QQuickTumbler"
defaultProperty: "data"
prototype: "QQuickControl"
- exports: ["QtQuick.Templates/Tumbler 2.0"]
+ exports: ["QtQuick.Templates/Tumbler 2.1"]
exportMetaObjectRevisions: [0]
attachedType: "QQuickTumblerAttached"
Property { name: "model"; type: "QVariant" }
@@ -2064,6 +2064,7 @@ Module {
Property { name: "currentItem"; type: "QQuickItem"; isReadonly: true; isPointer: true }
Property { name: "delegate"; type: "QQmlComponent"; isPointer: true }
Property { name: "visibleItemCount"; type: "int" }
+ Property { name: "wrap"; type: "bool" }
}
Component {
name: "QQuickTumblerAttached"
diff --git a/src/imports/controls/qtquickcontrols2plugin.cpp b/src/imports/controls/qtquickcontrols2plugin.cpp
index 84c17267..4cfbd340 100644
--- a/src/imports/controls/qtquickcontrols2plugin.cpp
+++ b/src/imports/controls/qtquickcontrols2plugin.cpp
@@ -49,6 +49,7 @@
#include <QtQuickControls2/private/qquickstyleplugin_p.h>
#include <QtQuickControls2/private/qquickstyleselector_p.h>
#include <QtQuickControls2/private/qquickcolorimageprovider_p.h>
+#include <QtQuickControls2/private/qquicktumblerview_p.h>
#include "qquickbusyindicatorring_p.h"
#include "qquickdialring_p.h"
@@ -142,6 +143,7 @@ void QtQuickControls2Plugin::registerTypes(const char *uri)
qmlRegisterType(selector.select(QStringLiteral("Slider.qml")), uri, 2, 1, "Slider");
qmlRegisterType(selector.select(QStringLiteral("StackView.qml")), uri, 2, 1, "StackView");
qmlRegisterType(selector.select(QStringLiteral("SwipeView.qml")), uri, 2, 1, "SwipeView");
+ qmlRegisterType(selector.select(QStringLiteral("Tumbler.qml")), uri, 2, 1, "Tumbler");
}
void QtQuickControls2Plugin::initializeEngine(QQmlEngine *engine, const char *uri)
@@ -156,6 +158,7 @@ void QtQuickControls2Plugin::initializeEngine(QQmlEngine *engine, const char *ur
qmlRegisterType<QQuickProgressStrip>(import, 2, 0, "ProgressStrip");
qmlRegisterType<QQuickProgressAnimator>(import, 2, 0, "ProgressStripAnimator");
qmlRegisterType<QQuickDialRing>(import, 2, 0, "DialRing");
+ qmlRegisterType<QQuickTumblerView>(import, 2, 1, "TumblerView");
qmlRegisterType(typeUrl(QStringLiteral("CheckIndicator.qml")), import, 2, 0, "CheckIndicator");
qmlRegisterType(typeUrl(QStringLiteral("RadioIndicator.qml")), import, 2, 0, "RadioIndicator");
diff --git a/src/imports/controls/universal/Tumbler.qml b/src/imports/controls/universal/Tumbler.qml
index 7b134b84..ead3a9d3 100644
--- a/src/imports/controls/universal/Tumbler.qml
+++ b/src/imports/controls/universal/Tumbler.qml
@@ -35,9 +35,10 @@
****************************************************************************/
import QtQuick 2.6
-import QtQuick.Templates 2.0 as T
+import QtQuick.Templates 2.1 as T
import QtQuick.Controls.Universal 2.0
-import QtQuick.Controls 2.0
+import QtQuick.Controls 2.1
+import QtQuick.Controls.impl 2.1
T.Tumbler {
id: control
@@ -54,22 +55,16 @@ T.Tumbler {
verticalAlignment: Text.AlignVCenter
}
- contentItem: PathView {
- id: pathView
+ contentItem: TumblerView {
+ id: tumblerView
model: control.model
delegate: control.delegate
- clip: true
- pathItemCount: control.visibleItemCount + 1
- preferredHighlightBegin: 0.5
- preferredHighlightEnd: 0.5
- dragMargin: width / 2
-
path: Path {
- startX: pathView.width / 2
- startY: -pathView.delegateHeight / 2
+ startX: tumblerView.width / 2
+ startY: -tumblerView.delegateHeight / 2
PathLine {
- x: pathView.width / 2
- y: pathView.pathItemCount * pathView.delegateHeight - pathView.delegateHeight / 2
+ x: tumblerView.width / 2
+ y: (control.visibleItemCount + 1) * tumblerView.delegateHeight - tumblerView.delegateHeight / 2
}
}
diff --git a/src/imports/templates/qtquicktemplates2plugin.cpp b/src/imports/templates/qtquicktemplates2plugin.cpp
index 07a4653f..28162e61 100644
--- a/src/imports/templates/qtquicktemplates2plugin.cpp
+++ b/src/imports/templates/qtquicktemplates2plugin.cpp
@@ -181,6 +181,7 @@ void QtQuickTemplates2Plugin::registerTypes(const char *uri)
qmlRegisterType<QQuickSlider, 1>(uri, 2, 1, "Slider");
qmlRegisterType<QQuickStackView, 1>(uri, 2, 1, "StackView");
qmlRegisterType<QQuickSwipeView, 1>(uri, 2, 1, "SwipeView");
+ qmlRegisterType<QQuickTumbler, 1>(uri, 2, 1, "Tumbler");
}
QT_END_NAMESPACE
diff --git a/src/quickcontrols2/qquicktumblerview.cpp b/src/quickcontrols2/qquicktumblerview.cpp
new file mode 100644
index 00000000..93906408
--- /dev/null
+++ b/src/quickcontrols2/qquicktumblerview.cpp
@@ -0,0 +1,238 @@
+/****************************************************************************
+**
+** Copyright (C) 2016 The Qt Company Ltd.
+** Contact: http://www.qt.io/licensing/
+**
+** This file is part of the Qt Quick Controls 2 module of the Qt Toolkit.
+**
+** $QT_BEGIN_LICENSE:LGPL3$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see http://www.qt.io/terms-conditions. For further
+** information use the contact form at http://www.qt.io/contact-us.
+**
+** GNU Lesser General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU Lesser
+** General Public License version 3 as published by the Free Software
+** Foundation and appearing in the file LICENSE.LGPLv3 included in the
+** packaging of this file. Please review the following information to
+** ensure the GNU Lesser General Public License version 3 requirements
+** will be met: https://www.gnu.org/licenses/lgpl.html.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 2.0 or later as published by the Free
+** Software Foundation and appearing in the file LICENSE.GPL included in
+** the packaging of this file. Please review the following information to
+** ensure the GNU General Public License version 2.0 requirements will be
+** met: http://www.gnu.org/licenses/gpl-2.0.html.
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+#include "qquicktumblerview_p.h"
+
+#include <QtQml/private/qqmldelegatemodel_p.h>
+#include <QtQuick/private/qquickitem_p.h>
+#include <QtQuick/private/qquicklistview_p.h>
+#include <QtQuick/private/qquickpathview_p.h>
+
+#include <QtQuickTemplates2/private/qquicktumbler_p.h>
+
+QT_BEGIN_NAMESPACE
+
+QQuickTumblerView::QQuickTumblerView(QQuickItem *parent) :
+ QQuickItem(parent),
+ m_tumbler(nullptr),
+ m_delegate(nullptr),
+ m_pathView(nullptr),
+ m_listView(nullptr),
+ m_path(nullptr)
+{
+ // We don't call createView() here because we don't know what the wrap flag is set to
+ // yet, and we don't want to create a view that might never get used.
+}
+
+QVariant QQuickTumblerView::model() const
+{
+ return m_model;
+}
+
+void QQuickTumblerView::setModel(const QVariant &model)
+{
+ if (model == m_model)
+ return;
+
+ m_model = model;
+
+ if (m_pathView) {
+ m_pathView->setModel(m_model);
+ } else if (m_listView) {
+ // QQuickItemView::setModel() resets the current index,
+ // but if we're still creating the Tumbler, it should be maintained.
+ const int oldCurrentIndex = m_listView->currentIndex();
+ m_listView->setModel(m_model);
+ if (!isComponentComplete())
+ m_listView->setCurrentIndex(oldCurrentIndex);
+ }
+
+ emit modelChanged();
+}
+
+QQmlComponent *QQuickTumblerView::delegate() const
+{
+ return m_delegate;
+}
+
+void QQuickTumblerView::setDelegate(QQmlComponent *delegate)
+{
+ if (delegate == m_delegate)
+ return;
+
+ m_delegate = delegate;
+
+ if (m_pathView)
+ m_pathView->setDelegate(m_delegate);
+ else if (m_listView)
+ m_listView->setDelegate(m_delegate);
+
+ emit delegateChanged();
+}
+
+QQuickPath *QQuickTumblerView::path() const
+{
+ return m_path;
+}
+
+void QQuickTumblerView::setPath(QQuickPath *path)
+{
+ if (path == m_path)
+ return;
+
+ m_path = path;
+ emit pathChanged();
+}
+
+void QQuickTumblerView::createView()
+{
+ Q_ASSERT(m_tumbler);
+
+ if (m_tumbler->wrap()) {
+ if (m_listView) {
+ delete m_listView;
+ m_listView = nullptr;
+ }
+
+ if (!m_pathView) {
+ m_pathView = new QQuickPathView;
+ QQmlEngine::setContextForObject(m_pathView, qmlContext(this));
+ QQml_setParent_noEvent(m_pathView, this);
+ // QQuickPathView::setPathItemCount() resets the offset animation,
+ // so we just skip the animation while constructing the view.
+ const int oldHighlightMoveDuration = m_pathView->highlightMoveDuration();
+ m_pathView->setHighlightMoveDuration(0);
+ m_pathView->setParentItem(this);
+ m_pathView->setModel(m_model);
+ m_pathView->setPath(m_path);
+ m_pathView->setDelegate(m_delegate);
+ m_pathView->setPreferredHighlightBegin(0.5);
+ m_pathView->setPreferredHighlightEnd(0.5);
+ m_pathView->setClip(true);
+
+ // Give the view a size.
+ updateView();
+
+ m_pathView->setHighlightMoveDuration(oldHighlightMoveDuration);
+ }
+ } else {
+ if (m_pathView) {
+ delete m_pathView;
+ m_pathView = nullptr;
+ }
+
+ if (!m_listView) {
+ m_listView = new QQuickListView;
+ QQmlEngine::setContextForObject(m_listView, qmlContext(this));
+ QQml_setParent_noEvent(m_listView, this);
+ m_listView->setParentItem(this);
+ m_listView->setSnapMode(QQuickListView::SnapToItem);
+ m_listView->setHighlightRangeMode(QQuickListView::StrictlyEnforceRange);
+ m_listView->setClip(true);
+ m_listView->setModel(m_model);
+ m_listView->setDelegate(m_delegate);
+
+ // Give the view a size.
+ updateView();
+ }
+ }
+}
+
+// Called whever the size or visibleItemCount changes.
+void QQuickTumblerView::updateView()
+{
+ QQuickItem *theView = view();
+ if (!theView)
+ return;
+
+ theView->setSize(QSizeF(width(), height()));
+
+ // Can be called in geometryChanged when it might not have a parent item yet.
+ if (!m_tumbler)
+ return;
+
+ // Set view-specific properties that have a dependency on the size, etc.
+ if (m_pathView) {
+ m_pathView->setPathItemCount(m_tumbler->visibleItemCount() + 1);
+ m_pathView->setDragMargin(width() / 2);
+ } else {
+ m_listView->setPreferredHighlightBegin(height() / 2 - (height() / m_tumbler->visibleItemCount() / 2));
+ m_listView->setPreferredHighlightEnd(height() / 2 + (height() / m_tumbler->visibleItemCount() / 2));
+ }
+}
+
+void QQuickTumblerView::geometryChanged(const QRectF &newGeometry, const QRectF &oldGeometry)
+{
+ QQuickItem::geometryChanged(newGeometry, oldGeometry);
+ updateView();
+}
+
+void QQuickTumblerView::componentComplete()
+{
+ QQuickItem::componentComplete();
+ updateView();
+}
+
+void QQuickTumblerView::itemChange(QQuickItem::ItemChange change, const QQuickItem::ItemChangeData &data)
+{
+ QQuickItem::itemChange(change, data);
+
+ if (change == QQuickItem::ItemParentHasChanged && data.item) {
+ if (m_tumbler)
+ m_tumbler->disconnect(this);
+
+ m_tumbler = qobject_cast<QQuickTumbler*>(parentItem());
+
+ if (m_tumbler) {
+ // We assume that the parentChanged() signal of the tumbler will be emitted before its wrap property is set...
+ connect(m_tumbler, &QQuickTumbler::wrapChanged, this, &QQuickTumblerView::createView);
+ connect(m_tumbler, &QQuickTumbler::visibleItemCountChanged, this, &QQuickTumblerView::updateView);
+ }
+ }
+}
+
+QQuickItem *QQuickTumblerView::view()
+{
+ if (!m_tumbler)
+ return nullptr;
+
+ if (m_tumbler->wrap())
+ return m_pathView;
+
+ return m_listView;
+}
+
+QT_END_NAMESPACE
diff --git a/src/quickcontrols2/qquicktumblerview_p.h b/src/quickcontrols2/qquicktumblerview_p.h
new file mode 100644
index 00000000..dad913da
--- /dev/null
+++ b/src/quickcontrols2/qquicktumblerview_p.h
@@ -0,0 +1,108 @@
+/****************************************************************************
+**
+** Copyright (C) 2016 The Qt Company Ltd.
+** Contact: http://www.qt.io/licensing/
+**
+** This file is part of the Qt Quick Controls 2 module of the Qt Toolkit.
+**
+** $QT_BEGIN_LICENSE:LGPL3$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see http://www.qt.io/terms-conditions. For further
+** information use the contact form at http://www.qt.io/contact-us.
+**
+** GNU Lesser General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU Lesser
+** General Public License version 3 as published by the Free Software
+** Foundation and appearing in the file LICENSE.LGPLv3 included in the
+** packaging of this file. Please review the following information to
+** ensure the GNU Lesser General Public License version 3 requirements
+** will be met: https://www.gnu.org/licenses/lgpl.html.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 2.0 or later as published by the Free
+** Software Foundation and appearing in the file LICENSE.GPL included in
+** the packaging of this file. Please review the following information to
+** ensure the GNU General Public License version 2.0 requirements will be
+** met: http://www.gnu.org/licenses/gpl-2.0.html.
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+#ifndef QQUICKTUMBLERVIEW_P_H
+#define QQUICKTUMBLERVIEW_P_H
+
+//
+// W A R N I N G
+// -------------
+//
+// This file is not part of the Qt API. It exists purely as an
+// implementation detail. This header file may change from version to
+// version without notice, or even be removed.
+//
+// We mean it.
+//
+
+#include <QQuickItem>
+#include <QtQuickControls2/private/qtquickcontrols2global_p.h>
+
+QT_BEGIN_NAMESPACE
+
+class QQuickListView;
+class QQuickPath;
+class QQuickPathView;
+
+class QQuickTumbler;
+
+class Q_QUICKCONTROLS2_PRIVATE_EXPORT QQuickTumblerView : public QQuickItem
+{
+ Q_OBJECT
+ Q_PROPERTY(QVariant model READ model WRITE setModel NOTIFY modelChanged)
+ Q_PROPERTY(QQmlComponent *delegate READ delegate WRITE setDelegate NOTIFY delegateChanged)
+ Q_PROPERTY(QQuickPath *path READ path WRITE setPath NOTIFY pathChanged)
+
+public:
+ QQuickTumblerView(QQuickItem *parent = nullptr);
+
+ QVariant model() const;
+ void setModel(const QVariant &model);
+
+ QQmlComponent *delegate() const;
+ void setDelegate(QQmlComponent *delegate);
+
+ QQuickPath *path() const;
+ void setPath(QQuickPath *path);
+
+Q_SIGNALS:
+ void modelChanged();
+ void delegateChanged();
+ void pathChanged();
+
+protected:
+ void geometryChanged(const QRectF &newGeometry, const QRectF &oldGeometry) override;
+ void componentComplete() override;
+ void itemChange(ItemChange change, const ItemChangeData &data) override;
+
+private:
+ QQuickItem *view();
+ void createView();
+ void updateView();
+
+ void wrapChange();
+
+ QQuickTumbler *m_tumbler;
+ QVariant m_model;
+ QQmlComponent *m_delegate;
+ QQuickPathView *m_pathView;
+ QQuickListView *m_listView;
+ QQuickPath *m_path;
+};
+
+QT_END_NAMESPACE
+
+#endif // TUMBLERVIEW_H
diff --git a/src/quickcontrols2/quickcontrols2.pri b/src/quickcontrols2/quickcontrols2.pri
index aaf01ed6..a86c9d12 100644
--- a/src/quickcontrols2/quickcontrols2.pri
+++ b/src/quickcontrols2/quickcontrols2.pri
@@ -6,7 +6,8 @@ HEADERS += \
$$PWD/qquickstyleplugin_p.h \
$$PWD/qquickstyleselector_p.h \
$$PWD/qquickstyleselector_p_p.h \
- $$PWD/qquickpaddedrectangle_p.h
+ $$PWD/qquickpaddedrectangle_p.h \
+ $$PWD/qquicktumblerview_p.h
SOURCES += \
$$PWD/qquickcolorimageprovider.cpp \
@@ -15,4 +16,5 @@ SOURCES += \
$$PWD/qquickstyleattached.cpp \
$$PWD/qquickstyleplugin.cpp \
$$PWD/qquickstyleselector.cpp \
- $$PWD/qquickpaddedrectangle.cpp
+ $$PWD/qquickpaddedrectangle.cpp \
+ $$PWD/qquicktumblerview.cpp
diff --git a/src/quicktemplates2/qquicktumbler.cpp b/src/quicktemplates2/qquicktumbler.cpp
index 580859ef..572f1a7e 100644
--- a/src/quicktemplates2/qquicktumbler.cpp
+++ b/src/quicktemplates2/qquicktumbler.cpp
@@ -57,13 +57,10 @@ QT_BEGIN_NAMESPACE
}
\endcode
- \section1 Non-wrapping Tumbler
+ By default, Tumbler wraps when it reaches the top and bottom. To achieve a
+ non-wrapping Tumbler, set the \l wrap property to \c false:
- The default contentItem of Tumbler is a \l PathView, which wraps when it
- reaches the top and bottom. To achieve a non-wrapping Tumbler, use ListView
- as the contentItem:
-
- \snippet tst_tumbler.qml contentItem
+ \snippet qtquickcontrols2-tumbler-timePicker.qml tumbler
\image qtquickcontrols2-tumbler-wrap.gif
@@ -77,7 +74,13 @@ class QQuickTumblerPrivate : public QQuickControlPrivate, public QQuickItemChang
public:
QQuickTumblerPrivate() :
delegate(nullptr),
- visibleItemCount(3)
+ visibleItemCount(3),
+ wrap(true),
+ view(nullptr),
+ viewContentItem(nullptr),
+ viewContentItemType(UnsupportedContentItemType),
+ currentIndex(-1),
+ ignoreCurrentIndexChanges(false)
{
}
@@ -85,70 +88,91 @@ public:
{
}
+ enum ContentItemType {
+ UnsupportedContentItemType,
+ PathViewContentItem,
+ ListViewContentItem
+ };
+
+ QQuickItem *determineViewType(QQuickItem *contentItem);
+ void resetViewData();
+ QList<QQuickItem *> viewContentItemChildItems() const;
+
+ static QQuickTumblerPrivate *get(QQuickTumbler *tumbler)
+ {
+ return tumbler->d_func();
+ }
+
QVariant model;
QQmlComponent *delegate;
int visibleItemCount;
+ bool wrap;
+ QQuickItem *view;
+ QQuickItem *viewContentItem;
+ ContentItemType viewContentItemType;
+ int currentIndex;
+ bool ignoreCurrentIndexChanges;
void _q_updateItemHeights();
void _q_updateItemWidths();
+ void _q_onViewCurrentIndexChanged();
+
+ void disconnectFromView();
+ void setupViewData(QQuickItem *newControlContentItem);
void itemChildAdded(QQuickItem *, QQuickItem *) override;
void itemChildRemoved(QQuickItem *, QQuickItem *) override;
};
-static QList<QQuickItem *> contentItemChildItems(QQuickItem *contentItem)
-{
- if (!contentItem)
- return QList<QQuickItem *>();
-
- // PathView has no contentItem property, but ListView does.
- QQuickFlickable *flickable = qobject_cast<QQuickFlickable *>(contentItem);
- return flickable ? flickable->contentItem()->childItems() : contentItem->childItems();
-}
-
namespace {
static inline qreal delegateHeight(const QQuickTumbler *tumbler)
{
return tumbler->availableHeight() / tumbler->visibleItemCount();
}
+}
- enum ContentItemType {
- UnsupportedContentItemType,
- PathViewContentItem,
- ListViewContentItem
- };
-
- static inline QQuickItem *actualContentItem(QQuickItem *rootContentItem, ContentItemType contentType)
- {
- if (contentType == PathViewContentItem)
- return rootContentItem;
- else if (contentType == ListViewContentItem)
- return qobject_cast<QQuickFlickable*>(rootContentItem)->contentItem();
-
- return nullptr;
+/*
+ Finds the contentItem of the view that is a child of the control's \a contentItem.
+ The type is stored in \a type.
+*/
+QQuickItem *QQuickTumblerPrivate::determineViewType(QQuickItem *contentItem)
+{
+ if (contentItem->inherits("QQuickPathView")) {
+ view = contentItem;
+ viewContentItem = contentItem;
+ viewContentItemType = PathViewContentItem;
+ return contentItem;
+ } else if (contentItem->inherits("QQuickListView")) {
+ view = contentItem;
+ viewContentItem = qobject_cast<QQuickFlickable*>(contentItem)->contentItem();
+ viewContentItemType = ListViewContentItem;
+ return contentItem;
+ } else {
+ const auto childItems = contentItem->childItems();
+ for (QQuickItem *childItem : childItems) {
+ QQuickItem *item = determineViewType(childItem);
+ if (item)
+ return item;
+ }
}
- static inline ContentItemType contentItemType(QQuickItem *rootContentItem)
- {
- if (rootContentItem->inherits("QQuickPathView"))
- return PathViewContentItem;
- else if (rootContentItem->inherits("QQuickListView"))
- return ListViewContentItem;
+ resetViewData();
+ return nullptr;
+}
- return UnsupportedContentItemType;
- }
+void QQuickTumblerPrivate::resetViewData()
+{
+ view = nullptr;
+ viewContentItem = nullptr;
+ viewContentItemType = UnsupportedContentItemType;
+}
- static inline ContentItemType contentItemTypeFromDelegate(QQuickItem *delegateItem)
- {
- if (delegateItem->parentItem()->inherits("QQuickPathView")) {
- return PathViewContentItem;
- } else if (delegateItem->parentItem()->parentItem()
- && delegateItem->parentItem()->parentItem()->inherits("QQuickListView")) {
- return ListViewContentItem;
- }
+QList<QQuickItem *> QQuickTumblerPrivate::viewContentItemChildItems() const
+{
+ if (!viewContentItem)
+ return QList<QQuickItem *>();
- return UnsupportedContentItemType;
- }
+ return viewContentItem->childItems();
}
void QQuickTumblerPrivate::_q_updateItemHeights()
@@ -157,7 +181,7 @@ void QQuickTumblerPrivate::_q_updateItemHeights()
// which doesn't affect them, only their getters.
Q_Q(const QQuickTumbler);
const qreal itemHeight = delegateHeight(q);
- const auto items = contentItemChildItems(contentItem);
+ const auto items = viewContentItemChildItems();
for (QQuickItem *childItem : items)
childItem->setHeight(itemHeight);
}
@@ -166,11 +190,25 @@ void QQuickTumblerPrivate::_q_updateItemWidths()
{
Q_Q(const QQuickTumbler);
const qreal availableWidth = q->availableWidth();
- const auto items = contentItemChildItems(contentItem);
+ const auto items = viewContentItemChildItems();
for (QQuickItem *childItem : items)
childItem->setWidth(availableWidth);
}
+void QQuickTumblerPrivate::_q_onViewCurrentIndexChanged()
+{
+ Q_Q(QQuickTumbler);
+ if (!ignoreCurrentIndexChanges) {
+ Q_ASSERT(view);
+ const int oldCurrentIndex = currentIndex;
+
+ currentIndex = view->property("currentIndex").toInt();
+
+ if (oldCurrentIndex != currentIndex)
+ emit q->currentIndexChanged();
+ }
+}
+
void QQuickTumblerPrivate::itemChildAdded(QQuickItem *, QQuickItem *)
{
_q_updateItemWidths();
@@ -196,6 +234,9 @@ QQuickTumbler::QQuickTumbler(QQuickItem *parent) :
QQuickTumbler::~QQuickTumbler()
{
+ Q_D(QQuickTumbler);
+ // Ensure that the item change listener is removed.
+ d->disconnectFromView();
}
/*!
@@ -228,7 +269,7 @@ void QQuickTumbler::setModel(const QVariant &model)
int QQuickTumbler::count() const
{
Q_D(const QQuickTumbler);
- return d->contentItem->property("count").toInt();
+ return d->view ? d->view->property("count").toInt() : 0;
}
/*!
@@ -239,13 +280,27 @@ int QQuickTumbler::count() const
int QQuickTumbler::currentIndex() const
{
Q_D(const QQuickTumbler);
- return d->contentItem ? d->contentItem->property("currentIndex").toInt() : -1;
+ return d->currentIndex;
}
void QQuickTumbler::setCurrentIndex(int currentIndex)
{
Q_D(QQuickTumbler);
- d->contentItem->setProperty("currentIndex", currentIndex);
+ if (currentIndex == d->currentIndex)
+ return;
+
+ d->currentIndex = currentIndex;
+
+ if (d->view) {
+ // The view might not have been created yet, as is the case
+ // if you create a Tumbler component and pass e.g. { currentIndex: 2 }
+ // to createObject().
+ d->ignoreCurrentIndexChanges = true;
+ d->view->setProperty("currentIndex", currentIndex);
+ d->ignoreCurrentIndexChanges = false;
+ }
+
+ emit currentIndexChanged();
}
/*!
@@ -257,7 +312,7 @@ void QQuickTumbler::setCurrentIndex(int currentIndex)
QQuickItem *QQuickTumbler::currentItem() const
{
Q_D(const QQuickTumbler);
- return d->contentItem ? d->contentItem->property("currentItem").value<QQuickItem*>() : nullptr;
+ return d->view ? d->view->property("currentItem").value<QQuickItem*>() : nullptr;
}
/*!
@@ -304,11 +359,61 @@ void QQuickTumbler::setVisibleItemCount(int visibleItemCount)
emit visibleItemCountChanged();
}
+/*!
+ \qmlproperty bool QtQuick.Controls::Tumbler::wrap
+ \since QtQuick.Controls 2.1
+
+ This property determines whether or not the tumbler wraps around when it
+ reaches the top or bottom.
+
+ It is recommended to set this property to \c false when \l count is less than
+ \l visibleItemCount, as it is simpler to interact with a non-wrapping Tumbler
+ when there are only a few items.
+*/
+bool QQuickTumbler::wrap() const
+{
+ Q_D(const QQuickTumbler);
+ return d->wrap;
+}
+
+void QQuickTumbler::setWrap(bool wrap)
+{
+ Q_D(QQuickTumbler);
+ if (isComponentComplete() && wrap == d->wrap)
+ return;
+
+ // Since we use the currentIndex of the contentItem directly, we must
+ // ensure that we keep track of the currentIndex so it doesn't get lost
+ // between view changes.
+ const int oldCurrentIndex = currentIndex();
+
+ d->disconnectFromView();
+
+ d->wrap = wrap;
+
+ // New views will set their currentIndex upon creation, which we'd otherwise
+ // take as the correct one, so we must ignore them.
+ d->ignoreCurrentIndexChanges = true;
+
+ // This will cause the view to be created if our contentItem is a TumblerView.
+ emit wrapChanged();
+
+ d->ignoreCurrentIndexChanges = false;
+
+ // The view should have been created now, so we can start determining its type, etc.
+ // If the delegates use attached properties, this will have already been called,
+ // in which case it will return early. If the delegate doesn't use attached properties,
+ // we need to call it here.
+ d->setupViewData(d->contentItem);
+
+ setCurrentIndex(oldCurrentIndex);
+}
+
QQuickTumblerAttached *QQuickTumbler::qmlAttachedProperties(QObject *object)
{
QQuickItem *delegateItem = qobject_cast<QQuickItem *>(object);
if (!delegateItem) {
- qWarning() << "Tumbler: attached properties of Tumbler must be accessed from within a delegate item";
+ qWarning() << "Tumbler: attached properties of Tumbler must be accessed through a delegate item";
return nullptr;
}
@@ -333,6 +438,16 @@ void QQuickTumbler::componentComplete()
QQuickControl::componentComplete();
d->_q_updateItemHeights();
d->_q_updateItemWidths();
+
+ if (!d->view) {
+ // We don't want to create a PathView or ListView until we're certain
+ // which one we need, and if wrap is not set, it will be the default.
+ // We can only know the final value of wrap when componentComplete() is called,
+ // so, if the view hasn't already been created, we cause it to be created here.
+ emit wrapChanged();
+ // Then, we determine the type of view for attached properties, etc.
+ d->setupViewData(d->contentItem);
+ }
}
void QQuickTumbler::contentItemChange(QQuickItem *newItem, QQuickItem *oldItem)
@@ -341,59 +456,88 @@ void QQuickTumbler::contentItemChange(QQuickItem *newItem, QQuickItem *oldItem)
QQuickControl::contentItemChange(newItem, oldItem);
- // Since we use the currentIndex of the contentItem directly, we must
- // ensure that we keep track of the currentIndex so it doesn't get lost
- // between contentItem changes.
- const int previousCurrentIndex = currentIndex();
-
- if (oldItem) {
- disconnect(oldItem, SIGNAL(currentIndexChanged()), this, SIGNAL(currentIndexChanged()));
- disconnect(oldItem, SIGNAL(currentItemChanged()), this, SIGNAL(currentItemChanged()));
- disconnect(oldItem, SIGNAL(countChanged()), this, SIGNAL(countChanged()));
-
- ContentItemType oldContentItemType = contentItemType(oldItem);
- QQuickItem *actualOldContentItem = actualContentItem(oldItem, oldContentItemType);
- QQuickItemPrivate *actualContentItemPrivate = QQuickItemPrivate::get(actualOldContentItem);
- actualContentItemPrivate->removeItemChangeListener(d, QQuickItemPrivate::Children);
- }
+ if (oldItem)
+ d->disconnectFromView();
if (newItem) {
- ContentItemType contentType = contentItemType(newItem);
- if (contentType == UnsupportedContentItemType) {
- qWarning() << "Tumbler: contentItems other than PathView and ListView are not supported";
- return;
+ // We wait until wrap is set to that we know which type of view to create.
+ // If we try to set up the view too early, we'll issue warnings about it not existing.
+ if (isComponentComplete()) {
+ // Make sure we use the new content item and not the current one, as that won't
+ // be changed until after contentItemChange() has finished.
+ d->setupViewData(newItem);
}
+ }
+}
- connect(newItem, SIGNAL(currentIndexChanged()), this, SIGNAL(currentIndexChanged()));
- connect(newItem, SIGNAL(currentItemChanged()), this, SIGNAL(currentItemChanged()));
- connect(newItem, SIGNAL(countChanged()), this, SIGNAL(countChanged()));
+void QQuickTumblerPrivate::disconnectFromView()
+{
+ Q_Q(QQuickTumbler);
+ if (!view && contentItem && !q->isComponentComplete()) {
+ // If a custom content item is declared, it can happen that
+ // the original contentItem exists without the view etc. having been
+ // determined yet, and then this is called when the custom content item
+ // is eventually set. In all other cases, a view should exist.
+ return;
+ }
- QQuickItem *actualNewContentItem = actualContentItem(newItem, contentType);
- QQuickItemPrivate *actualContentItemPrivate = QQuickItemPrivate::get(actualNewContentItem);
- actualContentItemPrivate->addItemChangeListener(d, QQuickItemPrivate::Children);
+ Q_ASSERT(view);
+ QObject::disconnect(view, SIGNAL(currentIndexChanged()), q, SLOT(_q_onViewCurrentIndexChanged()));
+ QObject::disconnect(view, SIGNAL(currentItemChanged()), q, SIGNAL(currentItemChanged()));
+ QObject::disconnect(view, SIGNAL(countChanged()), q, SIGNAL(countChanged()));
- // If the previous currentIndex is -1, it means we had no contentItem previously.
- if (previousCurrentIndex != -1) {
- // Can't call setCurrentIndex here, as contentItemChange() is
- // called *before* the contentItem is set.
- newItem->setProperty("currentIndex", previousCurrentIndex);
- }
+ QQuickItemPrivate *oldViewContentItemPrivate = QQuickItemPrivate::get(viewContentItem);
+ oldViewContentItemPrivate->removeItemChangeListener(this, QQuickItemPrivate::Children);
+
+ resetViewData();
+}
+
+void QQuickTumblerPrivate::setupViewData(QQuickItem *newControlContentItem)
+{
+ // Don't do anything if we've already set up.
+ if (view)
+ return;
+
+ determineViewType(newControlContentItem);
+
+ if (viewContentItemType == QQuickTumblerPrivate::UnsupportedContentItemType) {
+ qWarning() << "Tumbler: contentItem must contain either a PathView or a ListView";
+ return;
+ }
+
+ Q_Q(QQuickTumbler);
+ QObject::connect(view, SIGNAL(currentIndexChanged()), q, SLOT(_q_onViewCurrentIndexChanged()));
+ QObject::connect(view, SIGNAL(currentItemChanged()), q, SIGNAL(currentItemChanged()));
+ QObject::connect(view, SIGNAL(countChanged()), q, SIGNAL(countChanged()));
+
+ QQuickItemPrivate *viewContentItemPrivate = QQuickItemPrivate::get(viewContentItem);
+ viewContentItemPrivate->addItemChangeListener(this, QQuickItemPrivate::Children);
+
+ const int actualViewIndex = view->property("currentIndex").toInt();
+ if (actualViewIndex != currentIndex) {
+ ignoreCurrentIndexChanges = true;
+ view->setProperty("currentIndex", currentIndex);
+ ignoreCurrentIndexChanges = false;
+
+ // If we still couldn't set the currentIndex, it's probably out of bounds,
+ // in which case we must respect the actual currentIndex.
+ if (view->property("currentIndex").toInt() != currentIndex)
+ q->setCurrentIndex(actualViewIndex);
}
}
void QQuickTumbler::keyPressEvent(QKeyEvent *event)
{
- Q_D(QQuickTumbler);
-
QQuickControl::keyPressEvent(event);
- if (event->isAutoRepeat())
+ Q_D(QQuickTumbler);
+ if (event->isAutoRepeat() || !d->view)
return;
if (event->key() == Qt::Key_Up) {
- QMetaObject::invokeMethod(d->contentItem, "decrementCurrentIndex");
+ QMetaObject::invokeMethod(d->view, "decrementCurrentIndex");
} else if (event->key() == Qt::Key_Down) {
- QMetaObject::invokeMethod(d->contentItem, "incrementCurrentIndex");
+ QMetaObject::invokeMethod(d->view, "incrementCurrentIndex");
}
}
@@ -407,7 +551,7 @@ public:
displacement(0)
{
if (!delegateItem->parentItem()) {
- qWarning() << "Tumbler: attached properties must be accessed from within a delegate item that has a parent";
+ qWarning() << "Tumbler: attached properties must be accessed through a delegate item that has a parent";
return;
}
@@ -418,14 +562,12 @@ public:
}
index = indexContextProperty.toInt();
- const ContentItemType contentItemType = contentItemTypeFromDelegate(delegateItem);
- if (contentItemType == UnsupportedContentItemType)
- return;
- // ListView has an "additional" content item.
- tumbler = qobject_cast<QQuickTumbler* >(contentItemType == PathViewContentItem
- ? delegateItem->parentItem()->parentItem() : delegateItem->parentItem()->parentItem()->parentItem());
- Q_ASSERT(tumbler);
+ QQuickItem *parentItem = delegateItem;
+ while ((parentItem = parentItem->parentItem())) {
+ if ((tumbler = qobject_cast<QQuickTumbler*>(parentItem)))
+ break;
+ }
}
~QQuickTumblerAttachedPrivate() {
@@ -438,7 +580,7 @@ public:
void _q_calculateDisplacement();
// The Tumbler that contains the delegate. Required to calculated the displacement.
- QQuickTumbler *tumbler;
+ QPointer<QQuickTumbler> tumbler;
// The index of the delegate. Used to calculate the displacement.
int index;
// The displacement for our delegate.
@@ -476,32 +618,35 @@ void QQuickTumblerAttachedPrivate::_q_calculateDisplacement()
const int previousDisplacement = displacement;
displacement = 0;
- const int count = tumbler->count();
- // This can happen in tests, so it may happen in normal usage too.
- if (count == 0)
+ // Can happen if the attached properties are accessed on the wrong type of item or the tumbler was destroyed.
+ if (!tumbler)
return;
- ContentItemType contentType = contentItemType(tumbler->contentItem());
- if (contentType == UnsupportedContentItemType)
+ // Can happen if there is no ListView or PathView within the contentItem.
+ QQuickTumblerPrivate *tumblerPrivate = QQuickTumblerPrivate::get(tumbler);
+ if (!tumblerPrivate->viewContentItem)
return;
- qreal offset = 0;
+ const int count = tumbler->count();
+ // This can happen in tests, so it may happen in normal usage too.
+ if (count == 0)
+ return;
- if (contentType == PathViewContentItem) {
- offset = tumbler->contentItem()->property("offset").toReal();
+ if (tumblerPrivate->viewContentItemType == QQuickTumblerPrivate::PathViewContentItem) {
+ const qreal offset = tumblerPrivate->view->property("offset").toReal();
displacement = count > 1 ? count - index - offset : 0;
// Don't add 1 if count <= visibleItemCount
const int visibleItems = tumbler->visibleItemCount();
- int halfVisibleItems = visibleItems / 2 + (visibleItems < count ? 1 : 0);
+ const int halfVisibleItems = visibleItems / 2 + (visibleItems < count ? 1 : 0);
if (displacement > halfVisibleItems)
displacement -= count;
else if (displacement < -halfVisibleItems)
displacement += count;
} else {
- const qreal contentY = tumbler->contentItem()->property("contentY").toReal();
+ const qreal contentY = tumblerPrivate->view->property("contentY").toReal();
const qreal delegateH = delegateHeight(tumbler);
- const qreal preferredHighlightBegin = tumbler->contentItem()->property("preferredHighlightBegin").toReal();
+ const qreal preferredHighlightBegin = tumblerPrivate->view->property("preferredHighlightBegin").toReal();
// Tumbler's displacement goes from negative at the top to positive towards the bottom, so we must switch this around.
const qreal reverseDisplacement = (contentY + preferredHighlightBegin) / delegateH;
displacement = reverseDisplacement - index;
@@ -517,14 +662,23 @@ QQuickTumblerAttached::QQuickTumblerAttached(QQuickItem *delegateItem) :
{
Q_D(QQuickTumblerAttached);
if (d->tumbler) {
- QQuickItem *rootContentItem = d->tumbler->contentItem();
- const ContentItemType contentType = contentItemType(rootContentItem);
- QQuickItemPrivate *p = QQuickItemPrivate::get(actualContentItem(rootContentItem, contentType));
+ // When the Tumbler is completed, wrapChanged() is emitted to let QQuickTumblerView
+ // know that it can create the view. The view itself might instantiate delegates
+ // that use attached properties. At this point, setupViewData() hasn't been called yet
+ // (it's called on the next line in componentComplete()), so we call it here so that
+ // we have access to the view.
+ QQuickTumblerPrivate *tumblerPrivate = QQuickTumblerPrivate::get(d->tumbler);
+ tumblerPrivate->setupViewData(tumblerPrivate->contentItem);
+
+ if (!tumblerPrivate->viewContentItem)
+ return;
+
+ QQuickItemPrivate *p = QQuickItemPrivate::get(tumblerPrivate->viewContentItem);
p->addItemChangeListener(d, QQuickItemPrivate::Geometry | QQuickItemPrivate::Children);
- const char *contentItemSignal = contentType == PathViewContentItem
+ const char *contentItemSignal = tumblerPrivate->viewContentItemType == QQuickTumblerPrivate::PathViewContentItem
? SIGNAL(offsetChanged()) : SIGNAL(contentYChanged());
- connect(d->tumbler->contentItem(), contentItemSignal, this, SLOT(_q_calculateDisplacement()));
+ connect(tumblerPrivate->view, contentItemSignal, this, SLOT(_q_calculateDisplacement()));
d->_q_calculateDisplacement();
}
diff --git a/src/quicktemplates2/qquicktumbler_p.h b/src/quicktemplates2/qquicktumbler_p.h
index e28102e4..d129fe33 100644
--- a/src/quicktemplates2/qquicktumbler_p.h
+++ b/src/quicktemplates2/qquicktumbler_p.h
@@ -66,6 +66,7 @@ class Q_QUICKTEMPLATES2_PRIVATE_EXPORT QQuickTumbler : public QQuickControl
Q_PROPERTY(QQuickItem *currentItem READ currentItem NOTIFY currentItemChanged FINAL)
Q_PROPERTY(QQmlComponent *delegate READ delegate WRITE setDelegate NOTIFY delegateChanged FINAL)
Q_PROPERTY(int visibleItemCount READ visibleItemCount WRITE setVisibleItemCount NOTIFY visibleItemCountChanged FINAL)
+ Q_PROPERTY(bool wrap READ wrap WRITE setWrap NOTIFY wrapChanged FINAL REVISION 1)
public:
explicit QQuickTumbler(QQuickItem *parent = nullptr);
@@ -86,6 +87,9 @@ public:
int visibleItemCount() const;
void setVisibleItemCount(int visibleItemCount);
+ bool wrap() const;
+ void setWrap(bool wrap);
+
static QQuickTumblerAttached *qmlAttachedProperties(QObject *object);
Q_SIGNALS:
@@ -95,6 +99,7 @@ Q_SIGNALS:
void currentItemChanged();
void delegateChanged();
void visibleItemCountChanged();
+ Q_REVISION(1) void wrapChanged();
protected:
void geometryChanged(const QRectF &newGeometry, const QRectF &oldGeometry) override;
@@ -108,6 +113,7 @@ private:
Q_PRIVATE_SLOT(d_func(), void _q_updateItemWidths())
Q_PRIVATE_SLOT(d_func(), void _q_updateItemHeights())
+ Q_PRIVATE_SLOT(d_func(), void _q_onViewCurrentIndexChanged())
};
class QQuickTumblerAttachedPrivate;
diff --git a/tests/auto/controls/data/TumblerDatePicker.qml b/tests/auto/controls/data/TumblerDatePicker.qml
index 18d7fb97..d78d8c65 100644
--- a/tests/auto/controls/data/TumblerDatePicker.qml
+++ b/tests/auto/controls/data/TumblerDatePicker.qml
@@ -1,6 +1,6 @@
/****************************************************************************
**
-** Copyright (C) 2015 The Qt Company Ltd.
+** Copyright (C) 2016 The Qt Company Ltd.
** Contact: http://www.qt.io/licensing/
**
** This file is part of the test suite of the Qt Toolkit.
diff --git a/tests/auto/controls/data/TumblerListView.qml b/tests/auto/controls/data/TumblerListView.qml
new file mode 100644
index 00000000..cccf8adf
--- /dev/null
+++ b/tests/auto/controls/data/TumblerListView.qml
@@ -0,0 +1,53 @@
+/****************************************************************************
+**
+** Copyright (C) 2016 The Qt Company Ltd.
+** Contact: http://www.qt.io/licensing/
+**
+** This file is part of the test suite 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 The Qt Company Ltd nor the names of its
+** contributors may be used to endorse or promote products derived
+** from this software without specific prior written permission.
+**
+**
+** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE."
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+import QtQuick 2.6
+
+ListView {
+ anchors.fill: parent
+ model: parent.model
+ delegate: parent.delegate
+
+ snapMode: ListView.SnapToItem
+ highlightRangeMode: ListView.StrictlyEnforceRange
+ preferredHighlightBegin: height / 2 - (height / parent.visibleItemCount / 2)
+ preferredHighlightEnd: height / 2 + (height / parent.visibleItemCount / 2)
+ clip: true
+}
diff --git a/tests/auto/controls/data/TumblerPathView.qml b/tests/auto/controls/data/TumblerPathView.qml
new file mode 100644
index 00000000..e325faf6
--- /dev/null
+++ b/tests/auto/controls/data/TumblerPathView.qml
@@ -0,0 +1,63 @@
+/****************************************************************************
+**
+** Copyright (C) 2015 The Qt Company Ltd.
+** Contact: http://www.qt.io/licensing/
+**
+** This file is part of the test suite 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 The Qt Company Ltd nor the names of its
+** contributors may be used to endorse or promote products derived
+** from this software without specific prior written permission.
+**
+**
+** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE."
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+import QtQuick 2.6
+
+PathView {
+ id: pathView
+ model: parent.model
+ delegate: parent.delegate
+ clip: true
+ pathItemCount: parent.visibleItemCount + 1
+ preferredHighlightBegin: 0.5
+ preferredHighlightEnd: 0.5
+ dragMargin: width / 2
+
+ path: Path {
+ startX: pathView.width / 2
+ startY: -pathView.delegateHeight / 2
+ PathLine {
+ x: pathView.width / 2
+ y: pathView.pathItemCount * pathView.delegateHeight - pathView.delegateHeight / 2
+ }
+ }
+
+ property real delegateHeight: parent.availableHeight / parent.visibleItemCount
+}
diff --git a/tests/auto/controls/data/tst_tumbler.qml b/tests/auto/controls/data/tst_tumbler.qml
index 2de52924..98128f9d 100644
--- a/tests/auto/controls/data/tst_tumbler.qml
+++ b/tests/auto/controls/data/tst_tumbler.qml
@@ -55,15 +55,32 @@ TestCase {
readonly property real implicitTumblerHeight: 200
readonly property real defaultImplicitDelegateHeight: implicitTumblerHeight / 3
readonly property real defaultListViewTumblerOffset: -defaultImplicitDelegateHeight
+ readonly property real tumblerDelegateHeight: tumbler ? tumbler.availableHeight / tumbler.visibleItemCount : 0
+ property Item tumblerView: null
+
+ Component {
+ id: tumblerComponent
+
+ Tumbler {}
+ }
function init() {
- tumbler = Qt.createQmlObject("import QtQuick.Controls 2.0; Tumbler { }", testCase, "");
- verify(tumbler, "Tumbler: failed to create an instance");
- compare(tumbler.contentItem.parent, tumbler);
+ createTumbler();
}
function cleanup() {
tumbler.destroy();
+ tumblerView = null;
+ }
+
+ function createTumbler(args) {
+ if (args === undefined)
+ tumbler = tumblerComponent.createObject(testCase);
+ else
+ tumbler = tumblerComponent.createObject(testCase, args);
+ verify(tumbler, "Tumbler: failed to create an instance");
+ tumblerView = findView(tumbler);
+ verify(tumblerView);
}
function tumblerXCenter() {
@@ -76,24 +93,45 @@ TestCase {
// visualItemIndex is from 0 to the amount of visible items.
function itemCenterPos(visualItemIndex) {
- var halfDelegateHeight = tumbler.contentItem.delegateHeight / 2;
+ var halfDelegateHeight = tumblerDelegateHeight / 2;
var yCenter = tumbler.y + tumbler.topPadding + halfDelegateHeight
- + (tumbler.contentItem.delegateHeight * visualItemIndex);
+ + (tumblerDelegateHeight * visualItemIndex);
return Qt.point(tumblerXCenter(), yCenter);
}
function checkItemSizes() {
- var contentChildren = tumbler.contentItem.hasOwnProperty("contentItem")
- ? tumbler.contentItem.contentItem.children : tumbler.contentItem.children;
+ var contentChildren = tumbler.wrap ? tumblerView.children : tumblerView.contentItem.children;
verify(contentChildren.length >= tumbler.count);
for (var i = 0; i < contentChildren.length; ++i) {
- compare(contentChildren[i].width, tumbler.width);
- compare(contentChildren[i].height, tumbler.contentItem.delegateHeight);
+ compare(contentChildren[i].width, tumbler.availableWidth);
+ compare(contentChildren[i].height, tumblerDelegateHeight);
}
}
- function tst_dynamicContentItemChange() {
- // test that currentIndex is maintained between contentItem changes...
+ function findView(parent) {
+ for (var i = 0; i < parent.children.length; ++i) {
+ var child = parent.children[i];
+ if (child.hasOwnProperty("currentIndex")) {
+ return child;
+ }
+
+ return findView(child);
+ }
+
+ return null;
+ }
+
+ property Component noAttachedPropertiesDelegate: Text {
+ text: modelData
+ }
+
+ function test_wrapWithoutAttachedProperties() {
+ verify(tumbler.wrap);
+
+ tumbler.delegate = noAttachedPropertiesDelegate;
+ // Shouldn't assert.
+ tumbler.wrap = false;
+ verify(findView(tumbler));
}
function test_currentIndex() {
@@ -104,14 +142,15 @@ TestCase {
// Set it through user interaction.
var pos = Qt.point(tumblerXCenter(), tumbler.height / 2);
- mouseDrag(tumbler, pos.x, pos.y, 0, -tumbler.contentItem.delegateHeight / 2, Qt.LeftButton, Qt.NoModifier, 200);
- compare(tumbler.currentIndex, 1);
- compare(tumbler.contentItem.currentIndex, 1);
+ mouseDrag(tumbler, pos.x, pos.y, 0, tumbler.height / 3, Qt.LeftButton, Qt.NoModifier, 200);
+ tryCompare(tumblerView, "offset", 1);
+ compare(tumbler.currentIndex, 4);
+ compare(tumblerView.currentIndex, 4);
// Set it manually.
tumbler.currentIndex = 2;
tryCompare(tumbler, "currentIndex", 2);
- compare(tumbler.contentItem.currentIndex, 2);
+ compare(tumblerView.currentIndex, 2);
// PathView has 0 as its currentIndex in this case for some reason.
tumbler.model = null;
@@ -121,52 +160,116 @@ TestCase {
tryCompare(tumbler, "currentIndex", 0);
}
+ Component {
+ id: currentIndexTumbler
+
+ Tumbler {
+ model: 5
+ currentIndex: 2
+ }
+ }
+
+ Component {
+ id: currentIndexTumblerNoWrap
+
+ Tumbler {
+ model: 5
+ currentIndex: 2
+ wrap: false
+ }
+ }
+
+ Component {
+ id: currentIndexTumblerNoWrapReversedOrder
+
+ Tumbler {
+ model: 5
+ wrap: false
+ currentIndex: 2
+ }
+ }
+
+ function test_currentIndexAtCreation_data() {
+ return [
+ { tag: "wrap: true", currentIndex: 2, wrap: true, component: currentIndexTumbler },
+ { tag: "wrap: false", currentIndex: 2, wrap: false, component: currentIndexTumblerNoWrap },
+ // Order of property assignments shouldn't matter
+ { tag: "wrap: false, reversed property assignment order", currentIndex: 2, wrap: false, component: currentIndexTumblerNoWrapReversedOrder }
+ ]
+ }
+
+ function test_currentIndexAtCreation(data) {
+ // Test setting currentIndex at creation time
+ var tumbler = data.component.createObject(testCase);
+ verify(tumbler);
+ compare(tumbler.currentIndex, data.currentIndex);
+
+ tumblerView = findView(tumbler);
+ // TODO: replace once QTBUG-19708 is fixed.
+ for (var delay = 1000; delay >= 0; delay -= 50) {
+ if (tumblerView.currentItem)
+ break;
+ wait(50);
+ }
+ verify(tumblerView.currentItem);
+ compare(tumblerView.currentIndex, data.currentIndex);
+ compare(tumblerView.currentItem.text, data.currentIndex.toString());
+
+ if (data.wrap) {
+ compare(tumblerView.offset, tumblerView.count - data.currentIndex);
+ } else {
+ compare(tumblerView.contentY, tumblerDelegateHeight * data.currentIndex - tumblerView.preferredHighlightBegin);
+ }
+
+ tumbler.destroy();
+ }
+
function test_keyboardNavigation() {
tumbler.model = 5;
tumbler.forceActiveFocus();
- tumbler.contentItem.highlightMoveDuration = 0;
+ tumblerView.highlightMoveDuration = 0;
// Navigate upwards through entire wheel.
for (var j = 0; j < tumbler.count - 1; ++j) {
keyClick(Qt.Key_Up, Qt.NoModifier);
- tryCompare(tumbler.contentItem, "offset", j + 1);
+ tryCompare(tumblerView, "offset", j + 1);
compare(tumbler.currentIndex, tumbler.count - 1 - j);
}
keyClick(Qt.Key_Up, Qt.NoModifier);
- tryCompare(tumbler.contentItem, "offset", 0);
+ tryCompare(tumblerView, "offset", 0);
compare(tumbler.currentIndex, 0);
// Navigate downwards through entire wheel.
for (j = 0; j < tumbler.count - 1; ++j) {
keyClick(Qt.Key_Down, Qt.NoModifier);
- tryCompare(tumbler.contentItem, "offset", tumbler.count - 1 - j);
+ tryCompare(tumblerView, "offset", tumbler.count - 1 - j);
compare(tumbler.currentIndex, j + 1);
}
keyClick(Qt.Key_Down, Qt.NoModifier);
- tryCompare(tumbler.contentItem, "offset", 0);
+ tryCompare(tumblerView, "offset", 0);
compare(tumbler.currentIndex, 0);
}
function test_itemsCorrectlyPositioned() {
tumbler.model = 4;
tumbler.height = 120;
- compare(tumbler.contentItem.delegateHeight, 40);
+ compare(tumblerDelegateHeight, 40);
checkItemSizes();
- wait(tumbler.contentItem.highlightMoveDuration);
+ wait(tumblerView.highlightMoveDuration);
var firstItemCenterPos = itemCenterPos(1);
- var firstItem = tumbler.contentItem.itemAt(firstItemCenterPos.x, firstItemCenterPos.y);
+ var firstItem = tumblerView.itemAt(firstItemCenterPos.x, firstItemCenterPos.y);
var actualPos = testCase.mapFromItem(firstItem, 0, 0);
compare(actualPos.x, tumbler.leftPadding);
compare(actualPos.y, tumbler.topPadding + 40);
tumbler.forceActiveFocus();
keyClick(Qt.Key_Down);
- tryCompare(tumbler.contentItem, "offset", 3.0);
+ tryCompare(tumblerView, "offset", 3.0);
firstItemCenterPos = itemCenterPos(0);
- firstItem = tumbler.contentItem.itemAt(firstItemCenterPos.x, firstItemCenterPos.y);
+ firstItem = tumblerView.itemAt(firstItemCenterPos.x, firstItemCenterPos.y);
verify(firstItem);
// Test QTBUG-40298.
actualPos = testCase.mapFromItem(firstItem, 0, 0);
@@ -174,12 +277,12 @@ TestCase {
compare(actualPos.y, tumbler.topPadding);
var secondItemCenterPos = itemCenterPos(1);
- var secondItem = tumbler.contentItem.itemAt(secondItemCenterPos.x, secondItemCenterPos.y);
+ var secondItem = tumblerView.itemAt(secondItemCenterPos.x, secondItemCenterPos.y);
verify(secondItem);
verify(firstItem.y < secondItem.y);
var thirdItemCenterPos = itemCenterPos(2);
- var thirdItem = tumbler.contentItem.itemAt(thirdItemCenterPos.x, thirdItemCenterPos.y);
+ var thirdItem = tumblerView.itemAt(thirdItemCenterPos.x, thirdItemCenterPos.y);
verify(thirdItem);
verify(firstItem.y < thirdItem.y);
verify(secondItem.y < thirdItem.y);
@@ -214,11 +317,11 @@ TestCase {
compare(tumbler.yearTumbler.currentIndex, 0);
compare(tumbler.yearTumbler.count, 100);
- verify(tumbler.dayTumbler.contentItem.children.length >= tumbler.dayTumbler.visibleItemCount);
- verify(tumbler.monthTumbler.contentItem.children.length >= tumbler.monthTumbler.visibleItemCount);
+ verify(findView(tumbler.dayTumbler).children.length >= tumbler.dayTumbler.visibleItemCount);
+ verify(findView(tumbler.monthTumbler).children.length >= tumbler.monthTumbler.visibleItemCount);
// TODO: do this properly somehow
wait(100);
- verify(tumbler.yearTumbler.contentItem.children.length >= tumbler.yearTumbler.visibleItemCount);
+ verify(findView(tumbler.yearTumbler).children.length >= tumbler.yearTumbler.visibleItemCount);
// March.
tumbler.monthTumbler.currentIndex = 2;
@@ -295,18 +398,32 @@ TestCase {
tumbler.model = data.count;
compare(tumbler.count, data.count);
- var delegate = findChild(tumbler.contentItem, "delegate" + data.index);
+ var delegate = findChild(tumblerView, "delegate" + data.index);
verify(delegate);
- tumbler.contentItem.offset = data.offset;
+ tumblerView.offset = data.offset;
compare(delegate.displacement, data.expectedDisplacement);
// test displacement after adding and removing items
}
+ function test_wrap() {
+ tumbler.model = 5;
+ compare(tumbler.count, 5);
+
+ tumbler.currentIndex = 2;
+ compare(tumblerView.currentIndex, 2);
+
+ tumbler.wrap = false;
+ tumblerView = findView(tumbler);
+ compare(tumbler.count, 5);
+ compare(tumbler.currentIndex, 2);
+ compare(tumblerView.currentIndex, 2);
+ }
+
Component {
- id: listViewTumblerComponent
- //! [contentItem]
+ id: customListViewTumblerComponent
+
Tumbler {
id: listViewTumbler
@@ -322,7 +439,95 @@ TestCase {
clip: true
}
}
- //! [contentItem]
+ }
+
+ Component {
+ id: customPathViewTumblerComponent
+
+ Tumbler {
+ id: pathViewTumbler
+
+ contentItem: PathView {
+ id: pathView
+ model: pathViewTumbler.model
+ delegate: pathViewTumbler.delegate
+ clip: true
+ pathItemCount: pathViewTumbler.visibleItemCount + 1
+ preferredHighlightBegin: 0.5
+ preferredHighlightEnd: 0.5
+ dragMargin: width / 2
+
+ path: Path {
+ startX: pathView.width / 2
+ startY: -pathView.delegateHeight / 2
+ PathLine {
+ x: pathView.width / 2
+ y: pathView.pathItemCount * pathView.delegateHeight - pathView.delegateHeight / 2
+ }
+ }
+
+ property real delegateHeight: pathViewTumbler.availableHeight / pathViewTumbler.visibleItemCount
+ }
+ }
+ }
+
+ function test_customContentItemAtConstruction_data() {
+ return [
+ { tag: "ListView", component: customListViewTumblerComponent },
+ { tag: "PathView", component: customPathViewTumblerComponent }
+ ];
+ }
+
+ function test_customContentItemAtConstruction(data) {
+ var tumbler = data.component.createObject(testCase);
+ // Shouldn't assert.
+
+ tumbler.model = 5;
+ compare(tumbler.count, 5);
+
+ tumbler.currentIndex = 2;
+ var tumblerView = findView(tumbler);
+ compare(tumblerView.currentIndex, 2);
+
+ tumblerView.incrementCurrentIndex();
+ compare(tumblerView.currentIndex, 3);
+ compare(tumbler.currentIndex, 3);
+
+ // Shouldn't have any affect.
+ tumbler.wrap = false;
+ compare(tumbler.count, 5);
+ compare(tumblerView.currentIndex, 3);
+ compare(tumbler.currentIndex, 3);
+
+ tumbler.destroy();
+ }
+
+ function test_customContentItemAfterConstruction_data() {
+ return [
+ { tag: "ListView", componentPath: "TumblerListView.qml" },
+ { tag: "PathView", componentPath: "TumblerPathView.qml" }
+ ];
+ }
+
+ function test_customContentItemAfterConstruction(data) {
+ tumbler.model = 5;
+ compare(tumbler.count, 5);
+
+ tumbler.currentIndex = 2;
+ compare(tumblerView.currentIndex, 2);
+
+ var contentItemComponent = Qt.createComponent(data.componentPath);
+ compare(contentItemComponent.status, Component.Ready);
+
+ var customContentItem = contentItemComponent.createObject(tumbler);
+ tumbler.contentItem = customContentItem;
+ compare(tumbler.count, 5);
+ tumblerView = findView(tumbler);
+ compare(tumblerView.currentIndex, 2);
+
+ tumblerView.incrementCurrentIndex();
+ compare(tumblerView.currentIndex, 3);
+ compare(tumbler.currentIndex, 3);
}
function test_displacementListView_data() {
@@ -363,21 +568,16 @@ TestCase {
}
function test_displacementListView(data) {
- tumbler.destroy();
- // Sanity check that they're aren't any children at this stage.
- tryCompare(testCase.children, "length", 0);
-
- tumbler = listViewTumblerComponent.createObject(testCase);
- verify(tumbler);
-
+ tumbler.wrap = false;
tumbler.delegate = displacementDelegate;
tumbler.model = 5;
compare(tumbler.count, 5);
// Ensure assumptions about the tumbler used in our data() function are correct.
- compare(tumbler.contentItem.contentY, -defaultImplicitDelegateHeight);
+ tumblerView = findView(tumbler);
+ compare(tumblerView.contentY, -defaultImplicitDelegateHeight);
var delegateCount = 0;
- var listView = tumbler.contentItem;
- var listViewContentItem = tumbler.contentItem.contentItem;
+ var listView = tumblerView;
+ var listViewContentItem = tumblerView.contentItem;
// We use the mouse instead of setting contentY directly, otherwise the
// items snap back into place. This doesn't seem to be an issue for
@@ -436,18 +636,15 @@ TestCase {
}
function test_listViewFlickAboveBounds(data) {
- tumbler.destroy();
-
- tumbler = listViewTumblerComponent.createObject(testCase);
- verify(tumbler);
-
+ tumbler.wrap = false;
tumbler.delegate = displacementDelegate;
tumbler.model = data.model;
+ tumblerView = findView(tumbler);
mousePress(tumbler, tumblerXCenter(), tumblerYCenter());
// Ensure it's stationary.
- var listView = tumbler.contentItem;
+ var listView = tumblerView;
compare(listView.contentY, defaultListViewTumblerOffset);
// We could just move up until the contentY changed, but this is safer.
@@ -512,9 +709,9 @@ TestCase {
for (var delegateIndex = 0; delegateIndex < data.visibleItemCount; ++delegateIndex) {
if (data.expectedYPositions.hasOwnProperty(delegateIndex)) {
- var delegate = findChild(tumbler.contentItem, "delegate" + delegateIndex);
+ var delegate = findChild(tumblerView, "delegate" + delegateIndex);
verify(delegate, "Delegate found at index " + delegateIndex);
- var expectedYPos = data.expectedYPositions[delegateIndex] * tumbler.contentItem.delegateHeight;
+ var expectedYPos = data.expectedYPositions[delegateIndex] * tumblerDelegateHeight;
compare(delegate.mapToItem(tumbler.contentItem, 0, 0).y, expectedYPos);
}
}
@@ -528,12 +725,6 @@ TestCase {
property real displacement: Tumbler.displacement
}
- property Component gridViewComponent: GridView {}
- property Component simpleDisplacementDelegate: Text {
- property real displacement: Tumbler.displacement
- property int index: -1
- }
-
function test_attachedProperties() {
// TODO: crashes somewhere in QML's guts
// tumbler.model = 5;
@@ -542,18 +733,12 @@ TestCase {
// // Cause displacement to be changed. The warning isn't triggered if we don't do this.
// tumbler.contentItem.offset += 1;
- ignoreWarning("Tumbler: attached properties must be accessed from within a delegate item that has a parent");
+ ignoreWarning("Tumbler: attached properties must be accessed through a delegate item that has a parent");
noParentDelegateComponent.createObject(null);
ignoreWarning("Tumbler: attempting to access attached property on item without an \"index\" property");
var object = noParentDelegateComponent.createObject(testCase);
object.destroy();
-
- // Should not be any warnings from this, as ListView, for example, doesn't produce warnings for the same code.
- var gridView = gridViewComponent.createObject(testCase);
- object = simpleDisplacementDelegate.createObject(gridView);
- object.destroy();
- gridView.destroy();
}
property Component paddingDelegate: Text {
@@ -650,8 +835,8 @@ TestCase {
}
// Force new items to be created, as there was a bug where the path was correct until this happened.
- compare(tumbler.contentItem.offset, 0);
+ compare(tumblerView.offset, 0);
++tumbler.currentIndex;
- tryCompare(tumbler.contentItem, "offset", 4, tumbler.contentItem.highlightMoveDuration * 2);
+ tryCompare(tumblerView, "offset", 4, tumblerView.highlightMoveDuration * 2);
}
}
diff --git a/tests/manual/gifs/data/qtquickcontrols2-tumbler-wrap.qml b/tests/manual/gifs/data/qtquickcontrols2-tumbler-wrap.qml
index e1139177..abfa7bca 100644
--- a/tests/manual/gifs/data/qtquickcontrols2-tumbler-wrap.qml
+++ b/tests/manual/gifs/data/qtquickcontrols2-tumbler-wrap.qml
@@ -1,6 +1,6 @@
/****************************************************************************
**
-** Copyright (C) 2015 The Qt Company Ltd.
+** Copyright (C) 2016 The Qt Company Ltd.
** Contact: http://www.qt.io/licensing/
**
** This file is part of the test suite of the Qt Toolkit.
@@ -38,82 +38,59 @@
**
****************************************************************************/
-import QtQuick 2.6
-import QtQuick.Window 2.0
-import QtQuick.Controls 2.0
+import QtQuick 2.8
+import QtQuick.Window 2.2
+import QtQuick.Controls 2.1
Window {
- width: 200
- height: 200
+ width: frame.implicitWidth + 10
+ height: frame.implicitHeight + 10
visible: true
- Frame {
- padding: 0
- anchors.centerIn: parent
- width: row.implicitWidth
- height: row.implicitHeight
-
- FontMetrics {
- id: fontMetrics
- }
+ function formatText(count, modelData) {
+ var data = count === 12 ? modelData + 1 : modelData;
+ return data.toString().length < 2 ? "0" + data : data;
+ }
- Component {
- id: delegateComponent
+ Component {
+ id: delegateComponent
- Text {
- text: modelData.toString().length < 2 ? "0" + modelData : modelData
- color: "#666666"
- opacity: 0.4 + Math.max(0, 1 - Math.abs(Tumbler.displacement)) * 0.6
- horizontalAlignment: Text.AlignHCenter
- verticalAlignment: Text.AlignVCenter
- font.pixelSize: fontMetrics.font.pixelSize * 1.25
- }
+ Label {
+ text: formatText(Tumbler.tumbler.count, modelData)
+ opacity: 1.0 - Math.abs(Tumbler.displacement) / (Tumbler.tumbler.visibleItemCount / 2)
+ horizontalAlignment: Text.AlignHCenter
+ verticalAlignment: Text.AlignVCenter
}
+ }
+
+ Frame {
+ id: frame
+ padding: 0
+ anchors.centerIn: parent
Row {
id: row
Tumbler {
id: hoursTumbler
+ visibleItemCount: 5
model: 12
- delegate: Text {
- text: (modelData.toString().length < 2 ? "0" : "") + (modelData + 1)
- color: "#666666"
- opacity: 0.4 + Math.max(0, 1 - Math.abs(Tumbler.displacement)) * 0.6
- horizontalAlignment: Text.AlignHCenter
- verticalAlignment: Text.AlignVCenter
- font.pixelSize: fontMetrics.font.pixelSize * 1.25
- }
- width: 50
- height: 150
+ delegate: delegateComponent
}
Tumbler {
id: minutesTumbler
- currentIndex: 6
+ visibleItemCount: 5
model: 60
delegate: delegateComponent
- width: 50
- height: 150
}
Tumbler {
id: amPmTumbler
+ visibleItemCount: 5
+ wrap: false
model: ["AM", "PM"]
delegate: delegateComponent
- width: 50
- height: 150
- contentItem: ListView {
- anchors.fill: parent
- model: amPmTumbler.model
- delegate: amPmTumbler.delegate
-
- snapMode: ListView.SnapToItem
- highlightRangeMode: ListView.StrictlyEnforceRange
- preferredHighlightBegin: height / 2 - (height / 3 / 2)
- preferredHighlightEnd: height / 2 + (height / 3 / 2)
- clip: true
- }
}
}
}
diff --git a/tests/manual/gifs/tst_gifs.cpp b/tests/manual/gifs/tst_gifs.cpp
index 078813e1..f77163b9 100644
--- a/tests/manual/gifs/tst_gifs.cpp
+++ b/tests/manual/gifs/tst_gifs.cpp
@@ -153,37 +153,40 @@ void tst_Gifs::tumblerWrap()
QTest::mouseMove(window, QPoint(105, 158), 10);
QTest::mouseMove(window, QPoint(105, 162), 13);
QTest::mouseRelease(window, Qt::LeftButton, Qt::NoModifier, QPoint(105, 162), 0);
- QTest::mousePress(window, Qt::LeftButton, Qt::NoModifier, QPoint(154, 130), 1098);
- QTest::mouseMove(window, QPoint(154, 129), 50);
- QTest::mouseMove(window, QPoint(153, 128), 0);
- QTest::mouseMove(window, QPoint(153, 125), 16);
- QTest::mouseMove(window, QPoint(152, 121), 0);
- QTest::mouseMove(window, QPoint(152, 117), 17);
- QTest::mouseMove(window, QPoint(151, 113), 0);
- QTest::mouseMove(window, QPoint(151, 106), 16);
- QTest::mouseMove(window, QPoint(150, 99), 1);
- QTest::mouseMove(window, QPoint(148, 93), 16);
- QTest::mouseMove(window, QPoint(148, 88), 0);
- QTest::mouseMove(window, QPoint(148, 84), 17);
- QTest::mouseMove(window, QPoint(147, 81), 0);
- QTest::mouseRelease(window, Qt::LeftButton, Qt::NoModifier, QPoint(147, 81), 0);
- QTest::mousePress(window, Qt::LeftButton, Qt::NoModifier, QPoint(147, 74), 550);
- QTest::mouseMove(window, QPoint(147, 75), 17);
- QTest::mouseMove(window, QPoint(147, 76), 17);
- QTest::mouseMove(window, QPoint(147, 80), 0);
- QTest::mouseMove(window, QPoint(148, 85), 16);
- QTest::mouseMove(window, QPoint(148, 92), 0);
- QTest::mouseMove(window, QPoint(148, 103), 17);
- QTest::mouseMove(window, QPoint(150, 119), 17);
- QTest::mouseMove(window, QPoint(151, 138), 16);
- QTest::mouseMove(window, QPoint(151, 145), 1);
- QTest::mouseMove(window, QPoint(153, 151), 16);
- QTest::mouseMove(window, QPoint(153, 157), 0);
- QTest::mouseMove(window, QPoint(153, 163), 17);
- QTest::mouseMove(window, QPoint(153, 167), 0);
- QTest::mouseMove(window, QPoint(155, 171), 17);
- QTest::mouseMove(window, QPoint(155, 175), 0);
- QTest::mouseRelease(window, Qt::LeftButton, Qt::NoModifier, QPoint(155, 175), 0);
+ QTest::mousePress(window, Qt::LeftButton, Qt::NoModifier, QPoint(154, 100), 1098);
+ QTest::mouseMove(window, QPoint(154, 99), 16);
+ QTest::mouseMove(window, QPoint(153, 98), 16);
+ QTest::mouseMove(window, QPoint(153, 95), 16);
+ QTest::mouseMove(window, QPoint(152, 91), 15);
+ QTest::mouseMove(window, QPoint(152, 87), 14);
+ QTest::mouseMove(window, QPoint(151, 83), 13);
+ QTest::mouseMove(window, QPoint(151, 86), 13);
+ QTest::mouseMove(window, QPoint(150, 79), 12);
+ QTest::mouseMove(window, QPoint(148, 73), 12);
+ QTest::mouseMove(window, QPoint(148, 68), 12);
+ QTest::mouseMove(window, QPoint(148, 60), 10);
+ QTest::mouseMove(window, QPoint(147, 50), 10);
+ QTest::mouseMove(window, QPoint(147, 40), 9);
+ QTest::mouseMove(window, QPoint(147, 30), 8);
+ QTest::mouseMove(window, QPoint(147, 20), 7);
+ QTest::mouseRelease(window, Qt::LeftButton, Qt::NoModifier, QPoint(147, 20), 0);
+ QTest::mousePress(window, Qt::LeftButton, Qt::NoModifier, QPoint(154, 100), 1000);
+ QTest::mouseMove(window, QPoint(147, 101), 16);
+ QTest::mouseMove(window, QPoint(147, 102), 16);
+ QTest::mouseMove(window, QPoint(147, 105), 16);
+ QTest::mouseMove(window, QPoint(148, 109), 15);
+ QTest::mouseMove(window, QPoint(148, 115), 14);
+ QTest::mouseMove(window, QPoint(148, 120), 13);
+ QTest::mouseMove(window, QPoint(150, 125), 13);
+ QTest::mouseMove(window, QPoint(151, 130), 12);
+ QTest::mouseMove(window, QPoint(151, 135), 12);
+ QTest::mouseMove(window, QPoint(153, 140), 12);
+ QTest::mouseMove(window, QPoint(153, 150), 10);
+ QTest::mouseMove(window, QPoint(153, 160), 10);
+ QTest::mouseMove(window, QPoint(153, 170), 9);
+ QTest::mouseMove(window, QPoint(155, 180), 8);
+ QTest::mouseMove(window, QPoint(155, 188), 7);
+ QTest::mouseRelease(window, Qt::LeftButton, Qt::NoModifier, QPoint(20, 188), 0);
#endif
gifRecorder.waitForFinish();