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