1/*
2 * SPDX-FileCopyrightText: 2019 Marco Martin <mart@kde.org>
3 *
4 * SPDX-License-Identifier: LGPL-2.0-or-later
5 */
6
7#include "columnview.h"
8#include "columnview_p.h"
9
10#include "loggingcategory.h"
11#include <QAbstractItemModel>
12#include <QGuiApplication>
13#include <QPropertyAnimation>
14#include <QQmlComponent>
15#include <QQmlContext>
16#include <QQmlEngine>
17#include <QStyleHints>
18
19#include "platform/units.h"
20
21class QmlComponentsPoolSingleton
22{
23public:
24 QmlComponentsPoolSingleton()
25 {
26 }
27 static QmlComponentsPool *instance(QQmlEngine *engine);
28
29private:
30 QHash<QQmlEngine *, QmlComponentsPool *> m_instances;
31};
32
33Q_GLOBAL_STATIC(QmlComponentsPoolSingleton, privateQmlComponentsPoolSelf)
34
35QmlComponentsPool *QmlComponentsPoolSingleton::instance(QQmlEngine *engine)
36{
37 Q_ASSERT(engine);
38 auto componentPool = privateQmlComponentsPoolSelf->m_instances.value(key: engine);
39
40 if (componentPool) {
41 return componentPool;
42 }
43
44 componentPool = new QmlComponentsPool(engine);
45
46 const auto removePool = [engine]() {
47 // NB: do not derefence engine. it may be dangling already!
48 if (privateQmlComponentsPoolSelf) {
49 privateQmlComponentsPoolSelf->m_instances.remove(key: engine);
50 }
51 };
52 QObject::connect(sender: engine, signal: &QObject::destroyed, context: engine, slot: removePool);
53 QObject::connect(sender: componentPool, signal: &QObject::destroyed, context: componentPool, slot: removePool);
54
55 privateQmlComponentsPoolSelf->m_instances[engine] = componentPool;
56 return componentPool;
57}
58
59QmlComponentsPool::QmlComponentsPool(QQmlEngine *engine)
60 : QObject(engine)
61{
62 QQmlComponent component(engine);
63
64 /* clang-format off */
65 component.setData(QByteArrayLiteral(R"(
66import QtQuick
67import org.kde.kirigami as Kirigami
68
69QtObject {
70 readonly property Component leadingSeparator: Kirigami.Separator {
71 property Item column
72 property bool inToolBar
73 property Kirigami.ColumnView view
74
75 // positioning trick to hide the very first separator
76 visible: {
77 if (!view || !view.separatorVisible) {
78 return false;
79 }
80
81 return view && (LayoutMirroring.enabled
82 ? view.contentX + view.width > column.x + column.width
83 : view.contentX < column.x);
84 }
85
86 anchors.top: column.top
87 anchors.left: column.left
88 anchors.bottom: column.bottom
89 anchors.topMargin: inToolBar ? Kirigami.Units.largeSpacing : 0
90 anchors.bottomMargin: inToolBar ? Kirigami.Units.largeSpacing : 0
91 Kirigami.Theme.colorSet: Kirigami.Theme.Header
92 Kirigami.Theme.inherit: false
93 }
94
95 readonly property Component trailingSeparator: Kirigami.Separator {
96 property Item column
97
98 anchors.top: column.top
99 anchors.right: column.right
100 anchors.bottom: column.bottom
101 Kirigami.Theme.colorSet: Kirigami.Theme.Header
102 Kirigami.Theme.inherit: false
103 }
104}
105)"), baseUrl: QUrl(QStringLiteral("columnview.cpp")));
106 /* clang-format on */
107
108 m_instance = component.create();
109 // qCWarning(KirigamiLog)<<component.errors();
110 Q_ASSERT(m_instance);
111 m_instance->setParent(this);
112
113 m_leadingSeparatorComponent = m_instance->property(name: "leadingSeparator").value<QQmlComponent *>();
114 Q_ASSERT(m_leadingSeparatorComponent);
115
116 m_trailingSeparatorComponent = m_instance->property(name: "trailingSeparator").value<QQmlComponent *>();
117 Q_ASSERT(m_trailingSeparatorComponent);
118
119 m_units = engine->singletonInstance<Kirigami::Platform::Units *>(uri: "org.kde.kirigami.platform", typeName: "Units");
120 Q_ASSERT(m_units);
121
122 connect(sender: m_units, signal: &Kirigami::Platform::Units::gridUnitChanged, context: this, slot: &QmlComponentsPool::gridUnitChanged);
123 connect(sender: m_units, signal: &Kirigami::Platform::Units::longDurationChanged, context: this, slot: &QmlComponentsPool::longDurationChanged);
124}
125
126QmlComponentsPool::~QmlComponentsPool()
127{
128}
129
130/////////
131
132ColumnViewAttached::ColumnViewAttached(QObject *parent)
133 : QObject(parent)
134{
135}
136
137ColumnViewAttached::~ColumnViewAttached()
138{
139}
140
141void ColumnViewAttached::setIndex(int index)
142{
143 if (!m_customFillWidth && m_view) {
144 const bool oldFillWidth = m_fillWidth;
145 m_fillWidth = index == m_view->count() - 1;
146 if (oldFillWidth != m_fillWidth) {
147 Q_EMIT fillWidthChanged();
148 }
149 }
150
151 if (index == m_index) {
152 return;
153 }
154
155 m_index = index;
156 Q_EMIT indexChanged();
157}
158
159int ColumnViewAttached::index() const
160{
161 return m_index;
162}
163
164void ColumnViewAttached::setFillWidth(bool fill)
165{
166 if (m_view) {
167 disconnect(sender: m_view.data(), signal: &ColumnView::countChanged, receiver: this, zero: nullptr);
168 }
169 m_customFillWidth = true;
170
171 if (fill == m_fillWidth) {
172 return;
173 }
174
175 m_fillWidth = fill;
176 Q_EMIT fillWidthChanged();
177
178 if (m_view) {
179 m_view->polish();
180 }
181}
182
183bool ColumnViewAttached::fillWidth() const
184{
185 return m_fillWidth;
186}
187
188qreal ColumnViewAttached::reservedSpace() const
189{
190 return m_reservedSpace;
191}
192
193void ColumnViewAttached::setReservedSpace(qreal space)
194{
195 if (m_view) {
196 disconnect(sender: m_view.data(), signal: &ColumnView::columnWidthChanged, receiver: this, zero: nullptr);
197 }
198 m_customReservedSpace = true;
199
200 if (qFuzzyCompare(p1: space, p2: m_reservedSpace)) {
201 return;
202 }
203
204 m_reservedSpace = space;
205 Q_EMIT reservedSpaceChanged();
206
207 if (m_view) {
208 m_view->polish();
209 }
210}
211
212ColumnView *ColumnViewAttached::view()
213{
214 return m_view;
215}
216
217void ColumnViewAttached::setView(ColumnView *view)
218{
219 if (view == m_view) {
220 return;
221 }
222
223 if (m_view) {
224 disconnect(sender: m_view.data(), signal: nullptr, receiver: this, member: nullptr);
225 }
226 m_view = view;
227
228 if (!m_customFillWidth && m_view) {
229 m_fillWidth = m_index == m_view->count() - 1;
230 connect(sender: m_view.data(), signal: &ColumnView::countChanged, context: this, slot: [this]() {
231 m_fillWidth = m_index == m_view->count() - 1;
232 Q_EMIT fillWidthChanged();
233 });
234 }
235 if (!m_customReservedSpace && m_view) {
236 m_reservedSpace = m_view->columnWidth();
237 connect(sender: m_view.data(), signal: &ColumnView::columnWidthChanged, context: this, slot: [this]() {
238 m_reservedSpace = m_view->columnWidth();
239 Q_EMIT reservedSpaceChanged();
240 });
241 }
242
243 Q_EMIT viewChanged();
244}
245
246QQuickItem *ColumnViewAttached::originalParent() const
247{
248 return m_originalParent;
249}
250
251void ColumnViewAttached::setOriginalParent(QQuickItem *parent)
252{
253 m_originalParent = parent;
254}
255
256bool ColumnViewAttached::shouldDeleteOnRemove() const
257{
258 return m_shouldDeleteOnRemove;
259}
260
261void ColumnViewAttached::setShouldDeleteOnRemove(bool del)
262{
263 m_shouldDeleteOnRemove = del;
264}
265
266bool ColumnViewAttached::preventStealing() const
267{
268 return m_preventStealing;
269}
270
271void ColumnViewAttached::setPreventStealing(bool prevent)
272{
273 if (prevent == m_preventStealing) {
274 return;
275 }
276
277 m_preventStealing = prevent;
278 Q_EMIT preventStealingChanged();
279}
280
281bool ColumnViewAttached::isPinned() const
282{
283 return m_pinned;
284}
285
286void ColumnViewAttached::setPinned(bool pinned)
287{
288 if (pinned == m_pinned) {
289 return;
290 }
291
292 m_pinned = pinned;
293
294 Q_EMIT pinnedChanged();
295
296 if (m_view) {
297 m_view->polish();
298 }
299}
300
301bool ColumnViewAttached::inViewport() const
302{
303 return m_inViewport;
304}
305
306void ColumnViewAttached::setInViewport(bool inViewport)
307{
308 if (m_inViewport == inViewport) {
309 return;
310 }
311
312 m_inViewport = inViewport;
313
314 Q_EMIT inViewportChanged();
315}
316
317QQuickItem *ColumnViewAttached::globalHeader() const
318{
319 return m_globalHeader;
320}
321
322void ColumnViewAttached::setGlobalHeader(QQuickItem *header)
323{
324 if (header == m_globalHeader) {
325 return;
326 }
327
328 QQuickItem *oldHeader = m_globalHeader;
329 if (m_globalHeader) {
330 disconnect(sender: m_globalHeader, signal: nullptr, receiver: this, member: nullptr);
331 }
332
333 m_globalHeader = header;
334
335 connect(sender: header, signal: &QObject::destroyed, context: this, slot: [this, header]() {
336 globalHeaderChanged(oldHeader: header, newHeader: nullptr);
337 });
338
339 Q_EMIT globalHeaderChanged(oldHeader, newHeader: header);
340}
341
342QQuickItem *ColumnViewAttached::globalFooter() const
343{
344 return m_globalFooter;
345}
346
347void ColumnViewAttached::setGlobalFooter(QQuickItem *footer)
348{
349 if (footer == m_globalFooter) {
350 return;
351 }
352
353 QQuickItem *oldFooter = m_globalFooter;
354 if (m_globalFooter) {
355 disconnect(sender: m_globalFooter, signal: nullptr, receiver: this, member: nullptr);
356 }
357
358 m_globalFooter = footer;
359
360 connect(sender: footer, signal: &QObject::destroyed, context: this, slot: [this, footer]() {
361 globalFooterChanged(oldFooter: footer, newFooter: nullptr);
362 });
363
364 Q_EMIT globalFooterChanged(oldFooter, newFooter: footer);
365}
366
367/////////
368
369ContentItem::ContentItem(ColumnView *parent)
370 : QQuickItem(parent)
371 , m_view(parent)
372{
373 m_globalHeaderParent = new QQuickItem(this);
374 m_globalFooterParent = new QQuickItem(this);
375
376 setFlags(flags() | ItemIsFocusScope);
377 m_slideAnim = new QPropertyAnimation(this);
378 m_slideAnim->setTargetObject(this);
379 m_slideAnim->setPropertyName("x");
380 // NOTE: the duration will be taken from kirigami units upon classBegin
381 m_slideAnim->setDuration(0);
382 m_slideAnim->setEasingCurve(QEasingCurve(QEasingCurve::OutExpo));
383 connect(sender: m_slideAnim, signal: &QPropertyAnimation::finished, context: this, slot: [this]() {
384 if (!m_view->currentItem()) {
385 m_view->setCurrentIndex(m_items.indexOf(t: m_viewAnchorItem));
386 } else {
387 QRectF mapped = m_view->currentItem()->mapRectToItem(item: m_view, rect: QRectF(QPointF(0, 0), m_view->currentItem()->size()));
388 if (!QRectF(QPointF(0, 0), m_view->size()).intersects(r: mapped)) {
389 m_view->setCurrentIndex(m_items.indexOf(t: m_viewAnchorItem));
390 }
391 }
392 });
393
394 connect(sender: this, signal: &QQuickItem::xChanged, context: this, slot: &ContentItem::layoutPinnedItems);
395 m_creationInProgress = false;
396}
397
398ContentItem::~ContentItem()
399{
400}
401
402void ContentItem::setBoundedX(qreal x)
403{
404 if (!parentItem()) {
405 return;
406 }
407 m_slideAnim->stop();
408 setX(qRound(d: qBound(min: qMin(a: 0.0, b: -width() + parentItem()->width()), val: x, max: 0.0)));
409}
410
411void ContentItem::animateX(qreal newX)
412{
413 if (!parentItem()) {
414 return;
415 }
416
417 const qreal to = qRound(d: qBound(min: qMin(a: 0.0, b: -width() + parentItem()->width()), val: newX, max: 0.0));
418
419 m_slideAnim->stop();
420 m_slideAnim->setStartValue(x());
421 m_slideAnim->setEndValue(to);
422 m_slideAnim->start();
423}
424
425void ContentItem::snapToItem()
426{
427 QQuickItem *firstItem = childAt(x: viewportLeft(), y: height() / 2);
428 if (!firstItem) {
429 return;
430 }
431 QQuickItem *nextItem = childAt(x: firstItem->x() + firstItem->width() + 1, y: height() / 2);
432
433 // need to make the last item visible?
434 if (nextItem && //
435 ((m_view->dragging() && m_lastDragDelta < 0) //
436 || (!m_view->dragging() //
437 && (width() - viewportRight()) < (viewportLeft() - firstItem->x())))) {
438 m_viewAnchorItem = nextItem;
439 animateX(newX: -nextItem->x() + m_leftPinnedSpace);
440
441 // The first one found?
442 } else if ((m_view->dragging() && m_lastDragDelta >= 0) //
443 || (!m_view->dragging() && (viewportLeft() <= (firstItem->x() + (firstItem->width() / 2)))) //
444 || !nextItem) {
445 m_viewAnchorItem = firstItem;
446 animateX(newX: -firstItem->x() + m_leftPinnedSpace);
447
448 // the second?
449 } else {
450 m_viewAnchorItem = nextItem;
451 animateX(newX: -nextItem->x() + m_leftPinnedSpace);
452 }
453}
454
455qreal ContentItem::viewportLeft() const
456{
457 return -x() + m_leftPinnedSpace;
458}
459
460qreal ContentItem::viewportRight() const
461{
462 return -x() + m_view->width() - m_rightPinnedSpace;
463}
464
465qreal ContentItem::childWidth(QQuickItem *child)
466{
467 if (!parentItem()) {
468 return 0.0;
469 }
470
471 ColumnViewAttached *attached = qobject_cast<ColumnViewAttached *>(object: qmlAttachedPropertiesObject<ColumnView>(obj: child, create: true));
472
473 if (m_columnResizeMode == ColumnView::SingleColumn) {
474 return qRound(d: parentItem()->width());
475
476 } else if (attached->fillWidth()) {
477 return qRound(d: qBound(min: m_columnWidth, val: (parentItem()->width() - attached->reservedSpace()), max: std::max(a: m_columnWidth, b: parentItem()->width())));
478
479 } else if (m_columnResizeMode == ColumnView::FixedColumns) {
480 return qRound(d: qMin(a: parentItem()->width(), b: m_columnWidth));
481
482 // DynamicColumns
483 } else {
484 // TODO:look for Layout size hints
485 qreal width = child->implicitWidth();
486
487 if (width < 1.0) {
488 width = m_columnWidth;
489 }
490
491 return qRound(d: qMin(a: m_view->width(), b: width));
492 }
493}
494
495void ContentItem::layoutItems()
496{
497 setY(m_view->topPadding());
498 setHeight(m_view->height() - m_view->topPadding() - m_view->bottomPadding());
499
500 qreal implicitWidth = 0;
501 qreal implicitHeight = 0;
502 qreal partialWidth = 0;
503 int i = 0;
504 m_leftPinnedSpace = 0;
505 m_rightPinnedSpace = 0;
506
507 bool reverse = qApp->layoutDirection() == Qt::RightToLeft;
508 auto it = !reverse ? m_items.begin() : m_items.end();
509 int increment = reverse ? -1 : +1;
510 auto lastPos = reverse ? m_items.begin() : m_items.end();
511
512 for (; it != lastPos; it += increment) {
513 // for (QQuickItem *child : std::as_const(m_items)) {
514 QQuickItem *child = reverse ? *(it - 1) : *it;
515 ColumnViewAttached *attached = qobject_cast<ColumnViewAttached *>(object: qmlAttachedPropertiesObject<ColumnView>(obj: child, create: true));
516 if (child == m_globalHeaderParent || child == m_globalFooterParent) {
517 continue;
518 }
519
520 if (child->isVisible()) {
521 if (attached->isPinned() && m_view->columnResizeMode() != ColumnView::SingleColumn) {
522 QQuickItem *sep = nullptr;
523 int sepWidth = 0;
524 if (m_view->separatorVisible()) {
525 sep = ensureTrailingSeparator(item: child);
526 sepWidth = (sep ? sep->width() : 0);
527 }
528 const qreal width = childWidth(child);
529 const qreal widthDiff = std::max(a: 0.0, b: m_view->width() - child->width()); // it's possible for the view width to be smaller than the child width
530 const qreal pageX = std::clamp(val: partialWidth, lo: -x(), hi: -x() + widthDiff);
531 qreal headerHeight = .0;
532 qreal footerHeight = .0;
533 if (QQuickItem *header = attached->globalHeader()) {
534 headerHeight = header->isVisible() ? header->height() : .0;
535 header->setWidth(width + sepWidth);
536 header->setPosition(QPointF(pageX, .0));
537 header->setZ(2);
538 if (m_view->separatorVisible()) {
539 QQuickItem *sep = ensureTrailingSeparator(item: header);
540 sep->setProperty(name: "inToolBar", value: true);
541 }
542 }
543 if (QQuickItem *footer = attached->globalFooter()) {
544 footerHeight = footer->isVisible() ? footer->height() : .0;
545 footer->setWidth(width + sepWidth);
546 footer->setPosition(QPointF(pageX, height() - footerHeight));
547 footer->setZ(2);
548 if (m_view->separatorVisible()) {
549 QQuickItem *sep = ensureTrailingSeparator(item: footer);
550 sep->setProperty(name: "inToolBar", value: true);
551 }
552 }
553
554 child->setSize(QSizeF(width + sepWidth, height() - headerHeight - footerHeight));
555 child->setPosition(QPointF(pageX, headerHeight));
556 child->setZ(1);
557
558 if (partialWidth <= -x()) {
559 m_leftPinnedSpace = qMax(a: m_leftPinnedSpace, b: width);
560 } else if (partialWidth > -x() + m_view->width() - child->width() + sepWidth) {
561 m_rightPinnedSpace = qMax(a: m_rightPinnedSpace, b: child->width());
562 }
563
564 partialWidth += width;
565
566 } else {
567 const qreal width = childWidth(child);
568 qreal headerHeight = .0;
569 qreal footerHeight = .0;
570 if (QQuickItem *header = attached->globalHeader(); header && qmlEngine(header)) {
571 if (m_view->separatorVisible()) {
572 QQuickItem *sep = ensureLeadingSeparator(item: header);
573 sep->setProperty(name: "inToolBar", value: true);
574 }
575 headerHeight = header->isVisible() ? header->height() : .0;
576 header->setWidth(width);
577 header->setPosition(QPointF(partialWidth, .0));
578 header->setZ(1);
579 auto it = m_trailingSeparators.find(key: header);
580 if (it != m_trailingSeparators.end()) {
581 it.value()->deleteLater();
582 m_trailingSeparators.erase(it);
583 }
584 }
585 if (QQuickItem *footer = attached->globalFooter(); footer && qmlEngine(footer)) {
586 if (m_view->separatorVisible()) {
587 QQuickItem *sep = ensureLeadingSeparator(item: footer);
588 sep->setProperty(name: "inToolBar", value: true);
589 }
590 footerHeight = footer->isVisible() ? footer->height() : .0;
591 footer->setWidth(width);
592 footer->setPosition(QPointF(partialWidth, height() - footerHeight));
593 footer->setZ(1);
594 auto it = m_trailingSeparators.find(key: footer);
595 if (it != m_trailingSeparators.end()) {
596 it.value()->deleteLater();
597 m_trailingSeparators.erase(it);
598 }
599 }
600
601 child->setSize(QSizeF(width, height() - headerHeight - footerHeight));
602
603 auto it = m_trailingSeparators.find(key: child);
604 if (it != m_trailingSeparators.end()) {
605 it.value()->deleteLater();
606 m_trailingSeparators.erase(it);
607 }
608 child->setPosition(QPointF(partialWidth, headerHeight));
609 child->setZ(0);
610
611 partialWidth += child->width();
612 }
613 }
614
615 if (reverse) {
616 attached->setIndex(m_items.count() - (++i));
617 } else {
618 attached->setIndex(i++);
619 }
620
621 implicitWidth += child->implicitWidth();
622
623 implicitHeight = qMax(a: implicitHeight, b: child->implicitHeight());
624 }
625
626 setWidth(partialWidth);
627
628 setImplicitWidth(implicitWidth);
629 setImplicitHeight(implicitHeight);
630
631 m_view->setImplicitWidth(implicitWidth);
632 m_view->setImplicitHeight(implicitHeight + m_view->topPadding() + m_view->bottomPadding());
633
634 const qreal newContentX = m_viewAnchorItem ? -m_viewAnchorItem->x() : 0.0;
635 if (m_shouldAnimate) {
636 animateX(newX: newContentX);
637 } else {
638 setBoundedX(newContentX);
639 }
640
641 updateVisibleItems();
642}
643
644void ContentItem::layoutPinnedItems()
645{
646 if (m_view->columnResizeMode() == ColumnView::SingleColumn) {
647 return;
648 }
649
650 qreal partialWidth = 0;
651 m_leftPinnedSpace = 0;
652 m_rightPinnedSpace = 0;
653
654 for (QQuickItem *child : std::as_const(t&: m_items)) {
655 ColumnViewAttached *attached = qobject_cast<ColumnViewAttached *>(object: qmlAttachedPropertiesObject<ColumnView>(obj: child, create: true));
656
657 if (child->isVisible()) {
658 if (attached->isPinned()) {
659 QQuickItem *sep = nullptr;
660 int sepWidth = 0;
661 if (m_view->separatorVisible()) {
662 sep = ensureTrailingSeparator(item: child);
663 sepWidth = (sep ? sep->width() : 0);
664 }
665
666 const qreal pageX = qMin(a: qMax(a: -x(), b: partialWidth), b: -x() + m_view->width() - child->width() + sepWidth);
667 qreal headerHeight = .0;
668 qreal footerHeight = .0;
669 if (QQuickItem *header = attached->globalHeader()) {
670 headerHeight = header->isVisible() ? header->height() : .0;
671 header->setPosition(QPointF(pageX, .0));
672 if (m_view->separatorVisible()) {
673 QQuickItem *sep = ensureTrailingSeparator(item: header);
674 sep->setProperty(name: "inToolBar", value: true);
675 }
676 }
677 if (QQuickItem *footer = attached->globalFooter()) {
678 footerHeight = footer->isVisible() ? footer->height() : .0;
679 footer->setPosition(QPointF(pageX, height() - footerHeight));
680 if (m_view->separatorVisible()) {
681 QQuickItem *sep = ensureTrailingSeparator(item: footer);
682 sep->setProperty(name: "inToolBar", value: true);
683 }
684 }
685 child->setPosition(QPointF(pageX, headerHeight));
686
687 if (partialWidth <= -x()) {
688 m_leftPinnedSpace = qMax(a: m_leftPinnedSpace, b: child->width() - sepWidth);
689 } else if (partialWidth > -x() + m_view->width() - child->width() + sepWidth) {
690 m_rightPinnedSpace = qMax(a: m_rightPinnedSpace, b: child->width());
691 }
692 }
693
694 partialWidth += child->width();
695 }
696 }
697}
698
699void ContentItem::updateVisibleItems()
700{
701 QList<QObject *> newItems;
702
703 for (auto *item : std::as_const(t&: m_items)) {
704 ColumnViewAttached *attached = qobject_cast<ColumnViewAttached *>(object: qmlAttachedPropertiesObject<ColumnView>(obj: item, create: true));
705
706 if (item->isVisible() && item->x() + x() < m_view->width() && item->x() + item->width() + x() > 0) {
707 newItems << item;
708 connect(sender: item, signal: &QObject::destroyed, context: this, slot: [this, item] {
709 m_visibleItems.removeAll(t: item);
710 });
711 attached->setInViewport(true);
712 item->setEnabled(true);
713 } else {
714 attached->setInViewport(false);
715 item->setEnabled(false);
716 }
717 }
718
719 for (auto *item : std::as_const(t&: m_visibleItems)) {
720 disconnect(sender: item, signal: &QObject::destroyed, receiver: this, zero: nullptr);
721 }
722
723 const QQuickItem *oldLeadingVisibleItem = m_view->leadingVisibleItem();
724 const QQuickItem *oldTrailingVisibleItem = m_view->trailingVisibleItem();
725
726 if (newItems != m_visibleItems) {
727 m_visibleItems = newItems;
728 Q_EMIT m_view->visibleItemsChanged();
729 if (!m_visibleItems.isEmpty() && m_visibleItems.first() != oldLeadingVisibleItem) {
730 Q_EMIT m_view->leadingVisibleItemChanged();
731 }
732 if (!m_visibleItems.isEmpty() && m_visibleItems.last() != oldTrailingVisibleItem) {
733 Q_EMIT m_view->trailingVisibleItemChanged();
734 }
735 }
736}
737
738void ContentItem::forgetItem(QQuickItem *item)
739{
740 if (!m_items.contains(t: item)) {
741 return;
742 }
743
744 ColumnViewAttached *attached = qobject_cast<ColumnViewAttached *>(object: qmlAttachedPropertiesObject<ColumnView>(obj: item, create: true));
745 attached->setView(nullptr);
746 attached->setIndex(-1);
747
748 disconnect(sender: attached, signal: nullptr, receiver: this, member: nullptr);
749 disconnect(sender: item, signal: nullptr, receiver: this, member: nullptr);
750 disconnect(sender: item, signal: nullptr, receiver: m_view, member: nullptr);
751
752 QQuickItem *separatorItem = m_leadingSeparators.take(key: item);
753 if (separatorItem) {
754 separatorItem->deleteLater();
755 }
756 separatorItem = m_trailingSeparators.take(key: item);
757 if (separatorItem) {
758 separatorItem->deleteLater();
759 }
760
761 if (QQuickItem *header = attached->globalHeader()) {
762 header->setVisible(false);
763 header->setParentItem(item);
764 separatorItem = m_leadingSeparators.take(key: header);
765 if (separatorItem) {
766 separatorItem->deleteLater();
767 }
768 separatorItem = m_trailingSeparators.take(key: header);
769 if (separatorItem) {
770 separatorItem->deleteLater();
771 }
772 }
773 if (QQuickItem *footer = attached->globalFooter()) {
774 footer->setVisible(false);
775 footer->setParentItem(item);
776 separatorItem = m_leadingSeparators.take(key: footer);
777 if (separatorItem) {
778 separatorItem->deleteLater();
779 }
780 separatorItem = m_trailingSeparators.take(key: footer);
781 if (separatorItem) {
782 separatorItem->deleteLater();
783 }
784 }
785
786 const int index = m_items.indexOf(t: item);
787 m_items.removeAll(t: item);
788 // We are connected not only to destroyed but also to lambdas
789 disconnect(sender: item, signal: nullptr, receiver: this, member: nullptr);
790 updateVisibleItems();
791 m_shouldAnimate = true;
792 m_view->polish();
793
794 if (index <= m_view->currentIndex()) {
795 m_view->setCurrentIndex(m_items.isEmpty() ? 0 : qBound(min: 0, val: index - 1, max: m_items.count() - 1));
796 }
797 Q_EMIT m_view->countChanged();
798}
799
800QQuickItem *ContentItem::ensureLeadingSeparator(QQuickItem *item)
801{
802 QQuickItem *separatorItem = m_leadingSeparators.value(key: item);
803
804 if (!separatorItem) {
805 separatorItem = qobject_cast<QQuickItem *>(
806 o: QmlComponentsPoolSingleton::instance(engine: qmlEngine(item))->m_leadingSeparatorComponent->beginCreate(QQmlEngine::contextForObject(item)));
807 if (separatorItem) {
808 separatorItem->setParent(this);
809 separatorItem->setParentItem(item);
810 separatorItem->setZ(9999);
811 separatorItem->setProperty(name: "column", value: QVariant::fromValue(value: item));
812 separatorItem->setProperty(name: "view", value: QVariant::fromValue(value: m_view));
813 QmlComponentsPoolSingleton::instance(engine: qmlEngine(item))->m_leadingSeparatorComponent->completeCreate();
814 m_leadingSeparators[item] = separatorItem;
815 }
816 }
817
818 return separatorItem;
819}
820
821QQuickItem *ContentItem::ensureTrailingSeparator(QQuickItem *item)
822{
823 QQuickItem *separatorItem = m_trailingSeparators.value(key: item);
824
825 if (!separatorItem) {
826 separatorItem = qobject_cast<QQuickItem *>(
827 o: QmlComponentsPoolSingleton::instance(engine: qmlEngine(item))->m_trailingSeparatorComponent->beginCreate(QQmlEngine::contextForObject(item)));
828 if (separatorItem) {
829 separatorItem->setParent(this);
830 separatorItem->setParentItem(item);
831 separatorItem->setZ(9999);
832 separatorItem->setProperty(name: "column", value: QVariant::fromValue(value: item));
833 QmlComponentsPoolSingleton::instance(engine: qmlEngine(item))->m_trailingSeparatorComponent->completeCreate();
834 m_trailingSeparators[item] = separatorItem;
835 }
836 }
837
838 return separatorItem;
839}
840
841void ContentItem::itemChange(QQuickItem::ItemChange change, const QQuickItem::ItemChangeData &value)
842{
843 if (m_creationInProgress) {
844 QQuickItem::itemChange(change, value);
845 return;
846 }
847 switch (change) {
848 case QQuickItem::ItemChildAddedChange: {
849 ColumnViewAttached *attached = qobject_cast<ColumnViewAttached *>(object: qmlAttachedPropertiesObject<ColumnView>(obj: value.item, create: true));
850 attached->setView(m_view);
851
852 // connect(attached, &ColumnViewAttached::fillWidthChanged, m_view, &ColumnView::polish);
853 connect(sender: attached, signal: &ColumnViewAttached::fillWidthChanged, context: this, slot: [this] {
854 m_view->polish();
855 });
856 connect(sender: attached, signal: &ColumnViewAttached::reservedSpaceChanged, context: m_view, slot: &ColumnView::polish);
857
858 value.item->setVisible(true);
859
860 if (!m_items.contains(t: value.item)) {
861 connect(sender: value.item, signal: &QQuickItem::widthChanged, context: m_view, slot: &ColumnView::polish);
862 QQuickItem *item = value.item;
863 m_items << item;
864 connect(sender: item, signal: &QObject::destroyed, context: this, slot: [this, item]() {
865 m_view->removeItem(item);
866 });
867 }
868
869 if (m_view->separatorVisible()) {
870 ensureLeadingSeparator(item: value.item);
871 }
872
873 m_shouldAnimate = true;
874 m_view->polish();
875 Q_EMIT m_view->countChanged();
876 break;
877 }
878 case QQuickItem::ItemChildRemovedChange: {
879 forgetItem(item: value.item);
880 break;
881 }
882 case QQuickItem::ItemVisibleHasChanged:
883 updateVisibleItems();
884 if (value.boolValue) {
885 m_view->polish();
886 }
887 break;
888 default:
889 break;
890 }
891 QQuickItem::itemChange(change, value);
892}
893
894void ContentItem::geometryChange(const QRectF &newGeometry, const QRectF &oldGeometry)
895{
896 updateVisibleItems();
897 QQuickItem::geometryChange(newGeometry, oldGeometry);
898}
899
900void ContentItem::syncItemsOrder()
901{
902 if (m_items == childItems()) {
903 return;
904 }
905
906 m_items = childItems();
907 // NOTE: polish() here sometimes gets indefinitely delayed and items changing order isn't seen
908 layoutItems();
909}
910
911void ContentItem::updateRepeaterModel()
912{
913 if (!sender()) {
914 return;
915 }
916
917 QObject *modelObj = sender()->property(name: "model").value<QObject *>();
918
919 if (!modelObj) {
920 m_models.remove(key: sender());
921 return;
922 }
923
924 if (m_models[sender()]) {
925 disconnect(sender: m_models[sender()], signal: nullptr, receiver: this, member: nullptr);
926 }
927
928 m_models[sender()] = modelObj;
929
930 QAbstractItemModel *qaim = qobject_cast<QAbstractItemModel *>(object: modelObj);
931
932 if (qaim) {
933 connect(sender: qaim, signal: &QAbstractItemModel::rowsMoved, context: this, slot: &ContentItem::syncItemsOrder);
934
935 } else {
936 connect(sender: modelObj, SIGNAL(childrenChanged()), receiver: this, SLOT(syncItemsOrder()));
937 }
938}
939
940void ContentItem::connectHeader(QQuickItem *oldHeader, QQuickItem *newHeader)
941{
942 if (oldHeader) {
943 disconnect(sender: oldHeader, signal: nullptr, receiver: this, member: nullptr);
944 oldHeader->setParentItem(nullptr);
945 }
946 if (newHeader) {
947 connect(sender: newHeader, signal: &QQuickItem::heightChanged, context: this, slot: &ContentItem::layoutItems);
948 connect(sender: newHeader, signal: &QQuickItem::visibleChanged, context: this, slot: &ContentItem::layoutItems);
949 newHeader->setParentItem(m_globalHeaderParent);
950 }
951}
952
953void ContentItem::connectFooter(QQuickItem *oldFooter, QQuickItem *newFooter)
954{
955 if (oldFooter) {
956 disconnect(sender: oldFooter, signal: nullptr, receiver: this, member: nullptr);
957 oldFooter->setParentItem(nullptr);
958 }
959 if (newFooter) {
960 connect(sender: newFooter, signal: &QQuickItem::heightChanged, context: this, slot: &ContentItem::layoutItems);
961 connect(sender: newFooter, signal: &QQuickItem::visibleChanged, context: this, slot: &ContentItem::layoutItems);
962 newFooter->setParentItem(m_globalFooterParent);
963 }
964}
965
966ColumnView::ColumnView(QQuickItem *parent)
967 : QQuickItem(parent)
968 , m_contentItem(nullptr)
969{
970 // NOTE: this is to *not* trigger itemChange
971 m_contentItem = new ContentItem(this);
972 // Prevent interactions outside of ColumnView bounds, and let it act as a viewport.
973 setClip(true);
974 setAcceptedMouseButtons(Qt::LeftButton | Qt::BackButton | Qt::ForwardButton);
975 setAcceptTouchEvents(false); // Relies on synthetized mouse events
976 setFiltersChildMouseEvents(true);
977
978 connect(sender: m_contentItem->m_slideAnim, signal: &QPropertyAnimation::finished, context: this, slot: [this]() {
979 m_moving = false;
980 Q_EMIT movingChanged();
981 });
982 connect(sender: m_contentItem, signal: &ContentItem::widthChanged, context: this, slot: &ColumnView::contentWidthChanged);
983 connect(sender: m_contentItem, signal: &ContentItem::xChanged, context: this, slot: &ColumnView::contentXChanged);
984
985 connect(sender: this, signal: &ColumnView::activeFocusChanged, context: this, slot: [this]() {
986 if (hasActiveFocus() && m_currentItem) {
987 m_currentItem->forceActiveFocus();
988 }
989 });
990 ColumnViewAttached *attached = qobject_cast<ColumnViewAttached *>(object: qmlAttachedPropertiesObject<ColumnView>(obj: this, create: true));
991 attached->setView(this);
992 attached = qobject_cast<ColumnViewAttached *>(object: qmlAttachedPropertiesObject<ColumnView>(obj: m_contentItem, create: true));
993 attached->setView(this);
994}
995
996ColumnView::~ColumnView()
997{
998}
999
1000ColumnView::ColumnResizeMode ColumnView::columnResizeMode() const
1001{
1002 return m_contentItem->m_columnResizeMode;
1003}
1004
1005void ColumnView::setColumnResizeMode(ColumnResizeMode mode)
1006{
1007 if (m_contentItem->m_columnResizeMode == mode) {
1008 return;
1009 }
1010
1011 m_contentItem->m_columnResizeMode = mode;
1012 if (mode == SingleColumn && m_currentItem) {
1013 m_contentItem->m_viewAnchorItem = m_currentItem;
1014 }
1015 m_contentItem->m_shouldAnimate = false;
1016 polish();
1017 Q_EMIT columnResizeModeChanged();
1018}
1019
1020qreal ColumnView::columnWidth() const
1021{
1022 return m_contentItem->m_columnWidth;
1023}
1024
1025void ColumnView::setColumnWidth(qreal width)
1026{
1027 // Always forget the internal binding when the user sets anything, even the same value
1028 disconnect(sender: QmlComponentsPoolSingleton::instance(engine: qmlEngine(this)), signal: &QmlComponentsPool::gridUnitChanged, receiver: this, zero: nullptr);
1029
1030 if (m_contentItem->m_columnWidth == width) {
1031 return;
1032 }
1033
1034 m_contentItem->m_columnWidth = width;
1035 m_contentItem->m_shouldAnimate = false;
1036 polish();
1037 Q_EMIT columnWidthChanged();
1038}
1039
1040int ColumnView::currentIndex() const
1041{
1042 return m_currentIndex;
1043}
1044
1045void ColumnView::setCurrentIndex(int index)
1046{
1047 if (m_currentIndex == index || index < -1 || index >= m_contentItem->m_items.count()) {
1048 return;
1049 }
1050
1051 m_currentIndex = index;
1052
1053 if (index == -1) {
1054 m_currentItem.clear();
1055
1056 } else {
1057 m_currentItem = m_contentItem->m_items[index];
1058 Q_ASSERT(m_currentItem);
1059 m_currentItem->forceActiveFocus();
1060
1061 // If the current item is not on view, scroll
1062 QRectF mappedCurrent = m_currentItem->mapRectToItem(item: this, rect: QRectF(QPointF(0, 0), m_currentItem->size()));
1063
1064 if (m_contentItem->m_slideAnim->state() == QAbstractAnimation::Running) {
1065 mappedCurrent.moveLeft(pos: mappedCurrent.left() + m_contentItem->x() + m_contentItem->m_slideAnim->endValue().toInt());
1066 }
1067
1068 // m_contentItem->m_slideAnim->stop();
1069
1070 QRectF contentsRect(m_contentItem->m_leftPinnedSpace, //
1071 0,
1072 width() - m_contentItem->m_rightPinnedSpace - m_contentItem->m_leftPinnedSpace,
1073 height());
1074
1075 if (!m_mouseDown) {
1076 if (!contentsRect.contains(r: mappedCurrent)) {
1077 m_contentItem->m_viewAnchorItem = m_currentItem;
1078 if (qApp->layoutDirection() == Qt::RightToLeft) {
1079 m_contentItem->animateX(newX: -m_currentItem->x() - m_currentItem->width() + width());
1080 } else {
1081 m_contentItem->animateX(newX: -m_currentItem->x() + m_contentItem->m_leftPinnedSpace);
1082 }
1083 } else {
1084 m_contentItem->snapToItem();
1085 }
1086 }
1087 }
1088
1089 Q_EMIT currentIndexChanged();
1090 Q_EMIT currentItemChanged();
1091}
1092
1093QQuickItem *ColumnView::currentItem()
1094{
1095 return m_currentItem;
1096}
1097
1098QList<QObject *> ColumnView::visibleItems() const
1099{
1100 return m_contentItem->m_visibleItems;
1101}
1102
1103QQuickItem *ColumnView::leadingVisibleItem() const
1104{
1105 if (m_contentItem->m_visibleItems.isEmpty()) {
1106 return nullptr;
1107 }
1108
1109 return qobject_cast<QQuickItem *>(o: m_contentItem->m_visibleItems.first());
1110}
1111
1112QQuickItem *ColumnView::trailingVisibleItem() const
1113{
1114 if (m_contentItem->m_visibleItems.isEmpty()) {
1115 return nullptr;
1116 }
1117
1118 return qobject_cast<QQuickItem *>(o: m_contentItem->m_visibleItems.last());
1119}
1120
1121int ColumnView::count() const
1122{
1123 return m_contentItem->m_items.count();
1124}
1125
1126qreal ColumnView::topPadding() const
1127{
1128 return m_topPadding;
1129}
1130
1131void ColumnView::setTopPadding(qreal padding)
1132{
1133 if (padding == m_topPadding) {
1134 return;
1135 }
1136
1137 m_topPadding = padding;
1138 polish();
1139 Q_EMIT topPaddingChanged();
1140}
1141
1142qreal ColumnView::bottomPadding() const
1143{
1144 return m_bottomPadding;
1145}
1146
1147void ColumnView::setBottomPadding(qreal padding)
1148{
1149 if (padding == m_bottomPadding) {
1150 return;
1151 }
1152
1153 m_bottomPadding = padding;
1154 polish();
1155 Q_EMIT bottomPaddingChanged();
1156}
1157
1158QQuickItem *ColumnView::contentItem() const
1159{
1160 return m_contentItem;
1161}
1162
1163int ColumnView::scrollDuration() const
1164{
1165 return m_contentItem->m_slideAnim->duration();
1166}
1167
1168void ColumnView::setScrollDuration(int duration)
1169{
1170 disconnect(sender: QmlComponentsPoolSingleton::instance(engine: qmlEngine(this)), signal: &QmlComponentsPool::longDurationChanged, receiver: this, zero: nullptr);
1171
1172 if (m_contentItem->m_slideAnim->duration() == duration) {
1173 return;
1174 }
1175
1176 m_contentItem->m_slideAnim->setDuration(duration);
1177 Q_EMIT scrollDurationChanged();
1178}
1179
1180bool ColumnView::separatorVisible() const
1181{
1182 return m_separatorVisible;
1183}
1184
1185void ColumnView::setSeparatorVisible(bool visible)
1186{
1187 if (visible == m_separatorVisible) {
1188 return;
1189 }
1190
1191 m_separatorVisible = visible;
1192
1193 Q_EMIT separatorVisibleChanged();
1194}
1195
1196bool ColumnView::dragging() const
1197{
1198 return m_dragging;
1199}
1200
1201bool ColumnView::moving() const
1202{
1203 return m_moving;
1204}
1205
1206qreal ColumnView::contentWidth() const
1207{
1208 return m_contentItem->width();
1209}
1210
1211qreal ColumnView::contentX() const
1212{
1213 return -m_contentItem->x();
1214}
1215
1216void ColumnView::setContentX(qreal x) const
1217{
1218 m_contentItem->setX(qRound(d: -x));
1219}
1220
1221bool ColumnView::interactive() const
1222{
1223 return m_interactive;
1224}
1225
1226void ColumnView::setInteractive(bool interactive)
1227{
1228 if (m_interactive == interactive) {
1229 return;
1230 }
1231
1232 m_interactive = interactive;
1233
1234 if (!m_interactive) {
1235 if (m_dragging) {
1236 m_dragging = false;
1237 Q_EMIT draggingChanged();
1238 }
1239
1240 m_contentItem->snapToItem();
1241 setKeepMouseGrab(false);
1242 }
1243
1244 Q_EMIT interactiveChanged();
1245}
1246
1247bool ColumnView::acceptsMouse() const
1248{
1249 return m_acceptsMouse;
1250}
1251
1252void ColumnView::setAcceptsMouse(bool accepts)
1253{
1254 if (m_acceptsMouse == accepts) {
1255 return;
1256 }
1257
1258 m_acceptsMouse = accepts;
1259
1260 if (!m_acceptsMouse) {
1261 if (m_dragging) {
1262 m_dragging = false;
1263 Q_EMIT draggingChanged();
1264 }
1265
1266 m_contentItem->snapToItem();
1267 setKeepMouseGrab(false);
1268 }
1269
1270 Q_EMIT acceptsMouseChanged();
1271}
1272
1273void ColumnView::addItem(QQuickItem *item)
1274{
1275 insertItem(pos: m_contentItem->m_items.length(), item);
1276}
1277
1278void ColumnView::insertItem(int pos, QQuickItem *item)
1279{
1280 if (!item || m_contentItem->m_items.contains(t: item)) {
1281 return;
1282 }
1283
1284 m_contentItem->m_items.insert(i: qBound(min: 0, val: pos, max: m_contentItem->m_items.length()), t: item);
1285
1286 connect(sender: item, signal: &QObject::destroyed, context: m_contentItem, slot: [this, item]() {
1287 removeItem(item);
1288 });
1289 ColumnViewAttached *attached = qobject_cast<ColumnViewAttached *>(object: qmlAttachedPropertiesObject<ColumnView>(obj: item, create: true));
1290 attached->setOriginalParent(item->parentItem());
1291 attached->setShouldDeleteOnRemove(item->parentItem() == nullptr && QQmlEngine::objectOwnership(item) == QQmlEngine::JavaScriptOwnership);
1292 item->setParentItem(m_contentItem);
1293
1294 item->forceActiveFocus();
1295
1296 if (attached->globalHeader()) {
1297 m_contentItem->connectHeader(oldHeader: nullptr, newHeader: attached->globalHeader());
1298 }
1299 if (attached->globalFooter()) {
1300 m_contentItem->connectFooter(oldFooter: nullptr, newFooter: attached->globalFooter());
1301 }
1302 connect(sender: attached, signal: &ColumnViewAttached::globalHeaderChanged, context: m_contentItem, slot: &ContentItem::connectHeader);
1303 connect(sender: attached, signal: &ColumnViewAttached::globalFooterChanged, context: m_contentItem, slot: &ContentItem::connectFooter);
1304
1305 // Animate shift to new item.
1306 m_contentItem->m_shouldAnimate = true;
1307 m_contentItem->layoutItems();
1308 Q_EMIT contentChildrenChanged();
1309
1310 // In order to keep the same current item we need to increase the current index if displaced
1311 // NOTE: just updating m_currentIndex does *not* update currentItem (which is what we need atm) while setCurrentIndex will update also currentItem
1312 if (m_currentIndex >= pos) {
1313 ++m_currentIndex;
1314 Q_EMIT currentIndexChanged();
1315 }
1316
1317 Q_EMIT itemInserted(position: pos, item);
1318}
1319
1320void ColumnView::replaceItem(int pos, QQuickItem *item)
1321{
1322 if (pos < 0 || pos >= m_contentItem->m_items.length()) {
1323 qCWarning(KirigamiLog) << "Position" << pos << "passed to ColumnView::replaceItem is out of range.";
1324 return;
1325 }
1326
1327 if (!item) {
1328 qCWarning(KirigamiLog) << "Null item passed to ColumnView::replaceItem.";
1329 return;
1330 }
1331
1332 QQuickItem *oldItem = m_contentItem->m_items[pos];
1333
1334 // In order to keep the same current item we need to increase the current index if displaced
1335 if (m_currentIndex >= pos) {
1336 setCurrentIndex(m_currentIndex - 1);
1337 }
1338
1339 m_contentItem->forgetItem(item: oldItem);
1340 oldItem->setVisible(false);
1341
1342 ColumnViewAttached *attached = qobject_cast<ColumnViewAttached *>(object: qmlAttachedPropertiesObject<ColumnView>(obj: oldItem, create: false));
1343
1344 if (attached && attached->shouldDeleteOnRemove()) {
1345 oldItem->deleteLater();
1346 } else {
1347 oldItem->setParentItem(attached ? attached->originalParent() : nullptr);
1348 }
1349
1350 Q_EMIT itemRemoved(item: oldItem);
1351
1352 if (!m_contentItem->m_items.contains(t: item)) {
1353 m_contentItem->m_items.insert(i: qBound(min: 0, val: pos, max: m_contentItem->m_items.length()), t: item);
1354
1355 connect(sender: item, signal: &QObject::destroyed, context: m_contentItem, slot: [this, item]() {
1356 removeItem(item);
1357 });
1358 ColumnViewAttached *attached = qobject_cast<ColumnViewAttached *>(object: qmlAttachedPropertiesObject<ColumnView>(obj: item, create: true));
1359 attached->setOriginalParent(item->parentItem());
1360 attached->setShouldDeleteOnRemove(item->parentItem() == nullptr && QQmlEngine::objectOwnership(item) == QQmlEngine::JavaScriptOwnership);
1361 item->setParentItem(m_contentItem);
1362
1363 if (attached->globalHeader()) {
1364 m_contentItem->connectHeader(oldHeader: nullptr, newHeader: attached->globalHeader());
1365 }
1366 if (attached->globalFooter()) {
1367 m_contentItem->connectFooter(oldFooter: nullptr, newFooter: attached->globalFooter());
1368 }
1369 connect(sender: attached, signal: &ColumnViewAttached::globalHeaderChanged, context: m_contentItem, slot: &ContentItem::connectHeader);
1370 connect(sender: attached, signal: &ColumnViewAttached::globalFooterChanged, context: m_contentItem, slot: &ContentItem::connectFooter);
1371
1372 if (m_currentIndex >= pos) {
1373 ++m_currentIndex;
1374 Q_EMIT currentIndexChanged();
1375 }
1376
1377 Q_EMIT itemInserted(position: pos, item);
1378 }
1379
1380 // Disable animation so replacement happens immediately.
1381 m_contentItem->m_shouldAnimate = false;
1382 m_contentItem->layoutItems();
1383 Q_EMIT contentChildrenChanged();
1384}
1385
1386void ColumnView::moveItem(int from, int to)
1387{
1388 if (m_contentItem->m_items.isEmpty() //
1389 || from < 0 || from >= m_contentItem->m_items.length() //
1390 || to < 0 || to >= m_contentItem->m_items.length()) {
1391 return;
1392 }
1393
1394 m_contentItem->m_items.move(from, to);
1395 m_contentItem->m_shouldAnimate = true;
1396
1397 if (from == m_currentIndex) {
1398 m_currentIndex = to;
1399 Q_EMIT currentIndexChanged();
1400 } else if (from < m_currentIndex && to > m_currentIndex) {
1401 --m_currentIndex;
1402 Q_EMIT currentIndexChanged();
1403 } else if (from > m_currentIndex && to <= m_currentIndex) {
1404 ++m_currentIndex;
1405 Q_EMIT currentIndexChanged();
1406 }
1407
1408 polish();
1409}
1410
1411QQuickItem *ColumnView::removeItem(QQuickItem *item)
1412{
1413 if (m_contentItem->m_items.isEmpty() || !m_contentItem->m_items.contains(t: item)) {
1414 return nullptr;
1415 }
1416
1417 const int index = m_contentItem->m_items.indexOf(t: item);
1418
1419 // In order to keep the same current item we need to increase the current index if displaced
1420 if (m_currentIndex >= index) {
1421 setCurrentIndex(m_currentIndex - 1);
1422 }
1423
1424 m_contentItem->forgetItem(item);
1425 item->setVisible(false);
1426
1427 ColumnViewAttached *attached = qobject_cast<ColumnViewAttached *>(object: qmlAttachedPropertiesObject<ColumnView>(obj: item, create: false));
1428
1429 if (attached && attached->shouldDeleteOnRemove()) {
1430 item->deleteLater();
1431 } else {
1432 item->setParentItem(attached ? attached->originalParent() : nullptr);
1433 }
1434
1435 Q_EMIT contentChildrenChanged();
1436 Q_EMIT itemRemoved(item);
1437
1438 return item;
1439}
1440
1441QQuickItem *ColumnView::removeItem(const int index)
1442{
1443 if (m_contentItem->m_items.isEmpty() || index < 0 || index >= count()) {
1444 return nullptr;
1445 } else {
1446 return removeItem(item: m_contentItem->m_items[index]);
1447 }
1448}
1449
1450QQuickItem *ColumnView::removeItem(const QVariant &item)
1451{
1452 if (item.canConvert<QQuickItem *>()) {
1453 return removeItem(item: item.value<QQuickItem *>());
1454 } else if (item.canConvert<int>()) {
1455 return removeItem(index: item.toInt());
1456 } else {
1457 return nullptr;
1458 }
1459}
1460
1461QQuickItem *ColumnView::pop(const QVariant &item)
1462{
1463 if (item.canConvert<QQuickItem *>()) {
1464 return pop(item: item.value<QQuickItem *>());
1465 } else if (item.canConvert<int>()) {
1466 return pop(index: item.toInt());
1467 } else if (item.isNull()) {
1468 return pop();
1469 }
1470 return nullptr;
1471}
1472QQuickItem *ColumnView::pop(QQuickItem *item)
1473{
1474 QQuickItem *removed = nullptr;
1475
1476 while (!m_contentItem->m_items.isEmpty() && m_contentItem->m_items.last() != item) {
1477 removed = removeItem(item: m_contentItem->m_items.last());
1478 }
1479 return removed;
1480}
1481
1482QQuickItem *ColumnView::pop(const int index)
1483{
1484 if (index >= 0 && index < count() - 1) {
1485 return pop(item: m_contentItem->m_items.at(i: index));
1486 } else if (index == -1) {
1487 return pop(item: nullptr);
1488 }
1489 return nullptr;
1490}
1491
1492QQuickItem *ColumnView::pop()
1493{
1494 if (count() > 0) {
1495 return removeItem(index: count() - 1);
1496 }
1497 return nullptr;
1498}
1499
1500void ColumnView::clear()
1501{
1502 // Don't do an iterator on a list that gets progressively destroyed, treat it as a stack
1503 while (!m_contentItem->m_items.isEmpty()) {
1504 QQuickItem *item = m_contentItem->m_items.first();
1505 removeItem(item);
1506 }
1507
1508 m_contentItem->m_items.clear();
1509 Q_EMIT contentChildrenChanged();
1510}
1511
1512bool ColumnView::containsItem(QQuickItem *item)
1513{
1514 return m_contentItem->m_items.contains(t: item);
1515}
1516
1517QQuickItem *ColumnView::itemAt(qreal x, qreal y)
1518{
1519 return m_contentItem->childAt(x, y);
1520}
1521
1522ColumnViewAttached *ColumnView::qmlAttachedProperties(QObject *object)
1523{
1524 return new ColumnViewAttached(object);
1525}
1526
1527void ColumnView::geometryChange(const QRectF &newGeometry, const QRectF &oldGeometry)
1528{
1529 m_contentItem->setY(m_topPadding);
1530 m_contentItem->setHeight(newGeometry.height() - m_topPadding - m_bottomPadding);
1531 m_contentItem->m_shouldAnimate = false;
1532 polish();
1533
1534 m_contentItem->updateVisibleItems();
1535 QQuickItem::geometryChange(newGeometry, oldGeometry);
1536}
1537
1538bool ColumnView::childMouseEventFilter(QQuickItem *item, QEvent *event)
1539{
1540 if (!m_interactive || item == m_contentItem) {
1541 return QQuickItem::childMouseEventFilter(item, event);
1542 }
1543
1544 switch (event->type()) {
1545 case QEvent::MouseButtonPress: {
1546 QMouseEvent *me = static_cast<QMouseEvent *>(event);
1547
1548 if (me->button() != Qt::LeftButton) {
1549 return false;
1550 }
1551
1552 // On press, we set the current index of the view to the root item
1553 QQuickItem *candidateItem = item;
1554 while (candidateItem->parentItem() && candidateItem->parentItem() != m_contentItem) {
1555 candidateItem = candidateItem->parentItem();
1556 }
1557 if (int idx = m_contentItem->m_items.indexOf(t: candidateItem); idx >= 0 && candidateItem->parentItem() == m_contentItem) {
1558 setCurrentIndex(idx);
1559 }
1560
1561 // if !m_acceptsMouse we don't drag with mouse
1562 if (!m_acceptsMouse && me->source() == Qt::MouseEventNotSynthesized) {
1563 event->setAccepted(false);
1564 return false;
1565 }
1566
1567 m_contentItem->m_slideAnim->stop();
1568 if (item->property(name: "preventStealing").toBool()) {
1569 m_contentItem->snapToItem();
1570 return false;
1571 }
1572 m_oldMouseX = m_startMouseX = mapFromItem(item, point: me->position()).x();
1573 m_oldMouseY = m_startMouseY = mapFromItem(item, point: me->position()).y();
1574
1575 m_mouseDown = true;
1576 me->setAccepted(false);
1577 setKeepMouseGrab(false);
1578
1579 break;
1580 }
1581 case QEvent::MouseMove: {
1582 QMouseEvent *me = static_cast<QMouseEvent *>(event);
1583
1584 if (!m_acceptsMouse && me->source() == Qt::MouseEventNotSynthesized) {
1585 return false;
1586 }
1587
1588 if (!(me->buttons() & Qt::LeftButton)) {
1589 return false;
1590 }
1591
1592 const QPointF pos = mapFromItem(item, point: me->position());
1593
1594 bool verticalScrollIntercepted = false;
1595
1596 QQuickItem *candidateItem = item;
1597 while (candidateItem->parentItem() && candidateItem->parentItem() != m_contentItem) {
1598 candidateItem = candidateItem->parentItem();
1599 }
1600 if (candidateItem->parentItem() == m_contentItem) {
1601 ColumnViewAttached *attached = qobject_cast<ColumnViewAttached *>(object: qmlAttachedPropertiesObject<ColumnView>(obj: candidateItem, create: true));
1602 if (attached->preventStealing()) {
1603 return false;
1604 }
1605 }
1606
1607 {
1608 ColumnViewAttached *attached = qobject_cast<ColumnViewAttached *>(object: qmlAttachedPropertiesObject<ColumnView>(obj: candidateItem, create: true));
1609
1610 ScrollIntentionEvent scrollIntentionEvent;
1611 scrollIntentionEvent.delta = QPointF(pos.x() - m_oldMouseX, pos.y() - m_oldMouseY);
1612
1613 Q_EMIT attached->scrollIntention(event: &scrollIntentionEvent);
1614
1615 if (scrollIntentionEvent.accepted) {
1616 verticalScrollIntercepted = true;
1617 event->setAccepted(true);
1618 }
1619 }
1620
1621 if ((!keepMouseGrab() && item->keepMouseGrab()) || item->property(name: "preventStealing").toBool()) {
1622 m_contentItem->snapToItem();
1623 m_oldMouseX = pos.x();
1624 m_oldMouseY = pos.y();
1625 return false;
1626 }
1627
1628 const bool wasDragging = m_dragging;
1629 // If a drag happened, start to steal all events, use startDragDistance * 2 to give time to widgets to take the mouse grab by themselves
1630 m_dragging = keepMouseGrab() || qAbs(t: mapFromItem(item, point: me->position()).x() - m_startMouseX) > qApp->styleHints()->startDragDistance() * 3;
1631
1632 if (m_dragging != wasDragging) {
1633 m_moving = true;
1634 Q_EMIT movingChanged();
1635 Q_EMIT draggingChanged();
1636 }
1637
1638 if (m_dragging) {
1639 m_contentItem->setBoundedX(m_contentItem->x() + pos.x() - m_oldMouseX);
1640 }
1641
1642 m_contentItem->m_lastDragDelta = pos.x() - m_oldMouseX;
1643 m_oldMouseX = pos.x();
1644 m_oldMouseY = pos.y();
1645
1646 setKeepMouseGrab(m_dragging);
1647 me->setAccepted(m_dragging);
1648
1649 return m_dragging && !verticalScrollIntercepted;
1650 }
1651 case QEvent::MouseButtonRelease: {
1652 QMouseEvent *me = static_cast<QMouseEvent *>(event);
1653 if (item->property(name: "preventStealing").toBool()) {
1654 return false;
1655 }
1656
1657 if (me->button() == Qt::BackButton && m_currentIndex > 0) {
1658 setCurrentIndex(m_currentIndex - 1);
1659 me->accept();
1660 return true;
1661 } else if (me->button() == Qt::ForwardButton) {
1662 setCurrentIndex(m_currentIndex + 1);
1663 me->accept();
1664 return true;
1665 }
1666
1667 if (!m_acceptsMouse && me->source() == Qt::MouseEventNotSynthesized) {
1668 return false;
1669 }
1670
1671 if (me->button() != Qt::LeftButton) {
1672 return false;
1673 }
1674
1675 m_mouseDown = false;
1676
1677 if (m_dragging) {
1678 m_contentItem->snapToItem();
1679 m_contentItem->m_lastDragDelta = 0;
1680 m_dragging = false;
1681 Q_EMIT draggingChanged();
1682 }
1683
1684 event->accept();
1685
1686 // if a drag happened, don't pass the event
1687 const bool block = keepMouseGrab();
1688 setKeepMouseGrab(false);
1689
1690 me->setAccepted(block);
1691 return block;
1692 }
1693 default:
1694 break;
1695 }
1696
1697 return QQuickItem::childMouseEventFilter(item, event);
1698}
1699
1700void ColumnView::mousePressEvent(QMouseEvent *event)
1701{
1702 if (!m_acceptsMouse && event->source() == Qt::MouseEventNotSynthesized) {
1703 event->setAccepted(false);
1704 return;
1705 }
1706
1707 if (event->button() == Qt::BackButton || event->button() == Qt::ForwardButton) {
1708 event->accept();
1709 return;
1710 }
1711
1712 if (!m_interactive) {
1713 return;
1714 }
1715
1716 m_contentItem->snapToItem();
1717 m_oldMouseX = event->position().x();
1718 m_startMouseX = event->position().x();
1719 m_mouseDown = true;
1720 setKeepMouseGrab(false);
1721 event->accept();
1722}
1723
1724void ColumnView::mouseMoveEvent(QMouseEvent *event)
1725{
1726 if (event->buttons() & Qt::BackButton || event->buttons() & Qt::ForwardButton) {
1727 event->accept();
1728 return;
1729 }
1730
1731 if (!m_interactive) {
1732 return;
1733 }
1734
1735 const bool wasDragging = m_dragging;
1736 // Same startDragDistance * 2 as the event filter
1737 m_dragging = keepMouseGrab() || qAbs(t: event->position().x() - m_startMouseX) > qApp->styleHints()->startDragDistance() * 2;
1738 if (m_dragging != wasDragging) {
1739 m_moving = true;
1740 Q_EMIT movingChanged();
1741 Q_EMIT draggingChanged();
1742 }
1743
1744 setKeepMouseGrab(m_dragging);
1745
1746 if (m_dragging) {
1747 m_contentItem->setBoundedX(m_contentItem->x() + event->pos().x() - m_oldMouseX);
1748 }
1749
1750 m_contentItem->m_lastDragDelta = event->pos().x() - m_oldMouseX;
1751 m_oldMouseX = event->pos().x();
1752 event->accept();
1753}
1754
1755void ColumnView::mouseReleaseEvent(QMouseEvent *event)
1756{
1757 if (event->button() == Qt::BackButton && m_currentIndex > 0) {
1758 setCurrentIndex(m_currentIndex - 1);
1759 event->accept();
1760 return;
1761 } else if (event->button() == Qt::ForwardButton) {
1762 setCurrentIndex(m_currentIndex + 1);
1763 event->accept();
1764 return;
1765 }
1766
1767 m_mouseDown = false;
1768
1769 if (!m_interactive) {
1770 return;
1771 }
1772
1773 m_contentItem->snapToItem();
1774 m_contentItem->m_lastDragDelta = 0;
1775
1776 if (m_dragging) {
1777 m_dragging = false;
1778 Q_EMIT draggingChanged();
1779 }
1780
1781 setKeepMouseGrab(false);
1782 event->accept();
1783}
1784
1785void ColumnView::mouseUngrabEvent()
1786{
1787 m_mouseDown = false;
1788
1789 if (m_contentItem->m_slideAnim->state() != QAbstractAnimation::Running) {
1790 m_contentItem->snapToItem();
1791 }
1792 m_contentItem->m_lastDragDelta = 0;
1793
1794 if (m_dragging) {
1795 m_dragging = false;
1796 Q_EMIT draggingChanged();
1797 }
1798
1799 setKeepMouseGrab(false);
1800}
1801
1802void ColumnView::classBegin()
1803{
1804 auto syncColumnWidth = [this]() {
1805 m_contentItem->m_columnWidth = privateQmlComponentsPoolSelf->instance(engine: qmlEngine(this))->m_units->gridUnit() * 20;
1806 Q_EMIT columnWidthChanged();
1807 };
1808
1809 connect(sender: QmlComponentsPoolSingleton::instance(engine: qmlEngine(this)), signal: &QmlComponentsPool::gridUnitChanged, context: this, slot&: syncColumnWidth);
1810 syncColumnWidth();
1811
1812 auto syncDuration = [this]() {
1813 m_contentItem->m_slideAnim->setDuration(QmlComponentsPoolSingleton::instance(engine: qmlEngine(this))->m_units->veryLongDuration());
1814 Q_EMIT scrollDurationChanged();
1815 };
1816
1817 connect(sender: QmlComponentsPoolSingleton::instance(engine: qmlEngine(this)), signal: &QmlComponentsPool::longDurationChanged, context: this, slot&: syncDuration);
1818 syncDuration();
1819
1820 QQuickItem::classBegin();
1821}
1822
1823void ColumnView::componentComplete()
1824{
1825 m_complete = true;
1826 QQuickItem::componentComplete();
1827}
1828
1829void ColumnView::updatePolish()
1830{
1831 m_contentItem->layoutItems();
1832}
1833
1834void ColumnView::itemChange(QQuickItem::ItemChange change, const QQuickItem::ItemChangeData &value)
1835{
1836 switch (change) {
1837 case QQuickItem::ItemChildAddedChange:
1838 if (m_contentItem && value.item != m_contentItem && !value.item->inherits(classname: "QQuickRepeater")) {
1839 addItem(item: value.item);
1840 }
1841 break;
1842 default:
1843 break;
1844 }
1845 QQuickItem::itemChange(change, value);
1846}
1847
1848void ColumnView::contentChildren_append(QQmlListProperty<QQuickItem> *prop, QQuickItem *item)
1849{
1850 // This can only be called from QML
1851 ColumnView *view = static_cast<ColumnView *>(prop->object);
1852 if (!view) {
1853 return;
1854 }
1855
1856 view->m_contentItem->m_items.append(t: item);
1857 connect(sender: item, signal: &QObject::destroyed, context: view->m_contentItem, slot: [view, item]() {
1858 view->removeItem(item);
1859 });
1860
1861 ColumnViewAttached *attached = qobject_cast<ColumnViewAttached *>(object: qmlAttachedPropertiesObject<ColumnView>(obj: item, create: true));
1862 attached->setOriginalParent(item->parentItem());
1863 attached->setShouldDeleteOnRemove(item->parentItem() == nullptr && QQmlEngine::objectOwnership(item) == QQmlEngine::JavaScriptOwnership);
1864
1865 item->setParentItem(view->m_contentItem);
1866}
1867
1868qsizetype ColumnView::contentChildren_count(QQmlListProperty<QQuickItem> *prop)
1869{
1870 ColumnView *view = static_cast<ColumnView *>(prop->object);
1871 if (!view) {
1872 return 0;
1873 }
1874
1875 return view->m_contentItem->m_items.count();
1876}
1877
1878QQuickItem *ColumnView::contentChildren_at(QQmlListProperty<QQuickItem> *prop, qsizetype index)
1879{
1880 ColumnView *view = static_cast<ColumnView *>(prop->object);
1881 if (!view) {
1882 return nullptr;
1883 }
1884
1885 if (index < 0 || index >= view->m_contentItem->m_items.count()) {
1886 return nullptr;
1887 }
1888 return view->m_contentItem->m_items.value(i: index);
1889}
1890
1891void ColumnView::contentChildren_clear(QQmlListProperty<QQuickItem> *prop)
1892{
1893 ColumnView *view = static_cast<ColumnView *>(prop->object);
1894 if (!view) {
1895 return;
1896 }
1897
1898 return view->m_contentItem->m_items.clear();
1899}
1900
1901QQmlListProperty<QQuickItem> ColumnView::contentChildren()
1902{
1903 return QQmlListProperty<QQuickItem>(this, //
1904 nullptr,
1905 contentChildren_append,
1906 contentChildren_count,
1907 contentChildren_at,
1908 contentChildren_clear);
1909}
1910
1911void ColumnView::contentData_append(QQmlListProperty<QObject> *prop, QObject *object)
1912{
1913 ColumnView *view = static_cast<ColumnView *>(prop->object);
1914 if (!view) {
1915 return;
1916 }
1917
1918 view->m_contentData.append(t: object);
1919 QQuickItem *item = qobject_cast<QQuickItem *>(o: object);
1920 // exclude repeaters from layout
1921 if (item && item->inherits(classname: "QQuickRepeater")) {
1922 item->setParentItem(view);
1923
1924 connect(sender: item, SIGNAL(modelChanged()), receiver: view->m_contentItem, SLOT(updateRepeaterModel()));
1925
1926 } else if (item) {
1927 view->m_contentItem->m_items.append(t: item);
1928 connect(sender: item, signal: &QObject::destroyed, context: view->m_contentItem, slot: [view, item]() {
1929 view->removeItem(item);
1930 });
1931
1932 ColumnViewAttached *attached = qobject_cast<ColumnViewAttached *>(object: qmlAttachedPropertiesObject<ColumnView>(obj: item, create: true));
1933 attached->setOriginalParent(item->parentItem());
1934 attached->setShouldDeleteOnRemove(view->m_complete && !item->parentItem() && QQmlEngine::objectOwnership(item) == QQmlEngine::JavaScriptOwnership);
1935
1936 item->setParentItem(view->m_contentItem);
1937
1938 } else {
1939 object->setParent(view);
1940 }
1941}
1942
1943qsizetype ColumnView::contentData_count(QQmlListProperty<QObject> *prop)
1944{
1945 ColumnView *view = static_cast<ColumnView *>(prop->object);
1946 if (!view) {
1947 return 0;
1948 }
1949
1950 return view->m_contentData.count();
1951}
1952
1953QObject *ColumnView::contentData_at(QQmlListProperty<QObject> *prop, qsizetype index)
1954{
1955 ColumnView *view = static_cast<ColumnView *>(prop->object);
1956 if (!view) {
1957 return nullptr;
1958 }
1959
1960 if (index < 0 || index >= view->m_contentData.count()) {
1961 return nullptr;
1962 }
1963 return view->m_contentData.value(i: index);
1964}
1965
1966void ColumnView::contentData_clear(QQmlListProperty<QObject> *prop)
1967{
1968 ColumnView *view = static_cast<ColumnView *>(prop->object);
1969 if (!view) {
1970 return;
1971 }
1972
1973 return view->m_contentData.clear();
1974}
1975
1976QQmlListProperty<QObject> ColumnView::contentData()
1977{
1978 return QQmlListProperty<QObject>(this, //
1979 nullptr,
1980 contentData_append,
1981 contentData_count,
1982 contentData_at,
1983 contentData_clear);
1984}
1985
1986#include "moc_columnview.cpp"
1987#include "moc_columnview_p.cpp"
1988

source code of kirigami/src/columnview.cpp