diff options
author | Mitch Curtis <mitch.curtis@qt.io> | 2016-05-27 13:28:55 +0200 |
---|---|---|
committer | J-P Nurmi <jpnurmi@qt.io> | 2016-06-28 14:58:42 +0000 |
commit | 4a92e383039f759b198bfdfa8b4bf70cb35c9521 (patch) | |
tree | 8eab7ea2720350a5355fee3a99e880058e77d22b /src | |
parent | 31074d7767949832475bc7c9c643f80d97050ac4 (diff) |
Tumbler: add wrap property
[ChangeLog][Tumbler] Added wrap property to control whether or not
tumbler wraps when it reaches the top and bottom.
Change-Id: I27c543d98f7bc574bc5dc882a130abe0dcc13cea
Task-number: QTBUG-53587
Reviewed-by: J-P Nurmi <jpnurmi@qt.io>
Diffstat (limited to 'src')
-rw-r--r-- | src/imports/controls/Tumbler.qml | 27 | ||||
-rw-r--r-- | src/imports/controls/doc/qtquickcontrols2.qdocconf | 1 | ||||
-rw-r--r-- | src/imports/controls/doc/snippets/qtquickcontrols2-tumbler-listView.qml | 46 | ||||
-rw-r--r-- | src/imports/controls/doc/snippets/qtquickcontrols2-tumbler-pathView.qml | 57 | ||||
-rw-r--r-- | src/imports/controls/doc/snippets/qtquickcontrols2-tumbler-timePicker.qml | 103 | ||||
-rw-r--r-- | src/imports/controls/doc/src/qtquickcontrols2-customize.qdoc | 9 | ||||
-rw-r--r-- | src/imports/controls/material/Tumbler.qml | 23 | ||||
-rw-r--r-- | src/imports/controls/plugins.qmltypes | 3 | ||||
-rw-r--r-- | src/imports/controls/qtquickcontrols2plugin.cpp | 3 | ||||
-rw-r--r-- | src/imports/controls/universal/Tumbler.qml | 23 | ||||
-rw-r--r-- | src/imports/templates/qtquicktemplates2plugin.cpp | 1 | ||||
-rw-r--r-- | src/quickcontrols2/qquicktumblerview.cpp | 238 | ||||
-rw-r--r-- | src/quickcontrols2/qquicktumblerview_p.h | 108 | ||||
-rw-r--r-- | src/quickcontrols2/quickcontrols2.pri | 6 | ||||
-rw-r--r-- | src/quicktemplates2/qquicktumbler.cpp | 388 | ||||
-rw-r--r-- | src/quicktemplates2/qquicktumbler_p.h | 6 |
16 files changed, 875 insertions, 167 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; |