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