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

source code of qtquickcontrols2/src/quickcontrols2/qquicktumblerview.cpp