1// Copyright (C) 2019 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 "qquickpathview_p.h"
5#include "qquickpathview_p_p.h"
6#include "qquickflickablebehavior_p.h" //Contains flicking behavior defines
7#include "qquicktext_p.h"
8
9#include <QtQml/qqmlcomponent.h>
10#include <QtQuick/private/qquickstate_p.h>
11#include <private/qqmlglobal_p.h>
12#include <private/qqmlopenmetaobject_p.h>
13#include <private/qqmlchangeset_p.h>
14#include <qpa/qplatformtheme.h>
15
16#include <QtQml/qqmlinfo.h>
17
18#include <QtGui/private/qeventpoint_p.h>
19#include <QtGui/qevent.h>
20#include <QtGui/qguiapplication.h>
21#include <QtGui/private/qguiapplication_p.h>
22#include <QtGui/qstylehints.h>
23#include <QtCore/qmath.h>
24
25#include <cmath>
26
27#if QT_CONFIG(quick_itemview)
28#include <private/qquickitemview_p.h>
29#endif
30
31QT_BEGIN_NAMESPACE
32
33#if !QT_CONFIG(quick_itemview)
34Q_STATIC_LOGGING_CATEGORY(lcItemViewDelegateLifecycle, "qt.quick.itemview.lifecycle")
35#endif
36Q_STATIC_LOGGING_CATEGORY(lcPathView, "qt.quick.pathview")
37
38static QQmlOpenMetaObjectType *qPathViewAttachedType = nullptr;
39
40QQuickPathViewAttached::QQuickPathViewAttached(QObject *parent)
41: QObject(parent), m_percent(-1), m_view(nullptr), m_onPath(false), m_isCurrent(false)
42{
43 if (qPathViewAttachedType) {
44 m_metaobject = new QQmlOpenMetaObject(this, qPathViewAttachedType);
45 m_metaobject->setCached(true);
46 } else {
47 m_metaobject = new QQmlOpenMetaObject(this);
48 }
49}
50
51QQuickPathViewAttached::~QQuickPathViewAttached()
52{
53}
54
55QVariant QQuickPathViewAttached::value(const QByteArray &name) const
56{
57 return m_metaobject->value(name);
58}
59void QQuickPathViewAttached::setValue(const QByteArray &name, const QVariant &val)
60{
61 m_metaobject->setValue(name, val);
62}
63
64QQuickPathViewPrivate::QQuickPathViewPrivate()
65 : path(nullptr), currentIndex(0), currentItemOffset(0), startPc(0)
66 , offset(0), offsetAdj(0), mappedRange(1), mappedCache(0)
67 , stealMouse(false), ownModel(false), interactive(true), haveHighlightRange(true)
68 , autoHighlight(true), highlightUp(false), layoutScheduled(false)
69 , moving(false), flicking(false), dragging(false), inRequest(false), delegateValidated(false)
70 , inRefill(false)
71 , dragMargin(0), deceleration(100)
72 , maximumFlickVelocity(QGuiApplicationPrivate::platformTheme()->themeHint(hint: QPlatformTheme::FlickMaximumVelocity).toReal())
73 , moveOffset(this, &QQuickPathViewPrivate::setAdjustedOffset), flickDuration(0)
74 , pathItems(-1), requestedIndex(-1), cacheSize(0), requestedCacheSize(0), requestedZ(0)
75 , moveReason(Other), movementDirection(QQuickPathView::Shortest), moveDirection(QQuickPathView::Shortest)
76 , attType(nullptr), highlightComponent(nullptr), highlightItem(nullptr)
77 , moveHighlight(this, &QQuickPathViewPrivate::setHighlightPosition)
78 , highlightPosition(0)
79 , highlightRangeStart(0), highlightRangeEnd(0)
80 , highlightRangeMode(QQuickPathView::StrictlyEnforceRange)
81 , highlightMoveDuration(300), modelCount(0), snapMode(QQuickPathView::NoSnap)
82{
83 setSizePolicy(horizontalPolicy: QLayoutPolicy::Preferred, verticalPolicy: QLayoutPolicy::Preferred);
84}
85
86void QQuickPathViewPrivate::init()
87{
88 Q_Q(QQuickPathView);
89 offset = 0;
90 q->setAcceptedMouseButtons(Qt::LeftButton);
91 q->setFlag(flag: QQuickItem::ItemIsFocusScope);
92 q->setFiltersChildMouseEvents(true);
93 qmlobject_connect(&tl, QQuickTimeLine, SIGNAL(updated()),
94 q, QQuickPathView, SLOT(ticked()));
95 timer.invalidate();
96 qmlobject_connect(&tl, QQuickTimeLine, SIGNAL(completed()),
97 q, QQuickPathView, SLOT(movementEnding()));
98}
99
100QQuickItem *QQuickPathViewPrivate::getItem(int modelIndex, qreal z, bool async)
101{
102 Q_Q(QQuickPathView);
103 requestedIndex = modelIndex;
104 requestedZ = z;
105 inRequest = true;
106 QObject *object = model->object(index: modelIndex, incubationMode: async ? QQmlIncubator::Asynchronous : QQmlIncubator::AsynchronousIfNested);
107 QQuickItem *item = qmlobject_cast<QQuickItem*>(object);
108 if (!item) {
109 if (object) {
110 model->release(object);
111 if (!delegateValidated) {
112 delegateValidated = true;
113 QObject* delegate = q->delegate();
114 qmlWarning(me: delegate ? delegate : q) << QQuickPathView::tr(s: "Delegate must be of Item type");
115 }
116 }
117 } else {
118 item->setParentItem(q);
119 requestedIndex = -1;
120 QQuickItemPrivate *itemPrivate = QQuickItemPrivate::get(item);
121 itemPrivate->addItemChangeListener(
122 listener: this, types: QQuickItemPrivate::Geometry | QQuickItemPrivate::Destroyed);
123 }
124 inRequest = false;
125 return item;
126}
127
128void QQuickPathView::createdItem(int index, QObject *object)
129{
130 Q_D(QQuickPathView);
131 QQuickItem *item = qmlobject_cast<QQuickItem*>(object);
132 if (d->requestedIndex != index) {
133 qPathViewAttachedType = d->attachedType();
134 QQuickPathViewAttached *att = static_cast<QQuickPathViewAttached *>(qmlAttachedPropertiesObject<QQuickPathView>(obj: item));
135 qPathViewAttachedType = nullptr;
136 if (att) {
137 att->m_view = this;
138 att->setOnPath(false);
139 }
140 item->setParentItem(this);
141 d->updateItem(item, 1);
142 } else {
143 d->requestedIndex = -1;
144 if (!d->inRequest)
145 refill();
146 }
147}
148
149void QQuickPathView::initItem(int index, QObject *object)
150{
151 Q_D(QQuickPathView);
152 QQuickItem *item = qmlobject_cast<QQuickItem*>(object);
153 if (item && d->requestedIndex == index) {
154 QQuickItemPrivate::get(item)->setCulled(true);
155 item->setParentItem(this);
156 qPathViewAttachedType = d->attachedType();
157 QQuickPathViewAttached *att = static_cast<QQuickPathViewAttached *>(qmlAttachedPropertiesObject<QQuickPathView>(obj: item));
158 qPathViewAttachedType = nullptr;
159 if (att) {
160 att->m_view = this;
161 qreal percent = d->positionOfIndex(index);
162 if (percent < 1 && d->path) {
163 const auto attributes = d->path->attributes();
164 for (const QString &attr : attributes)
165 att->setValue(name: attr.toUtf8(), val: d->path->attributeAt(attr, percent));
166 item->setZ(d->requestedZ);
167 }
168 att->setOnPath(percent < 1);
169 }
170 }
171}
172
173void QQuickPathViewPrivate::releaseItem(QQuickItem *item)
174{
175 if (!item)
176 return;
177 qCDebug(lcItemViewDelegateLifecycle) << "release" << item;
178 QQuickItemPrivate *itemPrivate = QQuickItemPrivate::get(item);
179 itemPrivate->removeItemChangeListener(
180 this, types: QQuickItemPrivate::Geometry | QQuickItemPrivate::Destroyed);
181 if (!model)
182 return;
183 QQmlInstanceModel::ReleaseFlags flags = model->release(object: item);
184 if (!flags) {
185 // item was not destroyed, and we no longer reference it.
186 if (QQuickPathViewAttached *att = attached(item))
187 att->setOnPath(false);
188 } else if (flags & QQmlInstanceModel::Destroyed) {
189 // but we still reference it
190 item->setParentItem(nullptr);
191 }
192}
193
194QQuickPathViewAttached *QQuickPathViewPrivate::attached(QQuickItem *item)
195{
196 return static_cast<QQuickPathViewAttached *>(qmlAttachedPropertiesObject<QQuickPathView>(obj: item, create: false));
197}
198
199QQmlOpenMetaObjectType *QQuickPathViewPrivate::attachedType()
200{
201 if (!attType) {
202 // pre-create one metatype to share with all attached objects
203 attType = new QQmlOpenMetaObjectType(&QQuickPathViewAttached::staticMetaObject);
204 if (path) {
205 const auto attributes = path->attributes();
206 for (const QString &attr : attributes)
207 attType->createProperty(name: attr.toUtf8());
208 }
209 }
210
211 return attType;
212}
213
214void QQuickPathViewPrivate::clear()
215{
216 releaseCurrentItem();
217
218 for (QQuickItem *p : std::as_const(t&: items))
219 releaseItem(item: p);
220
221 for (QQuickItem *p : std::as_const(t&: itemCache))
222 releaseItem(item: p);
223
224 if (requestedIndex >= 0) {
225 if (model)
226 model->cancel(requestedIndex);
227 requestedIndex = -1;
228 }
229
230 items.clear();
231 itemCache.clear();
232 tl.clear();
233}
234
235void QQuickPathViewPrivate::updateMappedRange()
236{
237 // Update the actual cache size to be at max
238 // the available non-visible items.
239 cacheSize = qMax(a: 0, b: qMin(a: requestedCacheSize, b: modelCount - pathItems));
240
241 if (model && pathItems != -1 && pathItems < modelCount) {
242 mappedRange = qreal(modelCount)/pathItems;
243 mappedCache = qreal(cacheSize)/pathItems/2; // Half of cache at each end
244 } else {
245 mappedRange = 1;
246 mappedCache = 0;
247 }
248}
249
250qreal QQuickPathViewPrivate::positionOfIndex(qreal index) const
251{
252 qreal pos = -1;
253
254 if (model && index >= 0 && index < modelCount) {
255 qreal start = 0;
256 if (haveHighlightRange && (highlightRangeMode != QQuickPathView::NoHighlightRange
257 || snapMode != QQuickPathView::NoSnap))
258 start = highlightRangeStart;
259 qreal globalPos = index + offset;
260 globalPos = std::fmod(x: globalPos, y: qreal(modelCount)) / modelCount;
261 if (pathItems != -1 && pathItems < modelCount) {
262 globalPos += start / mappedRange;
263 globalPos = std::fmod(x: globalPos, y: qreal(1));
264 pos = globalPos * mappedRange;
265 } else {
266 pos = std::fmod(x: globalPos + start, y: qreal(1));
267 }
268 }
269
270 return pos;
271}
272
273// returns true if position is between lower and upper, taking into
274// account the circular space.
275bool QQuickPathViewPrivate::isInBound(qreal position, qreal lower,
276 qreal upper, bool emptyRangeCheck) const
277{
278 if (emptyRangeCheck && qFuzzyCompare(p1: lower, p2: upper))
279 return true;
280 if (lower > upper) {
281 if (position > upper && position > lower)
282 position -= mappedRange;
283 lower -= mappedRange;
284 }
285 return position >= lower && position < upper;
286}
287
288void QQuickPathViewPrivate::createHighlight()
289{
290 Q_Q(QQuickPathView);
291 if (!q->isComponentComplete())
292 return;
293
294 bool changed = false;
295 if (highlightItem) {
296 highlightItem->setParentItem(nullptr);
297 highlightItem->deleteLater();
298 highlightItem = nullptr;
299 changed = true;
300 }
301
302 QQuickItem *item = nullptr;
303 if (highlightComponent) {
304 QQmlContext *creationContext = highlightComponent->creationContext();
305 QQmlContext *highlightContext = new QQmlContext(
306 creationContext ? creationContext : qmlContext(q));
307 QObject *nobj = highlightComponent->create(context: highlightContext);
308 if (nobj) {
309 QQml_setParent_noEvent(object: highlightContext, parent: nobj);
310 item = qobject_cast<QQuickItem *>(o: nobj);
311 if (!item)
312 delete nobj;
313 } else {
314 delete highlightContext;
315 }
316 } else {
317 item = new QQuickItem;
318 }
319 if (item) {
320 QQml_setParent_noEvent(object: item, parent: q);
321 item->setParentItem(q);
322 highlightItem = item;
323 changed = true;
324 }
325 if (changed)
326 emit q->highlightItemChanged();
327}
328
329void QQuickPathViewPrivate::updateHighlight()
330{
331 Q_Q(QQuickPathView);
332 if (!q->isComponentComplete() || !isValid())
333 return;
334 if (highlightItem) {
335 if (haveHighlightRange && highlightRangeMode == QQuickPathView::StrictlyEnforceRange) {
336 updateItem(highlightItem, highlightRangeStart);
337 } else {
338 qreal target = currentIndex;
339
340 offsetAdj = 0;
341 tl.reset(moveHighlight);
342 moveHighlight.setValue(highlightPosition);
343
344 const int duration = highlightMoveDuration;
345
346 if (target - highlightPosition > modelCount/2) {
347 highlightUp = false;
348 qreal distance = modelCount - target + highlightPosition;
349 tl.move(moveHighlight, destination: 0, QEasingCurve(QEasingCurve::InQuad), time: int(duration * highlightPosition / distance));
350 tl.set(moveHighlight, modelCount-0.01);
351 tl.move(moveHighlight, destination: target, QEasingCurve(QEasingCurve::OutQuad), time: int(duration * (modelCount-target) / distance));
352 } else if (target - highlightPosition <= -modelCount/2) {
353 highlightUp = true;
354 qreal distance = modelCount - highlightPosition + target;
355 tl.move(moveHighlight, destination: modelCount-0.01, QEasingCurve(QEasingCurve::InQuad), time: int(duration * (modelCount-highlightPosition) / distance));
356 tl.set(moveHighlight, 0);
357 tl.move(moveHighlight, destination: target, QEasingCurve(QEasingCurve::OutQuad), time: int(duration * target / distance));
358 } else {
359 highlightUp = highlightPosition - target < 0;
360 tl.move(moveHighlight, destination: target, QEasingCurve(QEasingCurve::InOutQuad), time: duration);
361 }
362 }
363 }
364}
365
366void QQuickPathViewPrivate::setHighlightPosition(qreal pos)
367{
368 if (!(qFuzzyCompare(p1: pos, p2: highlightPosition))) {
369 qreal start = 0;
370 qreal end = 1;
371 if (haveHighlightRange && highlightRangeMode != QQuickPathView::NoHighlightRange) {
372 start = highlightRangeStart;
373 end = highlightRangeEnd;
374 }
375
376 qreal range = qreal(modelCount);
377 // calc normalized position of highlight relative to offset
378 qreal relativeHighlight = std::fmod(x: pos + offset, y: range) / range;
379
380 if (!highlightUp && relativeHighlight > end / mappedRange) {
381 qreal diff = 1 - relativeHighlight;
382 setOffset(offset + diff * range);
383 } else if (highlightUp && relativeHighlight >= (end - start) / mappedRange) {
384 qreal diff = relativeHighlight - (end - start) / mappedRange;
385 setOffset(offset - diff * range - 0.00001);
386 }
387
388 highlightPosition = pos;
389 qreal pathPos = positionOfIndex(index: pos);
390 updateItem(highlightItem, pathPos);
391 if (QQuickPathViewAttached *att = attached(item: highlightItem))
392 att->setOnPath(pathPos < 1);
393 }
394}
395
396void QQuickPathView::pathUpdated()
397{
398 Q_D(QQuickPathView);
399 for (QQuickItem *item : std::as_const(t&: d->items)) {
400 if (QQuickPathViewAttached *att = d->attached(item))
401 att->m_percent = -1;
402 }
403 refill();
404}
405
406void QQuickPathViewPrivate::updateItem(QQuickItem *item, qreal percent)
407{
408 if (!path)
409 return;
410 if (QQuickPathViewAttached *att = attached(item)) {
411 if (qFuzzyCompare(p1: att->m_percent, p2: percent))
412 return;
413 att->m_percent = percent;
414 const auto attributes = path->attributes();
415 for (const QString &attr : attributes)
416 att->setValue(name: attr.toUtf8(), val: path->attributeAt(attr, percent));
417 att->setOnPath(percent < 1);
418 }
419 QQuickItemPrivate::get(item)->setCulled(percent >= 1);
420 QPointF pf = path->pointAtPercent(t: qMin(a: percent, b: qreal(1)));
421 item->setX(pf.x() - item->width()/2);
422 item->setY(pf.y() - item->height()/2);
423}
424
425void QQuickPathViewPrivate::regenerate()
426{
427 Q_Q(QQuickPathView);
428 if (!q->isComponentComplete())
429 return;
430
431 clear();
432
433 if (!isValid())
434 return;
435
436 updateMappedRange();
437 q->refill();
438}
439
440void QQuickPathViewPrivate::setDragging(bool d)
441{
442 Q_Q(QQuickPathView);
443 if (dragging == d)
444 return;
445
446 dragging = d;
447 if (dragging)
448 emit q->dragStarted();
449 else
450 emit q->dragEnded();
451
452 emit q->draggingChanged();
453}
454
455/*!
456 \qmltype PathView
457 \nativetype QQuickPathView
458 \inqmlmodule QtQuick
459 \ingroup qtquick-paths
460 \ingroup qtquick-views
461 \inherits Item
462 \brief Lays out model-provided items on a path.
463
464 A PathView displays data from models created from built-in QML types like ListModel
465 and XmlListModel, or custom model classes defined in C++ that inherit from
466 QAbstractListModel.
467
468 The view has a \l model, which defines the data to be displayed, and
469 a \l delegate, which defines how the data should be displayed.
470 The \l delegate is instantiated for each item on the \l path.
471 The items may be flicked to move them along the path.
472
473 For example, if there is a simple list model defined in a file \c ContactModel.qml like this:
474
475 \snippet qml/pathview/ContactModel.qml 0
476
477 This data can be represented as a PathView, like this:
478
479 \snippet qml/pathview/pathview.qml 0
480
481 \image pathview.gif
482
483 (Note the above example uses PathAttribute to scale and modify the
484 opacity of the items as they rotate. This additional code can be seen in the
485 PathAttribute documentation.)
486
487 PathView does not automatically handle keyboard navigation. This is because
488 the keys to use for navigation will depend upon the shape of the path. Navigation
489 can be added quite simply by setting \c focus to \c true and calling
490 \l decrementCurrentIndex() or \l incrementCurrentIndex(), for example to navigate
491 using the left and right arrow keys:
492
493 \qml
494 PathView {
495 // ...
496 focus: true
497 Keys.onLeftPressed: decrementCurrentIndex()
498 Keys.onRightPressed: incrementCurrentIndex()
499 }
500 \endqml
501
502 The path view itself is a focus scope (see \l{Keyboard Focus in Qt Quick} for more details).
503
504 Delegates are instantiated as needed and may be destroyed at any time.
505 State should \e never be stored in a delegate.
506
507 PathView attaches a number of properties to the root item of the delegate, for example
508 \c {PathView.isCurrentItem}. In the following example, the root delegate item can access
509 this attached property directly as \c PathView.isCurrentItem, while the child
510 \c nameText object must refer to this property as \c wrapper.PathView.isCurrentItem.
511
512 \snippet qml/pathview/pathview.qml 1
513
514 \b Note that views do not enable \e clip automatically. If the view
515 is not clipped by another item or the screen, it will be necessary
516 to set \e {clip: true} in order to have the out of view items clipped
517 nicely.
518
519 \sa Path, {QML Data Models}, ListView, GridView, {Qt Quick Examples - Views}
520*/
521
522QQuickPathView::QQuickPathView(QQuickItem *parent)
523 : QQuickItem(*(new QQuickPathViewPrivate), parent)
524{
525 Q_D(QQuickPathView);
526 d->init();
527}
528
529QQuickPathView::~QQuickPathView()
530{
531 Q_D(QQuickPathView);
532 d->clear();
533 if (d->attType)
534 d->attType->release();
535 if (d->ownModel)
536 delete d->model;
537}
538
539/*!
540 \qmlattachedproperty PathView QtQuick::PathView::view
541 \readonly
542
543 This attached property holds the view that manages this delegate instance.
544
545 It is attached to each instance of the delegate.
546*/
547
548/*!
549 \qmlattachedproperty bool QtQuick::PathView::onPath
550 \readonly
551
552 This attached property holds whether the item is currently on the path.
553
554 If a pathItemCount has been set, it is possible that some items may
555 be instantiated, but not considered to be currently on the path.
556 Usually, these items would be set invisible, for example:
557
558 \qml
559 Component {
560 Rectangle {
561 visible: PathView.onPath
562 // ...
563 }
564 }
565 \endqml
566
567 It is attached to each instance of the delegate.
568*/
569
570/*!
571 \qmlattachedproperty bool QtQuick::PathView::isCurrentItem
572 \readonly
573
574 This attached property is true if this delegate is the current item; otherwise false.
575
576 It is attached to each instance of the delegate.
577
578 This property may be used to adjust the appearance of the current item.
579
580 \snippet qml/pathview/pathview.qml 1
581*/
582
583/*!
584 \qmlproperty model QtQuick::PathView::model
585 This property holds the model providing data for the view.
586
587 The model provides a set of data that is used to create the items for the view.
588 For large or dynamic datasets the model is usually provided by a C++ model object.
589 Models can also be created directly in QML, using the ListModel type.
590
591 \note changing the model will reset the offset and currentIndex to 0.
592
593 \sa {qml-data-models}{Data Models}
594*/
595QVariant QQuickPathView::model() const
596{
597 Q_D(const QQuickPathView);
598 return d->modelVariant;
599}
600
601void QQuickPathView::setModel(const QVariant &m)
602{
603 Q_D(QQuickPathView);
604 QVariant model = m;
605 if (model.userType() == qMetaTypeId<QJSValue>())
606 model = model.value<QJSValue>().toVariant();
607
608 if (d->modelVariant == model)
609 return;
610
611 if (d->model) {
612 qmlobject_disconnect(d->model, QQmlInstanceModel, SIGNAL(modelUpdated(QQmlChangeSet,bool)),
613 this, QQuickPathView, SLOT(modelUpdated(QQmlChangeSet,bool)));
614 qmlobject_disconnect(d->model, QQmlInstanceModel, SIGNAL(createdItem(int,QObject*)),
615 this, QQuickPathView, SLOT(createdItem(int,QObject*)));
616 qmlobject_disconnect(d->model, QQmlInstanceModel, SIGNAL(initItem(int,QObject*)),
617 this, QQuickPathView, SLOT(initItem(int,QObject*)));
618 d->clear();
619 }
620
621 d->modelVariant = model;
622 QObject *object = qvariant_cast<QObject*>(v: model);
623 QQmlInstanceModel *vim = nullptr;
624 if (object && (vim = qobject_cast<QQmlInstanceModel *>(object))) {
625 if (d->ownModel) {
626 delete d->model;
627 d->ownModel = false;
628 }
629 d->model = vim;
630 } else {
631 if (!d->ownModel) {
632 d->model = new QQmlDelegateModel(qmlContext(this));
633 d->ownModel = true;
634 if (isComponentComplete())
635 static_cast<QQmlDelegateModel *>(d->model.data())->componentComplete();
636 }
637 if (QQmlDelegateModel *dataModel = qobject_cast<QQmlDelegateModel*>(object: d->model))
638 dataModel->setModel(model);
639 }
640 int oldModelCount = d->modelCount;
641 d->modelCount = 0;
642 if (d->model) {
643 qmlobject_connect(d->model, QQmlInstanceModel, SIGNAL(modelUpdated(QQmlChangeSet,bool)),
644 this, QQuickPathView, SLOT(modelUpdated(QQmlChangeSet,bool)));
645 qmlobject_connect(d->model, QQmlInstanceModel, SIGNAL(createdItem(int,QObject*)),
646 this, QQuickPathView, SLOT(createdItem(int,QObject*)));
647 qmlobject_connect(d->model, QQmlInstanceModel, SIGNAL(initItem(int,QObject*)),
648 this, QQuickPathView, SLOT(initItem(int,QObject*)));
649 d->modelCount = d->model->count();
650 }
651 if (isComponentComplete()) {
652 if (d->currentIndex != 0) {
653 d->currentIndex = 0;
654 emit currentIndexChanged();
655 }
656 if (!(qFuzzyIsNull(d: d->offset))) {
657 d->offset = 0;
658 emit offsetChanged();
659 }
660 }
661 d->regenerate();
662 if (d->modelCount != oldModelCount)
663 emit countChanged();
664 emit modelChanged();
665}
666
667/*!
668 \qmlproperty int QtQuick::PathView::count
669 This property holds the number of items in the model.
670*/
671int QQuickPathView::count() const
672{
673 Q_D(const QQuickPathView);
674 return d->model ? d->modelCount : 0;
675}
676
677/*!
678 \qmlproperty Path QtQuick::PathView::path
679 This property holds the path used to lay out the items.
680 For more information see the \l Path documentation.
681*/
682QQuickPath *QQuickPathView::path() const
683{
684 Q_D(const QQuickPathView);
685 return d->path;
686}
687
688void QQuickPathView::setPath(QQuickPath *path)
689{
690 Q_D(QQuickPathView);
691 if (d->path == path)
692 return;
693 if (d->path)
694 qmlobject_disconnect(d->path, QQuickPath, SIGNAL(changed()),
695 this, QQuickPathView, SLOT(pathUpdated()));
696 d->path = path;
697
698 if (path) {
699 qmlobject_connect(d->path, QQuickPath, SIGNAL(changed()),
700 this, QQuickPathView, SLOT(pathUpdated()));
701 }
702
703 if (isComponentComplete()) {
704 d->clear();
705 if (d->isValid()) {
706 if (d->attType) {
707 d->attType->release();
708 d->attType = nullptr;
709 }
710 d->regenerate();
711 }
712 }
713
714 emit pathChanged();
715}
716
717/*!
718 \qmlproperty int QtQuick::PathView::currentIndex
719 This property holds the index of the current item.
720*/
721int QQuickPathView::currentIndex() const
722{
723 Q_D(const QQuickPathView);
724 return d->currentIndex;
725}
726
727void QQuickPathView::setCurrentIndex(int idx)
728{
729 Q_D(QQuickPathView);
730 if (!isComponentComplete()) {
731 if (idx != d->currentIndex) {
732 d->currentIndex = idx;
733 emit currentIndexChanged();
734 }
735 return;
736 }
737
738 idx = d->modelCount
739 ? ((idx % d->modelCount) + d->modelCount) % d->modelCount
740 : 0;
741 if (d->model && (idx != d->currentIndex || !d->currentItem)) {
742 const bool hadCurrentItem = d->currentItem != nullptr;
743 const int oldCurrentIdx = d->currentIndex;
744 if (hadCurrentItem) {
745 if (QQuickPathViewAttached *att = d->attached(item: d->currentItem))
746 att->setIsCurrentItem(false);
747 d->releaseCurrentItem();
748 }
749 d->moveReason = QQuickPathViewPrivate::SetIndex;
750 d->currentIndex = idx;
751 if (d->modelCount) {
752 d->createCurrentItem();
753 if (d->haveHighlightRange && d->highlightRangeMode == QQuickPathView::StrictlyEnforceRange)
754 d->snapToIndex(index: d->currentIndex, reason: QQuickPathViewPrivate::SetIndex);
755 d->currentItemOffset = d->positionOfIndex(index: d->currentIndex);
756 d->updateHighlight();
757 }
758 if (oldCurrentIdx != d->currentIndex)
759 emit currentIndexChanged();
760 if (hadCurrentItem)
761 emit currentItemChanged();
762 }
763}
764
765/*!
766 \qmlproperty Item QtQuick::PathView::currentItem
767 This property holds the current item in the view.
768*/
769QQuickItem *QQuickPathView::currentItem() const
770{
771 Q_D(const QQuickPathView);
772 return d->currentItem;
773}
774
775/*!
776 \qmlmethod QtQuick::PathView::incrementCurrentIndex()
777
778 Increments the current index.
779
780 \b Note: methods should only be called after the Component has completed.
781*/
782void QQuickPathView::incrementCurrentIndex()
783{
784 Q_D(QQuickPathView);
785 d->moveDirection = QQuickPathView::Positive;
786 setCurrentIndex(currentIndex()+1);
787}
788
789/*!
790 \qmlmethod QtQuick::PathView::decrementCurrentIndex()
791
792 Decrements the current index.
793
794 \b Note: methods should only be called after the Component has completed.
795*/
796void QQuickPathView::decrementCurrentIndex()
797{
798 Q_D(QQuickPathView);
799 d->moveDirection = QQuickPathView::Negative;
800 setCurrentIndex(currentIndex()-1);
801}
802
803/*!
804 \qmlproperty real QtQuick::PathView::offset
805
806 The offset specifies how far along the path the items are from their initial positions.
807 This is a real number that ranges from \c 0 to the count of items in the model.
808*/
809qreal QQuickPathView::offset() const
810{
811 Q_D(const QQuickPathView);
812 return d->offset;
813}
814
815void QQuickPathView::setOffset(qreal offset)
816{
817 Q_D(QQuickPathView);
818 d->moveReason = QQuickPathViewPrivate::Other;
819 d->setOffset(offset);
820 d->updateCurrent();
821}
822
823void QQuickPathViewPrivate::setOffset(qreal o)
824{
825 Q_Q(QQuickPathView);
826 if (!qFuzzyCompare(p1: offset, p2: o)) {
827 if (isValid() && q->isComponentComplete()) {
828 qreal oldOffset = offset;
829 offset = std::fmod(x: o, y: qreal(modelCount));
830 if (offset < 0)
831 offset += qreal(modelCount);
832 qCDebug(lcItemViewDelegateLifecycle) << o << "was" << oldOffset << "now" << offset;
833 q->refill();
834 } else {
835 offset = o;
836 }
837 emit q->offsetChanged();
838 }
839}
840
841void QQuickPathViewPrivate::setAdjustedOffset(qreal o)
842{
843 setOffset(o+offsetAdj);
844}
845
846/*!
847 \qmlproperty Component QtQuick::PathView::highlight
848 This property holds the component to use as the highlight.
849
850 An instance of the highlight component will be created for each view.
851 The geometry of the resultant component instance will be managed by the view
852 so as to stay with the current item.
853
854 The below example demonstrates how to make a simple highlight. Note the use
855 of the \l{PathView::onPath}{PathView.onPath} attached property to ensure that
856 the highlight is hidden when flicked away from the path.
857
858 \qml
859 Component {
860 Rectangle {
861 visible: PathView.onPath
862 // ...
863 }
864 }
865 \endqml
866
867 \sa highlightItem, highlightRangeMode
868*/
869QQmlComponent *QQuickPathView::highlight() const
870{
871 Q_D(const QQuickPathView);
872 return d->highlightComponent;
873}
874
875void QQuickPathView::setHighlight(QQmlComponent *highlight)
876{
877 Q_D(QQuickPathView);
878 if (highlight != d->highlightComponent) {
879 d->highlightComponent = highlight;
880 d->createHighlight();
881 d->updateHighlight();
882 emit highlightChanged();
883 }
884}
885
886/*!
887 \qmlproperty Item QtQuick::PathView::highlightItem
888
889 \c highlightItem holds the highlight item, which was created
890 from the \l highlight component.
891
892 \sa highlight
893*/
894QQuickItem *QQuickPathView::highlightItem() const
895{
896 Q_D(const QQuickPathView);
897 return d->highlightItem;
898}
899
900/*!
901 \qmlproperty real QtQuick::PathView::preferredHighlightBegin
902 \qmlproperty real QtQuick::PathView::preferredHighlightEnd
903 \qmlproperty enumeration QtQuick::PathView::highlightRangeMode
904
905 These properties set the preferred range of the highlight (current item)
906 within the view. The preferred values must be in the range from \c 0 to \c 1.
907
908 Valid values for \c highlightRangeMode are:
909
910 \value PathView.NoHighlightRange no range is applied: the highlight
911 will move freely within the view.
912 \value PathView.ApplyRange the view will attempt to maintain the highlight
913 within the range, however the highlight can move
914 outside of the range at the ends of the path or
915 due to a mouse interaction.
916 \value PathView.StrictlyEnforceRange the highlight will never move outside of the range.
917 This means that the current item will change if a
918 keyboard or mouse action would cause the highlight
919 to move outside of the range.
920
921 The default value is \e PathView.StrictlyEnforceRange.
922
923 Defining a highlight range is the correct way to influence where the
924 current item ends up when the view moves. For example, if you want the
925 currently selected item to be in the middle of the path, then set the
926 highlight range to be 0.5,0.5 and highlightRangeMode to \e PathView.StrictlyEnforceRange.
927 Then, when the path scrolls,
928 the currently selected item will be the item at that position. This also applies to
929 when the currently selected item changes - it will scroll to within the preferred
930 highlight range. Furthermore, the behaviour of the current item index will occur
931 whether or not a highlight exists.
932
933 \note A valid range requires \c preferredHighlightEnd to be greater
934 than or equal to \c preferredHighlightBegin.
935*/
936qreal QQuickPathView::preferredHighlightBegin() const
937{
938 Q_D(const QQuickPathView);
939 return d->highlightRangeStart;
940}
941
942void QQuickPathView::setPreferredHighlightBegin(qreal start)
943{
944 Q_D(QQuickPathView);
945 if (qFuzzyCompare(p1: d->highlightRangeStart, p2: start) || start < 0 || start > 1)
946 return;
947 d->highlightRangeStart = start;
948 d->haveHighlightRange = d->highlightRangeStart <= d->highlightRangeEnd;
949 refill();
950 emit preferredHighlightBeginChanged();
951}
952
953qreal QQuickPathView::preferredHighlightEnd() const
954{
955 Q_D(const QQuickPathView);
956 return d->highlightRangeEnd;
957}
958
959void QQuickPathView::setPreferredHighlightEnd(qreal end)
960{
961 Q_D(QQuickPathView);
962 if (qFuzzyCompare(p1: d->highlightRangeEnd, p2: end) || end < 0 || end > 1)
963 return;
964 d->highlightRangeEnd = end;
965 d->haveHighlightRange = d->highlightRangeStart <= d->highlightRangeEnd;
966 refill();
967 emit preferredHighlightEndChanged();
968}
969
970QQuickPathView::HighlightRangeMode QQuickPathView::highlightRangeMode() const
971{
972 Q_D(const QQuickPathView);
973 return d->highlightRangeMode;
974}
975
976void QQuickPathView::setHighlightRangeMode(HighlightRangeMode mode)
977{
978 Q_D(QQuickPathView);
979 if (d->highlightRangeMode == mode)
980 return;
981 d->highlightRangeMode = mode;
982 d->haveHighlightRange = d->highlightRangeStart <= d->highlightRangeEnd;
983 if (d->haveHighlightRange) {
984 d->regenerate();
985 int index = d->highlightRangeMode != NoHighlightRange ? d->currentIndex : d->calcCurrentIndex();
986 if (index >= 0)
987 d->snapToIndex(index, reason: QQuickPathViewPrivate::Other);
988 }
989 emit highlightRangeModeChanged();
990}
991
992/*!
993 \qmlproperty int QtQuick::PathView::highlightMoveDuration
994 This property holds the move animation duration of the highlight delegate.
995
996 If the highlightRangeMode is StrictlyEnforceRange then this property
997 determines the speed that the items move along the path.
998
999 The default value for the duration is 300ms.
1000*/
1001int QQuickPathView::highlightMoveDuration() const
1002{
1003 Q_D(const QQuickPathView);
1004 return d->highlightMoveDuration;
1005}
1006
1007void QQuickPathView::setHighlightMoveDuration(int duration)
1008{
1009 Q_D(QQuickPathView);
1010 if (d->highlightMoveDuration == duration)
1011 return;
1012 d->highlightMoveDuration = duration;
1013 emit highlightMoveDurationChanged();
1014}
1015
1016/*!
1017 \qmlproperty real QtQuick::PathView::dragMargin
1018 This property holds the maximum distance from the path that initiates mouse dragging.
1019
1020 By default the path can only be dragged by clicking on an item. If
1021 dragMargin is greater than zero, a drag can be initiated by clicking
1022 within dragMargin pixels of the path.
1023*/
1024qreal QQuickPathView::dragMargin() const
1025{
1026 Q_D(const QQuickPathView);
1027 return d->dragMargin;
1028}
1029
1030void QQuickPathView::setDragMargin(qreal dragMargin)
1031{
1032 Q_D(QQuickPathView);
1033 if (qFuzzyCompare(p1: d->dragMargin, p2: dragMargin))
1034 return;
1035 d->dragMargin = dragMargin;
1036 emit dragMarginChanged();
1037}
1038
1039/*!
1040 \qmlproperty real QtQuick::PathView::flickDeceleration
1041 This property holds the rate at which a flick will decelerate.
1042
1043 The default is 100.
1044*/
1045qreal QQuickPathView::flickDeceleration() const
1046{
1047 Q_D(const QQuickPathView);
1048 return d->deceleration;
1049}
1050
1051void QQuickPathView::setFlickDeceleration(qreal dec)
1052{
1053 Q_D(QQuickPathView);
1054 if (qFuzzyCompare(p1: d->deceleration, p2: dec))
1055 return;
1056 d->deceleration = dec;
1057 emit flickDecelerationChanged();
1058}
1059
1060/*!
1061 \qmlproperty real QtQuick::PathView::maximumFlickVelocity
1062 This property holds the approximate maximum velocity that the user can flick the view in pixels/second.
1063
1064 The default value is platform dependent.
1065*/
1066qreal QQuickPathView::maximumFlickVelocity() const
1067{
1068 Q_D(const QQuickPathView);
1069 return d->maximumFlickVelocity;
1070}
1071
1072void QQuickPathView::setMaximumFlickVelocity(qreal vel)
1073{
1074 Q_D(QQuickPathView);
1075 if (qFuzzyCompare(p1: vel, p2: d->maximumFlickVelocity))
1076 return;
1077 d->maximumFlickVelocity = vel;
1078 emit maximumFlickVelocityChanged();
1079}
1080
1081
1082/*!
1083 \qmlproperty bool QtQuick::PathView::interactive
1084
1085 A user cannot drag or flick a PathView that is not interactive.
1086
1087 This property is useful for temporarily disabling flicking. This allows
1088 special interaction with PathView's children.
1089*/
1090bool QQuickPathView::isInteractive() const
1091{
1092 Q_D(const QQuickPathView);
1093 return d->interactive;
1094}
1095
1096void QQuickPathView::setInteractive(bool interactive)
1097{
1098 Q_D(QQuickPathView);
1099 if (interactive != d->interactive) {
1100 d->interactive = interactive;
1101 if (!interactive)
1102 d->tl.clear();
1103 emit interactiveChanged();
1104 }
1105}
1106
1107/*!
1108 \qmlproperty bool QtQuick::PathView::moving
1109
1110 This property holds whether the view is currently moving
1111 due to the user either dragging or flicking the view.
1112*/
1113bool QQuickPathView::isMoving() const
1114{
1115 Q_D(const QQuickPathView);
1116 return d->moving;
1117}
1118
1119/*!
1120 \qmlproperty bool QtQuick::PathView::flicking
1121
1122 This property holds whether the view is currently moving
1123 due to the user flicking the view.
1124*/
1125bool QQuickPathView::isFlicking() const
1126{
1127 Q_D(const QQuickPathView);
1128 return d->flicking;
1129}
1130
1131/*!
1132 \qmlproperty bool QtQuick::PathView::dragging
1133
1134 This property holds whether the view is currently moving
1135 due to the user dragging the view.
1136*/
1137bool QQuickPathView::isDragging() const
1138{
1139 Q_D(const QQuickPathView);
1140 return d->dragging;
1141}
1142
1143/*!
1144 \qmlsignal QtQuick::PathView::movementStarted()
1145
1146 This signal is emitted when the view begins moving due to user
1147 interaction.
1148*/
1149
1150/*!
1151 \qmlsignal QtQuick::PathView::movementEnded()
1152
1153 This signal is emitted when the view stops moving due to user
1154 interaction. If a flick was generated, this signal will
1155 be emitted once the flick stops. If a flick was not
1156 generated, this signal will be emitted when the
1157 user stops dragging - i.e. a mouse or touch release.
1158*/
1159
1160/*!
1161 \qmlsignal QtQuick::PathView::flickStarted()
1162
1163 This signal is emitted when the view is flicked. A flick
1164 starts from the point that the mouse or touch is released,
1165 while still in motion.
1166*/
1167
1168/*!
1169 \qmlsignal QtQuick::PathView::flickEnded()
1170
1171 This signal is emitted when the view stops moving due to a flick.
1172*/
1173
1174/*!
1175 \qmlsignal QtQuick::PathView::dragStarted()
1176
1177 This signal is emitted when the view starts to be dragged due to user
1178 interaction.
1179*/
1180
1181/*!
1182 \qmlsignal QtQuick::PathView::dragEnded()
1183
1184 This signal is emitted when the user stops dragging the view.
1185
1186 If the velocity of the drag is suffient at the time the
1187 touch/mouse button is released then a flick will start.
1188*/
1189
1190/*!
1191 \qmlproperty Component QtQuick::PathView::delegate
1192
1193 The delegate provides a template defining each item instantiated by the view.
1194 The index is exposed as an accessible \c index property. Properties of the
1195 model are also available depending upon the type of \l {qml-data-models}{Data Model}.
1196
1197 The number of objects and bindings in the delegate has a direct effect on the
1198 flicking performance of the view when pathItemCount is specified. If at all possible, place functionality
1199 that is not needed for the normal display of the delegate in a \l Loader which
1200 can load additional components when needed.
1201
1202 Note that the PathView will layout the items based on the size of the root
1203 item in the delegate.
1204
1205 Here is an example delegate:
1206 \snippet qml/pathview/pathview.qml 1
1207*/
1208QQmlComponent *QQuickPathView::delegate() const
1209{
1210 Q_D(const QQuickPathView);
1211 if (d->model) {
1212 if (QQmlDelegateModel *dataModel = qobject_cast<QQmlDelegateModel*>(object: d->model))
1213 return dataModel->delegate();
1214 }
1215
1216 return nullptr;
1217}
1218
1219void QQuickPathView::setDelegate(QQmlComponent *delegate)
1220{
1221 Q_D(QQuickPathView);
1222 if (delegate == this->delegate())
1223 return;
1224 if (!d->ownModel) {
1225 d->model = new QQmlDelegateModel(qmlContext(this));
1226 d->ownModel = true;
1227 if (isComponentComplete())
1228 static_cast<QQmlDelegateModel *>(d->model.data())->componentComplete();
1229 }
1230 if (QQmlDelegateModel *dataModel = qobject_cast<QQmlDelegateModel*>(object: d->model)) {
1231 int oldCount = dataModel->count();
1232 dataModel->setDelegate(delegate);
1233 d->modelCount = dataModel->count();
1234 d->regenerate();
1235 if (oldCount != dataModel->count())
1236 emit countChanged();
1237 emit delegateChanged();
1238 d->delegateValidated = false;
1239 }
1240}
1241
1242/*!
1243 \qmlproperty int QtQuick::PathView::pathItemCount
1244 This property holds the number of items visible on the path at any one time.
1245
1246 Setting pathItemCount to undefined will show all items on the path.
1247*/
1248int QQuickPathView::pathItemCount() const
1249{
1250 Q_D(const QQuickPathView);
1251 return d->pathItems;
1252}
1253
1254void QQuickPathView::setPathItemCount(int i)
1255{
1256 Q_D(QQuickPathView);
1257 if (i == d->pathItems)
1258 return;
1259 if (i < 1)
1260 i = 1;
1261 d->pathItems = i;
1262 d->updateMappedRange();
1263 if (d->isValid() && isComponentComplete()) {
1264 d->regenerate();
1265 }
1266 emit pathItemCountChanged();
1267}
1268
1269void QQuickPathView::resetPathItemCount()
1270{
1271 Q_D(QQuickPathView);
1272 if (-1 == d->pathItems)
1273 return;
1274 d->pathItems = -1;
1275 d->updateMappedRange();
1276 if (d->isValid() && isComponentComplete())
1277 d->regenerate();
1278 emit pathItemCountChanged();
1279}
1280
1281/*!
1282 \qmlproperty int QtQuick::PathView::cacheItemCount
1283 This property holds the maximum number of items to cache off the path.
1284
1285 For example, a PathView with a model containing 20 items, a pathItemCount
1286 of 10, and an cacheItemCount of 4 will create up to 14 items, with 10 visible
1287 on the path and 4 invisible cached items.
1288
1289 The cached delegates are created asynchronously,
1290 allowing creation to occur across multiple frames and reducing the
1291 likelihood of skipping frames.
1292
1293 \note Setting this property is not a replacement for creating efficient delegates.
1294 It can improve the smoothness of scrolling behavior at the expense of additional
1295 memory usage. The fewer objects and bindings in a delegate, the faster a
1296 view can be scrolled. It is important to realize that setting cacheItemCount
1297 will only postpone issues caused by slow-loading delegates, it is not a
1298 solution for this scenario.
1299
1300 \sa pathItemCount
1301*/
1302int QQuickPathView::cacheItemCount() const
1303{
1304 Q_D(const QQuickPathView);
1305 return d->requestedCacheSize;
1306}
1307
1308void QQuickPathView::setCacheItemCount(int i)
1309{
1310 Q_D(QQuickPathView);
1311 if (i == d->requestedCacheSize || i < 0)
1312 return;
1313
1314 d->requestedCacheSize = i;
1315 d->updateMappedRange();
1316 refill();
1317 emit cacheItemCountChanged();
1318}
1319
1320/*!
1321 \qmlproperty enumeration QtQuick::PathView::snapMode
1322
1323 This property determines how the items will settle following a drag or flick.
1324 The possible values are:
1325
1326 \value PathView.NoSnap (default) the items stop anywhere along the path.
1327 \value PathView.SnapToItem the items settle with an item aligned with the \l preferredHighlightBegin.
1328 \value PathView.SnapOneItem the items settle no more than one item away from the item nearest
1329 \l preferredHighlightBegin at the time the press is released. This mode is particularly
1330 useful for moving one page at a time.
1331
1332 \c snapMode does not affect the \l currentIndex. To update the
1333 \l currentIndex as the view is moved, set \l highlightRangeMode
1334 to \c PathView.StrictlyEnforceRange (default for PathView).
1335
1336 \sa highlightRangeMode
1337*/
1338QQuickPathView::SnapMode QQuickPathView::snapMode() const
1339{
1340 Q_D(const QQuickPathView);
1341 return d->snapMode;
1342}
1343
1344void QQuickPathView::setSnapMode(SnapMode mode)
1345{
1346 Q_D(QQuickPathView);
1347 if (mode == d->snapMode)
1348 return;
1349 d->snapMode = mode;
1350 emit snapModeChanged();
1351}
1352
1353/*!
1354 \qmlproperty enumeration QtQuick::PathView::movementDirection
1355 \since 5.7
1356
1357 This property determines the direction in which items move when setting the current index.
1358 The possible values are:
1359
1360 \value PathView.Shortest (default) the items move in the direction that requires the least
1361 movement, which could be either \c Negative or \c Positive.
1362 \value PathView.Negative the items move backwards towards their destination.
1363 \value PathView.Positive the items move forwards towards their destination.
1364
1365 For example, suppose that there are 5 items in the model, and \l currentIndex is \c 0.
1366 If currentIndex is set to \c 2,
1367
1368 \list
1369 \li a \c Positive movement direction will result in the following order: 0, 1, 2
1370 \li a \c Negative movement direction will result in the following order: 0, 5, 4, 3, 2
1371 \li a \c Shortest movement direction will result in same order with \c Positive .
1372 \endlist
1373
1374 \note this property doesn't affect the movement of \l incrementCurrentIndex() and \l decrementCurrentIndex().
1375*/
1376QQuickPathView::MovementDirection QQuickPathView::movementDirection() const
1377{
1378 Q_D(const QQuickPathView);
1379 return d->movementDirection;
1380}
1381
1382void QQuickPathView::setMovementDirection(QQuickPathView::MovementDirection dir)
1383{
1384 Q_D(QQuickPathView);
1385 if (dir == d->movementDirection)
1386 return;
1387 d->movementDirection = dir;
1388 if (!d->tl.isActive())
1389 d->moveDirection = d->movementDirection;
1390 emit movementDirectionChanged();
1391}
1392
1393/*!
1394 \qmlmethod QtQuick::PathView::positionViewAtIndex(int index, PositionMode mode)
1395
1396 Positions the view such that the \a index is at the position specified by
1397 \a mode:
1398
1399 \value PathView.Beginning position item at the beginning of the path.
1400 \value PathView.Center position item in the center of the path.
1401 \value PathView.End position item at the end of the path.
1402 \value PathView.Contain ensure the item is positioned on the path.
1403 \value PathView.SnapPosition position the item at \l preferredHighlightBegin. This mode
1404 is only valid if \l highlightRangeMode is StrictlyEnforceRange or snapping is enabled
1405 via \l snapMode.
1406
1407 \b Note: methods should only be called after the Component has completed. To position
1408 the view at startup, this method should be called by Component.onCompleted. For
1409 example, to position the view at the end:
1410
1411 \code
1412 Component.onCompleted: positionViewAtIndex(count - 1, PathView.End)
1413 \endcode
1414*/
1415void QQuickPathView::positionViewAtIndex(int index, int mode)
1416{
1417 Q_D(QQuickPathView);
1418 if (!d->isValid())
1419 return;
1420 if (mode < QQuickPathView::Beginning || mode > QQuickPathView::SnapPosition || mode == 3) // 3 is unused in PathView
1421 return;
1422
1423 if (mode == QQuickPathView::Contain && (d->pathItems < 0 || d->modelCount <= d->pathItems))
1424 return;
1425
1426 int count = d->pathItems == -1 ? d->modelCount : qMin(a: d->pathItems, b: d->modelCount);
1427 int idx = (index+d->modelCount) % d->modelCount;
1428 bool snap = d->haveHighlightRange && (d->highlightRangeMode != QQuickPathView::NoHighlightRange
1429 || d->snapMode != QQuickPathView::NoSnap);
1430
1431 qreal beginOffset;
1432 qreal endOffset;
1433 if (snap) {
1434 beginOffset = d->modelCount - idx - qFloor(v: count * d->highlightRangeStart);
1435 endOffset = beginOffset + count - 1;
1436 } else {
1437 beginOffset = d->modelCount - idx;
1438 // Small offset since the last point coincides with the first and
1439 // this the only "end" position that gives the expected visual result.
1440 qreal adj = sizeof(qreal) == sizeof(float) ? 0.00001f : 0.000000000001;
1441 endOffset = std::fmod(x: beginOffset + count, y: qreal(d->modelCount)) - adj;
1442 }
1443 qreal offset = d->offset;
1444 switch (mode) {
1445 case Beginning:
1446 offset = beginOffset;
1447 break;
1448 case End:
1449 offset = endOffset;
1450 break;
1451 case Center:
1452 if (beginOffset < endOffset)
1453 offset = (beginOffset + endOffset)/2;
1454 else
1455 offset = (beginOffset + (endOffset + d->modelCount))/2;
1456 if (snap)
1457 offset = qRound(d: offset);
1458 break;
1459 case Contain:
1460 if ((beginOffset < endOffset && (d->offset < beginOffset || d->offset > endOffset))
1461 || (d->offset < beginOffset && d->offset > endOffset)) {
1462 qreal diff1 = std::fmod(x: beginOffset - d->offset + d->modelCount, y: qreal(d->modelCount));
1463 qreal diff2 = std::fmod(x: d->offset - endOffset + d->modelCount, y: qreal(d->modelCount));
1464 if (diff1 < diff2)
1465 offset = beginOffset;
1466 else
1467 offset = endOffset;
1468 }
1469 break;
1470 case SnapPosition:
1471 offset = d->modelCount - idx;
1472 break;
1473 }
1474
1475 d->tl.clear();
1476 setOffset(offset);
1477}
1478
1479/*!
1480 \qmlmethod int QtQuick::PathView::indexAt(real x, real y)
1481
1482 Returns the index of the item containing the point \a x, \a y in content
1483 coordinates. If there is no item at the point specified, -1 is returned.
1484
1485 \b Note: methods should only be called after the Component has completed.
1486*/
1487int QQuickPathView::indexAt(qreal x, qreal y) const
1488{
1489 Q_D(const QQuickPathView);
1490 QQuickItem *item = itemAt(x, y);
1491 return item ? d->model->indexOf(object: item, objectContext: nullptr) : -1;
1492}
1493
1494/*!
1495 \qmlmethod Item QtQuick::PathView::itemAt(real x, real y)
1496
1497 Returns the item containing the point \a x, \a y in content
1498 coordinates. If there is no item at the point specified, null is returned.
1499
1500 \b Note: methods should only be called after the Component has completed.
1501*/
1502QQuickItem *QQuickPathView::itemAt(qreal x, qreal y) const
1503{
1504 Q_D(const QQuickPathView);
1505 if (!d->isValid())
1506 return nullptr;
1507
1508 for (QQuickItem *item : d->items) {
1509 QPointF p = item->mapFromItem(item: this, point: QPointF(x, y));
1510 if (item->contains(point: p))
1511 return item;
1512 }
1513
1514 return nullptr;
1515}
1516
1517/*!
1518 \qmlmethod Item QtQuick::PathView::itemAtIndex(int index)
1519
1520 Returns the item for \a index. If there is no item for that index, for example
1521 because it has not been created yet, or because it has been panned out of
1522 the visible area and removed from the cache, null is returned.
1523
1524 \b Note: this method should only be called after the Component has completed.
1525 The returned value should also not be stored since it can turn to null
1526 as soon as control goes out of the calling scope, if the view releases that item.
1527
1528 \since 5.13
1529*/
1530QQuickItem *QQuickPathView::itemAtIndex(int index) const
1531{
1532 Q_D(const QQuickPathView);
1533 if (!d->isValid())
1534 return nullptr;
1535
1536 for (QQuickItem *item : d->items) {
1537 if (index == d->model->indexOf(object: item, objectContext: nullptr))
1538 return item;
1539 }
1540
1541 return nullptr;
1542}
1543
1544/*!
1545 \internal
1546
1547 Returns a point in the path, that has the closest distance from \a point.
1548 A value in the range 0-1 will be written to \a nearPercent if given, which
1549 represents where on the path the \a point is closest to. \c 0 means the very
1550 beginning of the path, and \c 1 means the very end.
1551*/
1552QPointF QQuickPathViewPrivate::pointNear(const QPointF &point, qreal *nearPercent) const
1553{
1554 const auto pathLength = path->path().length();
1555 qreal samples = qMin(a: pathLength / 5, b: qreal(500));
1556 qreal res = pathLength / samples;
1557
1558 qreal mindist = 1e10; // big number
1559 QPointF nearPoint = path->pointAtPercent(t: 0);
1560 qreal nearPc = 0;
1561
1562 // get rough pos
1563 for (qreal i=1; i < samples; i++) {
1564 QPointF pt = path->pointAtPercent(t: i/samples);
1565 QPointF diff = pt - point;
1566 qreal dist = diff.x()*diff.x() + diff.y()*diff.y();
1567 if (dist < mindist) {
1568 nearPoint = pt;
1569 nearPc = i;
1570 mindist = dist;
1571 }
1572 }
1573
1574 // now refine
1575 qreal approxPc = nearPc;
1576 for (qreal i = approxPc-1; i < approxPc+1; i += 1/(2*res)) {
1577 QPointF pt = path->pointAtPercent(t: i/samples);
1578 QPointF diff = pt - point;
1579 qreal dist = diff.x()*diff.x() + diff.y()*diff.y();
1580 if (dist < mindist) {
1581 nearPoint = pt;
1582 nearPc = i;
1583 mindist = dist;
1584 }
1585 }
1586
1587 if (nearPercent)
1588 *nearPercent = nearPc / samples;
1589
1590 return nearPoint;
1591}
1592
1593void QQuickPathViewPrivate::addVelocitySample(qreal v)
1594{
1595 velocityBuffer.append(v);
1596 if (velocityBuffer.count() > QML_FLICK_SAMPLEBUFFER)
1597 velocityBuffer.remove(idx: 0);
1598 qCDebug(lcPathView) << "instantaneous velocity" << v;
1599}
1600
1601qreal QQuickPathViewPrivate::calcVelocity() const
1602{
1603 qreal velocity = 0;
1604 if (velocityBuffer.count() > QML_FLICK_DISCARDSAMPLES) {
1605 int count = velocityBuffer.count()-QML_FLICK_DISCARDSAMPLES;
1606 for (int i = 0; i < count; ++i) {
1607 qreal v = velocityBuffer.at(idx: i);
1608 velocity += v;
1609 }
1610 velocity /= count;
1611 qCDebug(lcPathView) << "average velocity" << velocity << "based on" << count << "samples";
1612 }
1613 return velocity;
1614}
1615
1616qint64 QQuickPathViewPrivate::computeCurrentTime(QInputEvent *event) const
1617{
1618 if (0 != event->timestamp())
1619 return qint64(event->timestamp());
1620 return timer.elapsed();
1621}
1622
1623void QQuickPathView::mousePressEvent(QMouseEvent *event)
1624{
1625 Q_D(QQuickPathView);
1626 if (d->interactive) {
1627 d->handleMousePressEvent(event);
1628 event->accept();
1629 } else {
1630 QQuickItem::mousePressEvent(event);
1631 }
1632}
1633
1634void QQuickPathViewPrivate::handleMousePressEvent(QMouseEvent *event)
1635{
1636 Q_Q(QQuickPathView);
1637 if (!interactive || !items.size() || !model || !modelCount)
1638 return;
1639 velocityBuffer.clear();
1640 int idx = 0;
1641 for (; idx < items.size(); ++idx) {
1642 QQuickItem *item = items.at(i: idx);
1643 if (item->contains(point: item->mapFromScene(point: event->scenePosition())))
1644 break;
1645 }
1646 if (idx == items.size() && qFuzzyIsNull(d: dragMargin)) // didn't click on an item
1647 return;
1648
1649 startPoint = pointNear(point: event->position(), nearPercent: &startPc);
1650 startPos = event->position();
1651 if (idx == items.size()) {
1652 qreal distance = qAbs(t: event->position().x() - startPoint.x()) + qAbs(t: event->position().y() - startPoint.y());
1653 if (distance > dragMargin)
1654 return;
1655 }
1656
1657 if (tl.isActive() && flicking && flickDuration && qreal(tl.time()) / flickDuration < 0.8) {
1658 stealMouse = true; // If we've been flicked then steal the click.
1659 q->grabMouse(); // grab it right now too, just to be sure (QTBUG-77173)
1660 } else {
1661 stealMouse = false;
1662 }
1663 q->setKeepMouseGrab(stealMouse);
1664
1665 timer.start();
1666 lastPosTime = computeCurrentTime(event);
1667 tl.clear();
1668}
1669
1670void QQuickPathView::mouseMoveEvent(QMouseEvent *event)
1671{
1672 Q_D(QQuickPathView);
1673 if (d->interactive) {
1674 d->handleMouseMoveEvent(event);
1675 event->accept();
1676 } else {
1677 QQuickItem::mouseMoveEvent(event);
1678 }
1679}
1680
1681void QQuickPathViewPrivate::handleMouseMoveEvent(QMouseEvent *event)
1682{
1683 Q_Q(QQuickPathView);
1684 if (!interactive || !timer.isValid() || !model || !modelCount)
1685 return;
1686
1687 qint64 currentTimestamp = computeCurrentTime(event);
1688 qreal newPc;
1689 QPointF pathPoint = pointNear(point: event->position(), nearPercent: &newPc);
1690 if (!stealMouse) {
1691 QPointF posDelta = event->position() - startPos;
1692 if (QQuickDeliveryAgentPrivate::dragOverThreshold(d: posDelta.y(), axis: Qt::YAxis, event) ||
1693 QQuickDeliveryAgentPrivate::dragOverThreshold(d: posDelta.x(), axis: Qt::XAxis, event)) {
1694 // The touch has exceeded the threshold. If the movement along the path is close to the drag threshold
1695 // then we'll assume that this gesture targets the PathView. This ensures PathView gesture grabbing
1696 // is in sync with other items.
1697 QPointF pathDelta = pathPoint - startPoint;
1698 const int startDragDistance = QGuiApplication::styleHints()->startDragDistance();
1699 if (qAbs(t: pathDelta.x()) > startDragDistance * 0.8
1700 || qAbs(t: pathDelta.y()) > startDragDistance * 0.8) {
1701 stealMouse = true;
1702 q->setKeepMouseGrab(true);
1703 }
1704 }
1705 } else {
1706 moveReason = QQuickPathViewPrivate::Mouse;
1707 int count = pathItems == -1 ? modelCount : qMin(a: pathItems, b: modelCount);
1708 qreal diff = (newPc - startPc)*count;
1709 if (!qFuzzyIsNull(d: diff)) {
1710 q->setOffset(offset + diff);
1711
1712 if (diff > modelCount/2)
1713 diff -= modelCount;
1714 else if (diff < -modelCount/2)
1715 diff += modelCount;
1716
1717 qint64 elapsed = currentTimestamp - lastPosTime;
1718 if (elapsed > 0)
1719 addVelocitySample(v: diff / (qreal(elapsed) / 1000));
1720 }
1721 if (!moving) {
1722 moving = true;
1723 emit q->movingChanged();
1724 emit q->movementStarted();
1725 }
1726 setDragging(true);
1727 }
1728 startPc = newPc;
1729 lastPosTime = currentTimestamp;
1730}
1731
1732void QQuickPathView::mouseReleaseEvent(QMouseEvent *event)
1733{
1734 Q_D(QQuickPathView);
1735 if (d->interactive) {
1736 d->handleMouseReleaseEvent(event);
1737 event->accept();
1738 ungrabMouse();
1739 } else {
1740 QQuickItem::mouseReleaseEvent(event);
1741 }
1742}
1743
1744void QQuickPathViewPrivate::handleMouseReleaseEvent(QMouseEvent *event)
1745{
1746 Q_Q(QQuickPathView);
1747 stealMouse = false;
1748 q->setKeepMouseGrab(false);
1749 setDragging(false);
1750 if (!interactive || !timer.isValid() || !model || !modelCount) {
1751 timer.invalidate();
1752 if (!tl.isActive())
1753 q->movementEnding();
1754 return;
1755 }
1756
1757 qreal velocity = calcVelocity();
1758 qint64 elapsed = computeCurrentTime(event) - lastPosTime;
1759 // Let the velocity linearly decay such that it becomes 0 if elapsed time > QML_FLICK_VELOCITY_DECAY_TIME
1760 // The intention is that if you are flicking at some speed, then stop in one place for some time before releasing,
1761 // the previous velocity is lost. (QTBUG-77173, QTBUG-59052)
1762 velocity *= qreal(qMax(a: 0LL, QML_FLICK_VELOCITY_DECAY_TIME - elapsed)) / QML_FLICK_VELOCITY_DECAY_TIME;
1763 qCDebug(lcPathView) << "after elapsed time" << elapsed << "velocity decayed to" << velocity;
1764 qreal count = pathItems == -1 ? modelCount : qMin(a: pathItems, b: modelCount);
1765 const auto averageItemLength = path->path().length() / count;
1766 qreal pixelVelocity = averageItemLength * velocity;
1767 if (qAbs(t: pixelVelocity) > _q_MinimumFlickVelocity) {
1768 if (qAbs(t: pixelVelocity) > maximumFlickVelocity || snapMode == QQuickPathView::SnapOneItem) {
1769 // limit velocity
1770 qreal maxVel = velocity < 0 ? -maximumFlickVelocity : maximumFlickVelocity;
1771 velocity = maxVel / averageItemLength;
1772 }
1773 // Calculate the distance to be travelled
1774 qreal v2 = velocity*velocity;
1775 qreal accel = deceleration/10;
1776 qreal dist = 0;
1777 if (haveHighlightRange && (highlightRangeMode == QQuickPathView::StrictlyEnforceRange
1778 || snapMode != QQuickPathView::NoSnap)) {
1779 if (snapMode == QQuickPathView::SnapOneItem) {
1780 // encourage snapping one item in direction of motion
1781 if (velocity > 0)
1782 dist = qRound(d: 0.5 + offset) - offset;
1783 else
1784 dist = qRound(d: 0.5 - offset) + offset;
1785 } else {
1786 // + 0.25 to encourage moving at least one item in the flick direction
1787 dist = qMin(a: qreal(modelCount-1), b: qreal(v2 / (accel * 2) + 0.25));
1788
1789 // round to nearest item.
1790 if (velocity > 0)
1791 dist = qRound(d: dist + offset) - offset;
1792 else
1793 dist = qRound(d: dist - offset) + offset;
1794 }
1795 // Calculate accel required to stop on item boundary
1796 if (dist <= 0) {
1797 dist = 0;
1798 accel = 0;
1799 } else {
1800 accel = v2 / (2 * qAbs(t: dist));
1801 }
1802 } else {
1803 dist = qMin(a: qreal(modelCount-1), b: qreal(v2 / (accel * 2)));
1804 }
1805 flickDuration = int(1000 * qAbs(t: velocity) / accel);
1806 offsetAdj = 0;
1807 moveOffset.setValue(offset);
1808 tl.accel(moveOffset, velocity, accel, maxDistance: dist);
1809 tl.callback(QQuickTimeLineCallback(&moveOffset, fixOffsetCallback, this));
1810 if (!flicking) {
1811 flicking = true;
1812 emit q->flickingChanged();
1813 emit q->flickStarted();
1814 }
1815 } else {
1816 fixOffset();
1817 }
1818
1819 timer.invalidate();
1820 if (!tl.isActive())
1821 q->movementEnding();
1822}
1823
1824bool QQuickPathView::childMouseEventFilter(QQuickItem *i, QEvent *e)
1825{
1826 Q_D(QQuickPathView);
1827 if (!isVisible() || !d->interactive || !e->isPointerEvent())
1828 return QQuickItem::childMouseEventFilter(i, e);
1829
1830 QPointerEvent *pe = static_cast<QPointerEvent *>(e);
1831 if (QQuickDeliveryAgentPrivate::isMouseEvent(ev: pe)) {
1832 // The event is localized for the intended receiver (in the delegate, probably),
1833 // but we need to look at position relative to the PathView itself.
1834 const auto &point = pe->points().first();
1835 QPointF localPos = mapFromScene(point: point.scenePosition());
1836 QQuickItem *grabber = qmlobject_cast<QQuickItem *>(object: pe->exclusiveGrabber(point));
1837 if (grabber == this && d->stealMouse) {
1838 // we are already the grabber and we do want the mouse event to ourselves.
1839 return true;
1840 }
1841
1842 bool grabberDisabled = grabber && !grabber->isEnabled();
1843 bool stealThisEvent = d->stealMouse;
1844 if ((stealThisEvent || contains(point: localPos)) && (!grabber || !grabber->keepMouseGrab() || grabberDisabled)) {
1845 // Make a localized copy of the QMouseEvent.
1846 QMutableSinglePointEvent localizedEvent(*static_cast<QMouseEvent *>(pe));
1847 QMutableEventPoint::setPosition(p&: localizedEvent.point(i: 0), arg: localPos);
1848 localizedEvent.setAccepted(false);
1849
1850 switch (localizedEvent.type()) {
1851 case QEvent::MouseMove:
1852 d->handleMouseMoveEvent(event: static_cast<QMouseEvent *>(static_cast<QSinglePointEvent *>(&localizedEvent)));
1853 break;
1854 case QEvent::MouseButtonPress:
1855 d->handleMousePressEvent(event: static_cast<QMouseEvent *>(static_cast<QSinglePointEvent *>(&localizedEvent)));
1856 stealThisEvent = d->stealMouse; // Update stealThisEvent in case changed by function call above
1857 break;
1858 case QEvent::MouseButtonRelease:
1859 d->handleMouseReleaseEvent(event: static_cast<QMouseEvent *>(static_cast<QSinglePointEvent *>(&localizedEvent)));
1860 break;
1861 default:
1862 break;
1863 }
1864
1865 grabber = qmlobject_cast<QQuickItem *>(object: localizedEvent.exclusiveGrabber(point: localizedEvent.points().first()));
1866 if ((grabber && stealThisEvent && !grabber->keepMouseGrab() && grabber != this) || grabberDisabled)
1867 pe->setExclusiveGrabber(point, exclusiveGrabber: this);
1868
1869 const bool filtered = stealThisEvent || grabberDisabled;
1870 if (filtered)
1871 pe->setAccepted(stealThisEvent && grabber == this && grabber->isEnabled());
1872
1873 return filtered;
1874 } else if (d->timer.isValid()) {
1875 d->timer.invalidate();
1876 d->fixOffset();
1877 }
1878 if (pe->type() == QEvent::MouseButtonRelease || (grabber && grabber->keepMouseGrab() && !grabberDisabled))
1879 d->stealMouse = false;
1880 return false;
1881 }
1882
1883 return QQuickItem::childMouseEventFilter(i, e);
1884}
1885
1886void QQuickPathView::mouseUngrabEvent()
1887{
1888 Q_D(QQuickPathView);
1889 if (d->stealMouse ||
1890 (!d->flicking && d->snapMode != NoSnap && !qFuzzyCompare(p1: qRound(d: d->offset), p2: d->offset))) {
1891 // if our mouse grab has been removed (probably by a Flickable),
1892 // or if we should snap but haven't done it, fix our state
1893 d->stealMouse = false;
1894 setKeepMouseGrab(false);
1895 d->timer.invalidate();
1896 d->fixOffset();
1897 d->setDragging(false);
1898 if (!d->tl.isActive())
1899 movementEnding();
1900 }
1901}
1902
1903void QQuickPathView::updatePolish()
1904{
1905 QQuickItem::updatePolish();
1906 refill();
1907}
1908
1909static inline int currentIndexRemainder(int currentIndex, int modelCount) noexcept
1910{
1911 if (currentIndex < 0)
1912 return modelCount + currentIndex % modelCount;
1913 else
1914 return currentIndex % modelCount;
1915}
1916
1917void QQuickPathView::componentComplete()
1918{
1919 Q_D(QQuickPathView);
1920 if (d->model && d->ownModel)
1921 static_cast<QQmlDelegateModel *>(d->model.data())->componentComplete();
1922
1923 QQuickItem::componentComplete();
1924
1925 if (d->model) {
1926 d->modelCount = d->model->count();
1927 if (d->modelCount && d->currentIndex != 0) // an initial value has been provided for currentIndex
1928 d->offset = std::fmod(x: qreal(d->modelCount - currentIndexRemainder(currentIndex: d->currentIndex, modelCount: d->modelCount)), y: qreal(d->modelCount));
1929 }
1930
1931 d->createHighlight();
1932 d->regenerate();
1933 d->updateHighlight();
1934 d->updateCurrent();
1935
1936 if (d->modelCount)
1937 emit countChanged();
1938}
1939
1940void QQuickPathView::refill()
1941{
1942 Q_D(QQuickPathView);
1943
1944 if (d->inRefill) {
1945 d->scheduleLayout();
1946 return;
1947 }
1948
1949 d->layoutScheduled = false;
1950
1951 if (!d->isValid() || !isComponentComplete())
1952 return;
1953
1954 d->inRefill = true;
1955
1956 bool currentVisible = false;
1957 int count = d->pathItems == -1 ? d->modelCount : qMin(a: d->pathItems, b: d->modelCount);
1958
1959 // first move existing items and remove items off path
1960 qCDebug(lcItemViewDelegateLifecycle) << "currentIndex" << d->currentIndex << "offset" << d->offset;
1961 QList<QQuickItem*>::iterator it = d->items.begin();
1962 while (it != d->items.end()) {
1963 QQuickItem *item = *it;
1964 int idx = d->model->indexOf(object: item, objectContext: nullptr);
1965 qreal pos = d->positionOfIndex(index: idx);
1966 if (lcItemViewDelegateLifecycle().isDebugEnabled()) {
1967 QQuickText *text = qmlobject_cast<QQuickText*>(object: item);
1968 if (text)
1969 qCDebug(lcItemViewDelegateLifecycle) << "idx" << idx << "@" << pos << ": QQuickText" << text->objectName() << QStringView{text->text()}.left(n: 40);
1970 else
1971 qCDebug(lcItemViewDelegateLifecycle) << "idx" << idx << "@" << pos << ":" << item;
1972 }
1973 if (pos < 1) {
1974 d->updateItem(item, percent: pos);
1975 if (idx == d->currentIndex) {
1976 currentVisible = true;
1977 d->currentItemOffset = pos;
1978 }
1979 ++it;
1980 } else {
1981 d->updateItem(item, percent: pos);
1982 if (QQuickPathViewAttached *att = d->attached(item))
1983 att->setOnPath(pos < 1);
1984 if (!d->isInBound(position: pos, lower: d->mappedRange - d->mappedCache, upper: 1 + d->mappedCache)) {
1985 qCDebug(lcItemViewDelegateLifecycle) << "release" << idx << "@" << pos << ", !isInBound: lower" << (d->mappedRange - d->mappedCache) << "upper" << (1 + d->mappedCache);
1986 d->releaseItem(item);
1987 it = d->items.erase(pos: it);
1988 } else {
1989 ++it;
1990 }
1991 }
1992 }
1993
1994 bool waiting = false;
1995 if (d->modelCount) {
1996 // add items as needed
1997 if (d->items.size() < count+d->cacheSize) {
1998 int endIdx = 0;
1999 qreal endPos;
2000 int startIdx = 0;
2001 qreal startPos = 0;
2002 const bool wasEmpty = d->items.isEmpty();
2003 if (!wasEmpty) {
2004 //Find the beginning and end, items may not be in sorted order
2005 endPos = -1;
2006 startPos = 2;
2007
2008 for (QQuickItem * item : std::as_const(t&: d->items)) {
2009 int idx = d->model->indexOf(object: item, objectContext: nullptr);
2010 qreal curPos = d->positionOfIndex(index: idx);
2011 if (curPos > endPos) {
2012 endPos = curPos;
2013 endIdx = idx;
2014 }
2015
2016 if (curPos < startPos) {
2017 startPos = curPos;
2018 startIdx = idx;
2019 }
2020 }
2021 } else {
2022 if (d->haveHighlightRange
2023 && (d->highlightRangeMode != QQuickPathView::NoHighlightRange
2024 || d->snapMode != QQuickPathView::NoSnap))
2025 startPos = d->highlightRangeStart;
2026 // With no items, then "end" is just off the top so we populate via append
2027 endIdx = (qRound(d: d->modelCount - d->offset) - 1) % d->modelCount;
2028 endIdx = qMax(a: -1, b: endIdx); // endIdx shouldn't be smaller than -1
2029 endPos = d->positionOfIndex(index: endIdx);
2030 }
2031 //Append
2032 int idx = endIdx + 1;
2033 if (idx >= d->modelCount)
2034 idx = 0;
2035 qreal nextPos = d->positionOfIndex(index: idx);
2036 while ((d->isInBound(position: nextPos, lower: endPos, upper: 1 + d->mappedCache, emptyRangeCheck: false) || !d->items.size())
2037 && d->items.size() < count + d->cacheSize) {
2038 qCDebug(lcItemViewDelegateLifecycle) << "append" << idx << "@" << nextPos << (d->currentIndex == idx ? "current" : "") << "items count was" << d->items.size();
2039 QQuickItem *item = d->getItem(modelIndex: idx, z: idx+1, async: nextPos >= 1);
2040 if (!item) {
2041 waiting = true;
2042 break;
2043 }
2044 if (d->items.contains(t: item)) {
2045 d->releaseItem(item);
2046 break; //Otherwise we'd "re-add" it, and get confused
2047 }
2048 if (d->currentIndex == idx) {
2049 currentVisible = true;
2050 d->currentItemOffset = nextPos;
2051 }
2052 d->items.append(t: item);
2053 d->updateItem(item, percent: nextPos);
2054 endIdx = idx;
2055 endPos = nextPos;
2056 ++idx;
2057 if (idx >= d->modelCount)
2058 idx = 0;
2059 nextPos = d->positionOfIndex(index: idx);
2060 }
2061
2062 //Prepend
2063 idx = (wasEmpty ? d->calcCurrentIndex() : startIdx) - 1;
2064
2065 if (idx < 0)
2066 idx = d->modelCount - 1;
2067 nextPos = d->positionOfIndex(index: idx);
2068 while (!waiting && d->isInBound(position: nextPos, lower: d->mappedRange - d->mappedCache, upper: startPos)
2069 && d->items.size() < count+d->cacheSize) {
2070 qCDebug(lcItemViewDelegateLifecycle) << "prepend" << idx << "@" << nextPos << (d->currentIndex == idx ? "current" : "") << "items count was" << d->items.size();
2071 QQuickItem *item = d->getItem(modelIndex: idx, z: idx+1, async: nextPos >= 1);
2072 if (!item) {
2073 waiting = true;
2074 break;
2075 }
2076 if (d->items.contains(t: item)) {
2077 d->releaseItem(item);
2078 break; //Otherwise we'd "re-add" it, and get confused
2079 }
2080 if (d->currentIndex == idx) {
2081 currentVisible = true;
2082 d->currentItemOffset = nextPos;
2083 }
2084 d->items.prepend(t: item);
2085 d->updateItem(item, percent: nextPos);
2086 startIdx = idx;
2087 startPos = nextPos;
2088 --idx;
2089 if (idx < 0)
2090 idx = d->modelCount - 1;
2091 nextPos = d->positionOfIndex(index: idx);
2092 }
2093
2094 // In rare cases, when jumping around with pathCount close to modelCount,
2095 // new items appear in the middle. This more generic addition iteration handles this
2096 // Since this is the rare case, we try append/prepend first and only do this if
2097 // there are gaps still left to fill.
2098 if (!waiting && d->items.size() < count+d->cacheSize) {
2099 qCDebug(lcItemViewDelegateLifecycle) << "Checking for pathview middle inserts, items count was" << d->items.size();
2100 idx = startIdx;
2101 QQuickItem *lastItem = d->items.at(i: 0);
2102 while (idx != endIdx) {
2103 nextPos = d->positionOfIndex(index: idx);
2104 if (d->isInBound(position: nextPos, lower: d->mappedRange - d->mappedCache, upper: 1 + d->mappedCache)) {
2105 //This gets the reference from the delegate model, and will not re-create
2106 QQuickItem *item = d->getItem(modelIndex: idx, z: idx+1, async: nextPos >= 1);
2107 if (!item) {
2108 waiting = true;
2109 break;
2110 }
2111
2112 if (!d->items.contains(t: item)) { //We found a hole
2113 qCDebug(lcItemViewDelegateLifecycle) << "middle insert" << idx << "@" << nextPos
2114 << (d->currentIndex == idx ? "current" : "")
2115 << "items count was" << d->items.size();
2116 if (d->currentIndex == idx) {
2117 currentVisible = true;
2118 d->currentItemOffset = nextPos;
2119 }
2120 int lastListIdx = d->items.indexOf(t: lastItem);
2121 d->items.insert(i: lastListIdx + 1, t: item);
2122 d->updateItem(item, percent: nextPos);
2123 } else {
2124 d->releaseItem(item);
2125 }
2126
2127 lastItem = item;
2128 }
2129
2130 ++idx;
2131 if (idx >= d->modelCount)
2132 idx = 0;
2133 }
2134 }
2135 }
2136 }
2137
2138 bool currentChanged = false;
2139 if (!currentVisible) {
2140 d->currentItemOffset = 1;
2141 if (d->currentItem) {
2142 d->updateItem(item: d->currentItem, percent: 1);
2143 } else if (!waiting && d->currentIndex >= 0 && d->currentIndex < d->modelCount) {
2144 if ((d->currentItem = d->getItem(modelIndex: d->currentIndex, z: d->currentIndex))) {
2145 currentChanged = true;
2146 d->updateItem(item: d->currentItem, percent: 1);
2147 if (QQuickPathViewAttached *att = d->attached(item: d->currentItem))
2148 att->setIsCurrentItem(true);
2149 }
2150 }
2151 } else if (!waiting && !d->currentItem) {
2152 if ((d->currentItem = d->getItem(modelIndex: d->currentIndex, z: d->currentIndex))) {
2153 currentChanged = true;
2154 d->currentItem->setFocus(true);
2155 if (QQuickPathViewAttached *att = d->attached(item: d->currentItem))
2156 att->setIsCurrentItem(true);
2157 }
2158 }
2159
2160 if (d->highlightItem && d->haveHighlightRange && d->highlightRangeMode == QQuickPathView::StrictlyEnforceRange) {
2161 d->updateItem(item: d->highlightItem, percent: d->highlightRangeStart);
2162 if (QQuickPathViewAttached *att = d->attached(item: d->highlightItem))
2163 att->setOnPath(true);
2164 } else if (d->highlightItem && d->moveReason != QQuickPathViewPrivate::SetIndex) {
2165 d->updateItem(item: d->highlightItem, percent: d->currentItemOffset);
2166 if (QQuickPathViewAttached *att = d->attached(item: d->highlightItem))
2167 att->setOnPath(currentVisible);
2168 }
2169 for (QQuickItem *item : std::as_const(t&: d->itemCache))
2170 d->releaseItem(item);
2171 d->itemCache.clear();
2172
2173 d->inRefill = false;
2174 if (currentChanged)
2175 emit currentItemChanged();
2176}
2177
2178void QQuickPathView::modelUpdated(const QQmlChangeSet &changeSet, bool reset)
2179{
2180 Q_D(QQuickPathView);
2181 if (!d->model || !d->model->isValid() || !d->path || !isComponentComplete())
2182 return;
2183
2184 if (reset) {
2185 d->modelCount = d->model->count();
2186 d->regenerate();
2187 emit countChanged();
2188 return;
2189 }
2190
2191 if (changeSet.removes().isEmpty() && changeSet.inserts().isEmpty())
2192 return;
2193
2194 const int modelCount = d->modelCount;
2195 int moveId = -1;
2196 int moveOffset = 0;
2197 bool currentChanged = false;
2198 bool changedOffset = false;
2199 for (const QQmlChangeSet::Change &r : changeSet.removes()) {
2200 if (moveId == -1 && d->currentIndex >= r.index + r.count) {
2201 d->currentIndex -= r.count;
2202 currentChanged = true;
2203 } else if (moveId == -1 && d->currentIndex >= r.index && d->currentIndex < r.index + r.count) {
2204 // current item has been removed.
2205 if (r.isMove()) {
2206 moveId = r.moveId;
2207 moveOffset = d->currentIndex - r.index;
2208 } else if (d->currentItem) {
2209 if (QQuickPathViewAttached *att = d->attached(item: d->currentItem))
2210 att->setIsCurrentItem(true);
2211 d->releaseCurrentItem();
2212 }
2213 d->currentIndex = qMin(a: r.index, b: d->modelCount - r.count - 1);
2214 currentChanged = true;
2215 }
2216
2217 if (r.index > d->currentIndex) {
2218 changedOffset = true;
2219 d->offset -= r.count;
2220 d->offsetAdj -= r.count;
2221 }
2222 d->modelCount -= r.count;
2223 }
2224 for (const QQmlChangeSet::Change &i : changeSet.inserts()) {
2225 if (d->modelCount) {
2226 if (moveId == -1 && i.index <= d->currentIndex) {
2227 d->currentIndex += i.count;
2228 currentChanged = true;
2229 } else {
2230 if (moveId != -1 && moveId == i.moveId) {
2231 d->currentIndex = i.index + moveOffset;
2232 currentChanged = true;
2233 }
2234 if (i.index > d->currentIndex) {
2235 d->offset += i.count;
2236 d->offsetAdj += i.count;
2237 changedOffset = true;
2238 }
2239 }
2240 }
2241 d->modelCount += i.count;
2242 }
2243
2244 d->offset = std::fmod(x: d->offset, y: qreal(d->modelCount));
2245 if (d->offset < 0)
2246 d->offset += d->modelCount;
2247 if (d->currentIndex == -1)
2248 d->currentIndex = d->calcCurrentIndex();
2249
2250 d->itemCache += d->items;
2251 d->items.clear();
2252
2253 if (!d->modelCount) {
2254 for (QQuickItem * item : std::as_const(t&: d->itemCache))
2255 d->releaseItem(item);
2256 d->itemCache.clear();
2257 d->offset = 0;
2258 changedOffset = true;
2259 d->tl.reset(d->moveOffset);
2260 } else {
2261 if (!d->flicking && !d->moving && d->haveHighlightRange && d->highlightRangeMode == QQuickPathView::StrictlyEnforceRange) {
2262 d->offset = std::fmod(x: qreal(d->modelCount - d->currentIndex), y: qreal(d->modelCount));
2263 changedOffset = true;
2264 }
2265 d->updateMappedRange();
2266 d->scheduleLayout();
2267 }
2268 if (changedOffset)
2269 emit offsetChanged();
2270 if (currentChanged)
2271 emit currentIndexChanged();
2272 if (d->modelCount != modelCount)
2273 emit countChanged();
2274}
2275
2276void QQuickPathView::destroyingItem(QObject *item)
2277{
2278 Q_UNUSED(item);
2279}
2280
2281void QQuickPathView::ticked()
2282{
2283 Q_D(QQuickPathView);
2284 d->updateCurrent();
2285}
2286
2287void QQuickPathView::movementEnding()
2288{
2289 Q_D(QQuickPathView);
2290 if (d->flicking) {
2291 d->flicking = false;
2292 emit flickingChanged();
2293 emit flickEnded();
2294 }
2295 if (d->moving && !d->stealMouse) {
2296 d->moving = false;
2297 emit movingChanged();
2298 emit movementEnded();
2299 }
2300 d->moveDirection = d->movementDirection;
2301}
2302
2303// find the item closest to the snap position
2304int QQuickPathViewPrivate::calcCurrentIndex()
2305{
2306 int current = 0;
2307 if (modelCount && model && items.size()) {
2308 offset = std::fmod(x: offset, y: qreal(modelCount));
2309 if (offset < 0)
2310 offset += modelCount;
2311 current = qRound(d: qAbs(t: std::fmod(x: modelCount - offset, y: qreal(modelCount))));
2312 current = current % modelCount;
2313 }
2314
2315 return current;
2316}
2317
2318void QQuickPathViewPrivate::createCurrentItem()
2319{
2320 if (requestedIndex != -1)
2321 return;
2322
2323 bool inItems = false;
2324 for (QQuickItem *item : std::as_const(t&: items)) {
2325 if (model->indexOf(object: item, objectContext: nullptr) == currentIndex) {
2326 inItems = true;
2327 break;
2328 }
2329 }
2330
2331 if (inItems) {
2332 if ((currentItem = getItem(modelIndex: currentIndex, z: currentIndex))) {
2333 currentItem->setFocus(true);
2334 if (QQuickPathViewAttached *att = attached(item: currentItem))
2335 att->setIsCurrentItem(true);
2336 }
2337 } else if (currentIndex >= 0 && currentIndex < modelCount) {
2338 if ((currentItem = getItem(modelIndex: currentIndex, z: currentIndex))) {
2339 updateItem(item: currentItem, percent: 1);
2340 if (QQuickPathViewAttached *att = attached(item: currentItem))
2341 att->setIsCurrentItem(true);
2342 }
2343 }
2344}
2345
2346void QQuickPathViewPrivate::updateCurrent()
2347{
2348 Q_Q(QQuickPathView);
2349 if (moveReason == SetIndex)
2350 return;
2351 if (!modelCount || !haveHighlightRange || highlightRangeMode != QQuickPathView::StrictlyEnforceRange)
2352 return;
2353
2354 int idx = calcCurrentIndex();
2355 if (model && (idx != currentIndex || !currentItem)) {
2356 if (currentItem) {
2357 if (QQuickPathViewAttached *att = attached(item: currentItem))
2358 att->setIsCurrentItem(false);
2359 releaseCurrentItem();
2360 }
2361 int oldCurrentIndex = currentIndex;
2362 currentIndex = idx;
2363 currentItem = nullptr;
2364 createCurrentItem();
2365 if (oldCurrentIndex != currentIndex)
2366 emit q->currentIndexChanged();
2367 emit q->currentItemChanged();
2368 }
2369}
2370
2371void QQuickPathViewPrivate::fixOffsetCallback(void *d)
2372{
2373 static_cast<QQuickPathViewPrivate *>(d)->fixOffset();
2374}
2375
2376void QQuickPathViewPrivate::fixOffset()
2377{
2378 Q_Q(QQuickPathView);
2379 if (model && items.size()) {
2380 if (haveHighlightRange && (highlightRangeMode == QQuickPathView::StrictlyEnforceRange
2381 || snapMode != QQuickPathView::NoSnap)) {
2382 int curr = calcCurrentIndex();
2383 if (curr != currentIndex && highlightRangeMode == QQuickPathView::StrictlyEnforceRange)
2384 q->setCurrentIndex(curr);
2385 else
2386 snapToIndex(index: curr, reason: Other);
2387 }
2388 }
2389}
2390
2391void QQuickPathViewPrivate::snapToIndex(int index, MovementReason reason)
2392{
2393 if (!model || modelCount <= 0)
2394 return;
2395
2396 qreal targetOffset = std::fmod(x: qreal(modelCount - index), y: qreal(modelCount));
2397 moveReason = reason;
2398 offsetAdj = 0;
2399 tl.reset(moveOffset);
2400 moveOffset.setValue(offset);
2401
2402 const int duration = highlightMoveDuration;
2403
2404 const qreal count = pathItems == -1 ? modelCount : qMin(a: pathItems, b: modelCount);
2405 const qreal averageItemLength = path->path().length() / count;
2406 const qreal threshold = 0.5 / averageItemLength; // if we are within .5 px, we want to immediately assign rather than animate
2407
2408 if (!duration || qAbs(t: offset - targetOffset) < threshold || (qFuzzyIsNull(d: targetOffset) && qAbs(t: modelCount - offset) < threshold)) {
2409 tl.set(moveOffset, targetOffset);
2410 } else if (moveDirection == QQuickPathView::Positive || (moveDirection == QQuickPathView::Shortest && targetOffset - offset > modelCount/2)) {
2411 qreal distance = modelCount - targetOffset + offset;
2412 if (targetOffset > moveOffset) {
2413 tl.move(moveOffset, destination: 0, QEasingCurve(QEasingCurve::InQuad), time: int(duration * offset / distance));
2414 tl.set(moveOffset, modelCount);
2415 tl.move(moveOffset, destination: targetOffset, QEasingCurve(qFuzzyIsNull(d: offset) ? QEasingCurve::InOutQuad : QEasingCurve::OutQuad), time: int(duration * (modelCount-targetOffset) / distance));
2416 } else {
2417 tl.move(moveOffset, destination: targetOffset, QEasingCurve(QEasingCurve::InOutQuad), time: duration);
2418 }
2419 } else if (moveDirection == QQuickPathView::Negative || targetOffset - offset <= -modelCount/2) {
2420 qreal distance = modelCount - offset + targetOffset;
2421 if (targetOffset < moveOffset) {
2422 tl.move(moveOffset, destination: modelCount, QEasingCurve(qFuzzyIsNull(d: targetOffset) ? QEasingCurve::InOutQuad : QEasingCurve::InQuad), time: int(duration * (modelCount-offset) / distance));
2423 tl.set(moveOffset, 0);
2424 tl.move(moveOffset, destination: targetOffset, QEasingCurve(QEasingCurve::OutQuad), time: int(duration * targetOffset / distance));
2425 } else {
2426 tl.move(moveOffset, destination: targetOffset, QEasingCurve(QEasingCurve::InOutQuad), time: duration);
2427 }
2428 } else {
2429 tl.move(moveOffset, destination: targetOffset, QEasingCurve(QEasingCurve::InOutQuad), time: duration);
2430 }
2431}
2432
2433QQuickPathViewAttached *QQuickPathView::qmlAttachedProperties(QObject *obj)
2434{
2435 return new QQuickPathViewAttached(obj);
2436}
2437
2438QT_END_NAMESPACE
2439
2440#include "moc_qquickpathview_p.cpp"
2441

source code of qtdeclarative/src/quick/items/qquickpathview.cpp