diff options
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(); |