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

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