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

Provided by KDAB

Privacy Policy
Learn to use CMake with our Intro Training
Find out more

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