1// Copyright (C) 2017 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
3
4#include "qquicktumblerview_p.h"
5
6#include <QtCore/qloggingcategory.h>
7#include <QtQuick/private/qquickitem_p.h>
8#include <QtQuick/private/qquicklistview_p.h>
9#include <QtQuick/private/qquickpathview_p.h>
10
11#include <QtQuickTemplates2/private/qquicktumbler_p.h>
12#include <QtQuickTemplates2/private/qquicktumbler_p_p.h>
13
14QT_BEGIN_NAMESPACE
15
16Q_LOGGING_CATEGORY(lcTumblerView, "qt.quick.controls.tumblerview")
17
18QQuickTumblerView::QQuickTumblerView(QQuickItem *parent) :
19 QQuickItem(parent)
20{
21 // We don't call createView() here because we don't know what the wrap flag is set to
22 // yet, and we don't want to create a view that might never get used.
23}
24
25QVariant QQuickTumblerView::model() const
26{
27 return m_model;
28}
29
30void QQuickTumblerView::setModel(const QVariant &model)
31{
32 qCDebug(lcTumblerView) << "setting model to:" << model << "on"
33 << (m_pathView ? static_cast<QObject*>(m_pathView) : static_cast<QObject*>(m_listView));
34 if (model == m_model)
35 return;
36
37 m_model = model;
38
39 if (m_pathView) {
40 m_pathView->setModel(m_model);
41 } else if (m_listView) {
42 // QQuickItemView::setModel() resets the current index,
43 // but if we're still creating the Tumbler, it should be maintained.
44 const int oldCurrentIndex = m_listView->currentIndex();
45 m_listView->setModel(m_model);
46 if (!isComponentComplete())
47 m_listView->setCurrentIndex(oldCurrentIndex);
48 }
49
50 emit modelChanged();
51}
52
53QQmlComponent *QQuickTumblerView::delegate() const
54{
55 return m_delegate;
56}
57
58void QQuickTumblerView::setDelegate(QQmlComponent *delegate)
59{
60 qCDebug(lcTumblerView) << "setting delegate to:" << delegate << "on"
61 << (m_pathView ? static_cast<QObject*>(m_pathView) : static_cast<QObject*>(m_listView));
62 if (delegate == m_delegate)
63 return;
64
65 m_delegate = delegate;
66
67 if (m_pathView)
68 m_pathView->setDelegate(m_delegate);
69 else if (m_listView)
70 m_listView->setDelegate(m_delegate);
71
72 emit delegateChanged();
73}
74
75QQuickPath *QQuickTumblerView::path() const
76{
77 return m_path;
78}
79
80void QQuickTumblerView::setPath(QQuickPath *path)
81{
82 if (path == m_path)
83 return;
84
85 m_path = path;
86 emit pathChanged();
87}
88
89void QQuickTumblerView::createView()
90{
91 Q_ASSERT(m_tumbler);
92
93 // We create a view regardless of whether or not we know
94 // the count yet, because we rely on the view to tell us the count.
95 if (m_tumbler->wrap()) {
96 if (m_listView) {
97 // It's necessary to call deleteLater() rather than delete,
98 // as this code is most likely being run in rensponse to a signal
99 // emission somewhere in the list view's internals, so we need to
100 // wait until that has finished.
101 m_listView->deleteLater();
102 QQml_setParent_noEvent(object: m_listView, parent: nullptr);
103 // The auto tests pass with unparenting the list view alone, but
104 // just to be sure, we unset some other things as well.
105 m_listView->setParentItem(nullptr);
106 m_listView->setVisible(false);
107 m_listView->setModel(QVariant());
108 m_listView = nullptr;
109 }
110
111 if (!m_pathView) {
112 qCDebug(lcTumblerView) << "creating PathView";
113
114 m_pathView = new QQuickPathView;
115 QQmlEngine::setContextForObject(m_pathView, qmlContext(this));
116 QQml_setParent_noEvent(object: m_pathView, parent: this);
117 m_pathView->setParentItem(this);
118 m_pathView->setPath(m_path);
119 m_pathView->setDelegate(m_delegate);
120 m_pathView->setPreferredHighlightBegin(0.5);
121 m_pathView->setPreferredHighlightEnd(0.5);
122 m_pathView->setHighlightMoveDuration(1000);
123 m_pathView->setClip(true);
124
125 // Give the view a size.
126 updateView();
127 // Set the model.
128 updateModel();
129
130 qCDebug(lcTumblerView) << "finished creating PathView";
131 }
132 } else {
133 if (m_pathView) {
134 m_pathView->deleteLater();
135 QQml_setParent_noEvent(object: m_pathView, parent: nullptr);
136 m_pathView->setParentItem(nullptr);
137 m_pathView->setVisible(false);
138 m_pathView->setModel(QVariant());
139 m_pathView = nullptr;
140 }
141
142 if (!m_listView) {
143 qCDebug(lcTumblerView) << "creating ListView";
144
145 m_listView = new QQuickListView;
146 QQmlEngine::setContextForObject(m_listView, qmlContext(this));
147 QQml_setParent_noEvent(object: m_listView, parent: this);
148 m_listView->setParentItem(this);
149 m_listView->setSnapMode(QQuickListView::SnapToItem);
150 m_listView->setClip(true);
151
152 // Give the view a size.
153 updateView();
154 // Set the model.
155 updateModel();
156
157 // Set these after the model is set so that the currentItem animation
158 // happens instantly on startup/after switching models. If we set them too early,
159 // the view animates any potential currentIndex change over one second,
160 // which we don't want when the contentItem has just been created.
161 m_listView->setDelegate(m_delegate);
162 // Set this after setting the delegate to avoid unexpected currentIndex changes: QTBUG-79150
163 m_listView->setHighlightRangeMode(QQuickListView::StrictlyEnforceRange);
164 m_listView->setHighlightMoveDuration(1000);
165
166 qCDebug(lcTumblerView) << "finished creating ListView";
167 }
168 }
169}
170
171// Called whenever the size or visibleItemCount changes.
172void QQuickTumblerView::updateView()
173{
174 QQuickItem *theView = view();
175 if (!theView)
176 return;
177
178 theView->setSize(QSizeF(width(), height()));
179
180 // Can be called in geometryChange when it might not have a parent item yet.
181 if (!m_tumbler)
182 return;
183
184 // Set view-specific properties that have a dependency on the size, etc.
185 if (m_pathView) {
186 m_pathView->setPathItemCount(m_tumbler->visibleItemCount() + 1);
187 m_pathView->setDragMargin(width() / 2);
188 } else {
189 m_listView->setPreferredHighlightBegin(height() / 2 - (height() / m_tumbler->visibleItemCount() / 2));
190 m_listView->setPreferredHighlightEnd(height() / 2 + (height() / m_tumbler->visibleItemCount() / 2));
191 }
192}
193
194void QQuickTumblerView::updateModel()
195{
196 if (m_pathView && !m_pathView->model().isValid() && m_model.isValid()) {
197 // QQuickPathView::setPathItemCount() resets the offset animation,
198 // so we just skip the animation while constructing the view.
199 const int oldHighlightMoveDuration = m_pathView->highlightMoveDuration();
200 m_pathView->setHighlightMoveDuration(0);
201
202 // Setting model can change the count, which can affect the wrap, which can cause
203 // the current view to be deleted before setModel() is finished, which causes a crash.
204 // Since QQuickTumbler can't know about QQuickTumblerView, we use its private API to
205 // inform it that it should delay setting wrap.
206 QQuickTumblerPrivate *tumblerPrivate = QQuickTumblerPrivate::get(tumbler: m_tumbler);
207 tumblerPrivate->beginSetModel();
208 m_pathView->setModel(m_model);
209 tumblerPrivate->endSetModel();
210
211 // The count-depends-on-wrap behavior could cause wrap to change after
212 // the call above, so we must check that we're still using a PathView.
213 if (m_pathView)
214 m_pathView->setHighlightMoveDuration(oldHighlightMoveDuration);
215 } else if (m_listView && !m_listView->model().isValid() && m_model.isValid()) {
216 const int currentIndex = m_tumbler->currentIndex();
217 QQuickTumblerPrivate *tumblerPrivate = QQuickTumblerPrivate::get(tumbler: m_tumbler);
218
219 // setModel() causes QQuickTumblerPrivate::_q_onViewCountChanged() to
220 // be called, which calls QQuickTumbler::setCurrentIndex(),
221 // which results in QQuickItemViewPrivate::createHighlightItem() being
222 // called. When the highlight item is created,
223 // QQuickTumblerPrivate::itemChildAdded() is notified and
224 // QQuickTumblerPrivate::_q_updateItemHeights() is called, which causes
225 // a geometry change in the item and createHighlight() is called again.
226 // However, since the highlight item hadn't been assigned yet in the
227 // previous call frame, the "if (highlight) { delete highlight; }"
228 // check doesn't succeed, so the item is never deleted.
229 //
230 // To avoid this, we tell QQuickTumblerPrivate to ignore signals while
231 // setting the model, and manually call _q_onViewCountChanged() to
232 // ensure the correct sequence of calls happens (_q_onViewCountChanged()
233 // has to be within the ignoreSignals scope, because it also generates
234 // recursion otherwise).
235 tumblerPrivate->ignoreSignals = true;
236 m_listView->setModel(m_model);
237 m_listView->setCurrentIndex(currentIndex);
238
239 tumblerPrivate->_q_onViewCountChanged();
240 tumblerPrivate->ignoreSignals = false;
241 }
242}
243
244void QQuickTumblerView::geometryChange(const QRectF &newGeometry, const QRectF &oldGeometry)
245{
246 QQuickItem::geometryChange(newGeometry, oldGeometry);
247 updateView();
248}
249
250void QQuickTumblerView::componentComplete()
251{
252 QQuickItem::componentComplete();
253 updateView();
254}
255
256void QQuickTumblerView::itemChange(QQuickItem::ItemChange change, const QQuickItem::ItemChangeData &data)
257{
258 QQuickItem::itemChange(change, data);
259
260 if (change == QQuickItem::ItemParentHasChanged && data.item) {
261 if (m_tumbler)
262 m_tumbler->disconnect(receiver: this);
263
264 m_tumbler = qobject_cast<QQuickTumbler*>(object: parentItem());
265
266 if (m_tumbler) {
267 // We assume that the parentChanged() signal of the tumbler will be emitted before its wrap property is set...
268 connect(sender: m_tumbler, signal: &QQuickTumbler::wrapChanged, context: this, slot: &QQuickTumblerView::createView);
269 connect(sender: m_tumbler, signal: &QQuickTumbler::visibleItemCountChanged, context: this, slot: &QQuickTumblerView::updateView);
270 }
271 }
272}
273
274QQuickItem *QQuickTumblerView::view()
275{
276 if (!m_tumbler)
277 return nullptr;
278
279 if (m_tumbler->wrap())
280 return m_pathView;
281
282 return m_listView;
283}
284
285QT_END_NAMESPACE
286
287#include "moc_qquicktumblerview_p.cpp"
288

source code of qtdeclarative/src/quickcontrolsimpl/qquicktumblerview.cpp