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
163 QQuickTumblerPrivate *tumblerPrivate = QQuickTumblerPrivate::get(tumbler: m_tumbler);
164 // Ignore currentIndex change:
165 // If the view's currentIndex is changed by setHighlightRangeMode(),
166 // it will be reset later.
167 tumblerPrivate->ignoreCurrentIndexChanges = true;
168 // Set this after setting the delegate to avoid unexpected currentIndex changes: QTBUG-79150
169 m_listView->setHighlightRangeMode(QQuickListView::StrictlyEnforceRange);
170 m_listView->setHighlightMoveDuration(1000);
171 tumblerPrivate->ignoreCurrentIndexChanges = false;
172
173 // Reset the view's current index when creating the view:
174 // Setting highlight range mode causes geometry change, and
175 // then the view considers the viewport has moved (viewportMoved()).
176 // The view will update the currentIndex due to the viewport movement.
177 // Here, we check that if the view's currentIndex is not the same as it is
178 // supposed to be (the initial value), and then reset the view's currentIndex.
179 if (m_listView->currentIndex() != tumblerPrivate->currentIndex)
180 m_listView->setCurrentIndex(tumblerPrivate->currentIndex);
181
182 qCDebug(lcTumblerView) << "finished creating ListView";
183 }
184 }
185}
186
187// Called whenever the size or visibleItemCount changes.
188void QQuickTumblerView::updateView()
189{
190 QQuickItem *theView = view();
191 if (!theView)
192 return;
193
194 theView->setSize(QSizeF(width(), height()));
195
196 // Can be called in geometryChange when it might not have a parent item yet.
197 if (!m_tumbler)
198 return;
199
200 // Set view-specific properties that have a dependency on the size, etc.
201 if (m_pathView) {
202 m_pathView->setPathItemCount(m_tumbler->visibleItemCount() + 1);
203 m_pathView->setDragMargin(width() / 2);
204 } else {
205 m_listView->setPreferredHighlightBegin(height() / 2 - (height() / m_tumbler->visibleItemCount() / 2));
206 m_listView->setPreferredHighlightEnd(height() / 2 + (height() / m_tumbler->visibleItemCount() / 2));
207 }
208}
209
210void QQuickTumblerView::updateModel()
211{
212 if (m_pathView && !m_pathView->model().isValid() && m_model.isValid()) {
213 // QQuickPathView::setPathItemCount() resets the offset animation,
214 // so we just skip the animation while constructing the view.
215 const int oldHighlightMoveDuration = m_pathView->highlightMoveDuration();
216 m_pathView->setHighlightMoveDuration(0);
217
218 // Setting model can change the count, which can affect the wrap, which can cause
219 // the current view to be deleted before setModel() is finished, which causes a crash.
220 // Since QQuickTumbler can't know about QQuickTumblerView, we use its private API to
221 // inform it that it should delay setting wrap.
222 QQuickTumblerPrivate *tumblerPrivate = QQuickTumblerPrivate::get(tumbler: m_tumbler);
223 tumblerPrivate->beginSetModel();
224 m_pathView->setModel(m_model);
225 tumblerPrivate->endSetModel();
226
227 // The count-depends-on-wrap behavior could cause wrap to change after
228 // the call above, so we must check that we're still using a PathView.
229 if (m_pathView)
230 m_pathView->setHighlightMoveDuration(oldHighlightMoveDuration);
231 } else if (m_listView && !m_listView->model().isValid() && m_model.isValid()) {
232 const int currentIndex = m_tumbler->currentIndex();
233 QQuickTumblerPrivate *tumblerPrivate = QQuickTumblerPrivate::get(tumbler: m_tumbler);
234
235 // setModel() causes QQuickTumblerPrivate::_q_onViewCountChanged() to
236 // be called, which calls QQuickTumbler::setCurrentIndex(),
237 // which results in QQuickItemViewPrivate::createHighlightItem() being
238 // called. When the highlight item is created,
239 // QQuickTumblerPrivate::itemChildAdded() is notified and
240 // QQuickTumblerPrivate::_q_updateItemHeights() is called, which causes
241 // a geometry change in the item and createHighlight() is called again.
242 // However, since the highlight item hadn't been assigned yet in the
243 // previous call frame, the "if (highlight) { delete highlight; }"
244 // check doesn't succeed, so the item is never deleted.
245 //
246 // To avoid this, we tell QQuickTumblerPrivate to ignore signals while
247 // setting the model, and manually call _q_onViewCountChanged() to
248 // ensure the correct sequence of calls happens (_q_onViewCountChanged()
249 // has to be within the ignoreSignals scope, because it also generates
250 // recursion otherwise).
251 tumblerPrivate->ignoreSignals = true;
252 m_listView->setModel(m_model);
253 m_listView->setCurrentIndex(currentIndex);
254
255 tumblerPrivate->_q_onViewCountChanged();
256 tumblerPrivate->ignoreSignals = false;
257 }
258}
259
260void QQuickTumblerView::geometryChange(const QRectF &newGeometry, const QRectF &oldGeometry)
261{
262 QQuickItem::geometryChange(newGeometry, oldGeometry);
263 updateView();
264}
265
266void QQuickTumblerView::componentComplete()
267{
268 QQuickItem::componentComplete();
269 updateView();
270}
271
272void QQuickTumblerView::itemChange(QQuickItem::ItemChange change, const QQuickItem::ItemChangeData &data)
273{
274 QQuickItem::itemChange(change, data);
275
276 if (change == QQuickItem::ItemParentHasChanged && data.item) {
277 if (m_tumbler)
278 m_tumbler->disconnect(receiver: this);
279
280 m_tumbler = qobject_cast<QQuickTumbler*>(object: parentItem());
281
282 if (m_tumbler) {
283 // We assume that the parentChanged() signal of the tumbler will be emitted before its wrap property is set...
284 connect(sender: m_tumbler, signal: &QQuickTumbler::wrapChanged, context: this, slot: &QQuickTumblerView::createView);
285 connect(sender: m_tumbler, signal: &QQuickTumbler::visibleItemCountChanged, context: this, slot: &QQuickTumblerView::updateView);
286 }
287 }
288}
289
290QQuickItem *QQuickTumblerView::view()
291{
292 if (!m_tumbler)
293 return nullptr;
294
295 if (m_tumbler->wrap())
296 return m_pathView;
297
298 return m_listView;
299}
300
301QT_END_NAMESPACE
302
303#include "moc_qquicktumblerview_p.cpp"
304

Provided by KDAB

Privacy Policy
Learn Advanced QML with KDAB
Find out more

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