diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -14,6 +14,7 @@ set(kirigami_SRCS kirigamiplugin.cpp + columnview.cpp enums.cpp delegaterecycler.cpp desktopicon.cpp diff --git a/src/columnview.h b/src/columnview.h new file mode 100644 --- /dev/null +++ b/src/columnview.h @@ -0,0 +1,408 @@ +/* + * Copyright 2019 Marco Martin + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU Library General Public License as + * published by the Free Software Foundation; either version 2, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details + * + * You should have received a copy of the GNU Library General Public + * License along with this program; if not, write to the + * Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#pragma once + +#include +#include +#include + +class ContentItem; +class ColumnView; + +/** + * This is an attached property to every item that is inserted in the ColumnView, + * used to access the view and page information such as the position and informations for layouting, such as fillWidth + * @since 2.7 + */ +class ColumnViewAttached : public QObject +{ + Q_OBJECT + + /** + * The index position of the column in the view, starting from 0 + */ + Q_PROPERTY(int index READ index WRITE setIndex NOTIFY indexChanged) + + /** + * If true, the column will expand to take the whole viewport space minus reservedSpace + */ + Q_PROPERTY(bool fillWidth READ fillWidth WRITE setFillWidth NOTIFY fillWidthChanged) + + /** + * When a column is fillWidth, it will keep reservedSpace amount of pixels from going to fill the full viewport width + */ + Q_PROPERTY(qreal reservedSpace READ reservedSpace WRITE setReservedSpace NOTIFY reservedSpaceChanged) + + /** + * Like the same property of MouseArea, when this is true, the column view won't + * try to manage events by itself when filtering from a child, not + * disturbing user interaction + */ + Q_PROPERTY(bool preventStealing READ preventStealing WRITE setPreventStealing NOTIFY preventStealingChanged) + + /** + * The view this column belongs to + */ + Q_PROPERTY(ColumnView *view READ view NOTIFY viewChanged) + +public: + ColumnViewAttached(QObject *parent = nullptr); + ~ColumnViewAttached(); + + void setIndex(int index); + int index() const; + + void setFillWidth(bool fill); + bool fillWidth() const; + + qreal reservedSpace() const; + void setReservedSpace(qreal space); + + ColumnView *view(); + void setView(ColumnView *view); + + //Private API, not for QML use + QQuickItem *originalParent() const; + void setOriginalParent(QQuickItem *parent); + + bool shouldDeleteOnRemove() const; + void setShouldDeleteOnRemove(bool del); + + bool preventStealing() const; + void setPreventStealing(bool prevent); + +Q_SIGNALS: + void indexChanged(); + void fillWidthChanged(); + void reservedSpaceChanged(); + void viewChanged(); + void preventStealingChanged(); + +private: + int m_index = -1; + bool m_fillWidth = false; + qreal m_reservedSpace = 0; + QPointer m_view; + QPointer m_originalParent; + bool m_customFillWidth = false; + bool m_customReservedSpace = false; + bool m_shouldDeleteOnRemove = true; + bool m_preventStealing = false; +}; + + +/** + * ColumnView is a container that lays out items horizontally in a row, + * when not all items fit in the ColumnView, it will behave ike a Flickable and will be a scrollable view which shows only a determined number of columns. + * The columns can either all have the same fixed size (recommended), + * size themselves with implicitWidth, or automatically expand to take all the available width: by default the last column will always be the expanding one. + * Items inside the Columnview can access info of the view and set layouting hints via the Columnview attached property. + * + * This is the base for the implementation of PageRow + * @since 2.7 + */ +class ColumnView : public QQuickItem +{ + Q_OBJECT + + /** + * The strategy to follow while automatically resizing the columns, + * the enum can have the following values: + * * FixedColumns: every column is fixed at the same width of the columnWidth property + * * DynamicColumns: columns take their width from their implicitWidth + * * SingleColumn: only one column at a time is shown, as wide as the viewport, eventual reservedSpace on the column's atttached property is ignored + */ + Q_PROPERTY(ColumnResizeMode columnResizeMode READ columnResizeMode WRITE setColumnResizeMode NOTIFY columnResizeModeChanged) + + /** + * The width of all columns when columnResizeMode is FixedColumns + */ + Q_PROPERTY(qreal columnWidth READ columnWidth WRITE setColumnWidth NOTIFY columnWidthChanged) + + /** + * How many columns this view containsItem*/ + Q_PROPERTY(int count READ count NOTIFY countChanged) + + /** + * The position of the currently active column. The current column will also have keyboard focus + */ + Q_PROPERTY(int currentIndex READ currentIndex WRITE setCurrentIndex NOTIFY currentIndexChanged) + + /** + * The currently active column. The current column will also have keyboard focus + */ + Q_PROPERTY(QQuickItem *currentItem READ currentItem NOTIFY currentItemChanged) + + /** + * The main content item of this view: it's the parent of the column items + */ + Q_PROPERTY(QQuickItem *contentItem READ contentItem CONSTANT) + + /** + * The value of the horizontal scroll of the view, in pixels + */ + Q_PROPERTY(qreal contentX READ contentX WRITE setContentX NOTIFY contentXChanged) + + /** + * The compound width of all columns in the view + */ + Q_PROPERTY(qreal contentWidth READ contentWidth NOTIFY contentWidthChanged) + + /** + * The duration for scrolling animations + */ + Q_PROPERTY(int scrollDuration READ scrollDuration WRITE setScrollDuration NOTIFY scrollDurationChanged) + + /** + * True if columns should be visually separed by a separator line + */ + Q_PROPERTY(bool separatorVisible READ separatorVisible WRITE setSeparatorVisible NOTIFY separatorVisibleChanged) + + /** + * The list of all visible column items that are at least partially in the viewport at any given moment + */ + Q_PROPERTY(QList visibleItems READ visibleItems NOTIFY visibleItemsChanged) + + /** + * The first of visibleItems provided from convenience + */ + Q_PROPERTY(QQuickItem *firstVisibleItem READ firstVisibleItem NOTIFY firstVisibleItemChanged) + + /** + * The last of visibleItems provided from convenience + */ + Q_PROPERTY(QQuickItem *lastVisibleItem READ lastVisibleItem NOTIFY lastVisibleItemChanged) + + // Properties to make it similar to Flickable + /** + * True when the user is dragging around with touch gestures the view contents + */ + Q_PROPERTY(bool dragging READ dragging NOTIFY draggingChanged) + + /** + * True both when the user is dragging around with touch gestures the view contents or the view is animating + */ + Q_PROPERTY(bool moving READ moving NOTIFY movingChanged) + + /** + * True if it supports moving the contents by dragging + */ + Q_PROPERTY(bool interactive READ interactive WRITE setInteractive NOTIFY interactiveChanged) + + // Default properties + /** + * Every column item the view contains + */ + Q_PROPERTY(QQmlListProperty contentChildren READ contentChildren NOTIFY contentChildrenChanged FINAL) + /** + * every item declared inside the view, both visual and non-visual items + */ + Q_PROPERTY(QQmlListProperty contentData READ contentData FINAL) + Q_CLASSINFO("DefaultProperty", "contentData") + + Q_ENUMS(ColumnResizeMode) + +public: + enum ColumnResizeMode { + FixedColumns = 0, + DynamicColumns, + SingleColumn + }; + ColumnView(QQuickItem *parent = nullptr); + ~ColumnView(); + + // QML property accessors + ColumnResizeMode columnResizeMode() const; + void setColumnResizeMode(ColumnResizeMode mode); + + qreal columnWidth() const; + void setColumnWidth(qreal width); + + int currentIndex() const; + void setCurrentIndex(int index); + + int scrollDuration() const; + void setScrollDuration(int duration); + + bool separatorVisible() const; + void setSeparatorVisible(bool visible); + + int count() const; + + QQuickItem *currentItem(); + + //NOTE: It's a QList as QML can't corectly build an Array out of QList + QList visibleItems() const; + QQuickItem *firstVisibleItem() const; + QQuickItem *lastVisibleItem() const; + + + QQuickItem *contentItem() const; + + QQmlListProperty contentChildren(); + QQmlListProperty contentData(); + + bool dragging() const; + bool moving() const; + qreal contentWidth() const; + + qreal contentX() const; + void setContentX(qreal x) const; + + bool interactive() const; + void setInteractive(bool interactive); + + // Api not intended for QML use + //can't do overloads in QML + QQuickItem *removeItem(QQuickItem *item); + QQuickItem *removeItem(int item); + + // QML attached property + static ColumnViewAttached *qmlAttachedProperties(QObject *object); + +public Q_SLOTS: + /** + * Pushes a new item at the end of the view + * @param item the new item which will be reparented and managed + */ + void addItem(QQuickItem *item); + + /** + * Inserts a new item in the view at a given position. + * The current Item will not be changed, currentIndex will be adjusted + * accordingly if needed to keep the same current item. + * @param pos the position we want the new item to be inserted in + * @param item the new item which will be reparented and managed + */ + void insertItem(int pos, QQuickItem *item); + + /** + * Move an item inside the view. + * The currentIndex property may be changed in order to keep currentItem the same. + * @param from the old position + * @param to the new position + */ + void moveItem(int from, int to); + + /** + * Removes an item from the view. + * Items will be reparented to their old parent. + * If they have JavaScript ownership and they didn't have an old parent, they will be destroyed. + * CurrentIndex may be changed in order to keep the same currentItem + * @param item it can either be a pointer of an item or an integer specifying the position to remove + * @returns the item that has just been removed + */ + QQuickItem *removeItem(const QVariant &item); + + /** + * Removes all the items after item. Starting from the last column, every column will be removed until item is found, which will be left in place. + * Items will be reparented to their old parent. + * If they have JavaScript ownership and they didn't have an old parent, they will be destroyed + * @param item the item which will be the new last one of the row. + * @returns the last item that has been removed + */ + QQuickItem *pop(QQuickItem *item); + + /** + * Removes every item in the view. + * Items will be reparented to their old parent. + * If they have JavaScript ownership and they didn't have an old parent, they will be destroyed + */ + void clear(); + + /** + * @returns true if the view contains the given item + */ + bool containsItem(QQuickItem *item); + +protected: + void classBegin() override; + void componentComplete() override; + void updatePolish() override; + void itemChange(QQuickItem::ItemChange change, const QQuickItem::ItemChangeData &value) override; + void geometryChanged(const QRectF &newGeometry, const QRectF &oldGeometry) override; + bool childMouseEventFilter(QQuickItem *item, QEvent *event) override; + void mousePressEvent(QMouseEvent *event) override; + void mouseMoveEvent(QMouseEvent *event) override; + void mouseReleaseEvent(QMouseEvent *event) override; + void mouseUngrabEvent() override; + +Q_SIGNALS: + /** + * A new item has been inserted + * @param position where the page has been inserted + * @param item a pointer to the new item + */ + void itemInserted(int position, QQuickItem *item); + + /** + * An item has just been removed from the view + * @param item a pointer to the item that has just been removed + */ + void itemRemoved(QQuickItem *item); + + // Property notifiers + void contentChildrenChanged(); + void columnResizeModeChanged(); + void columnWidthChanged(); + void currentIndexChanged(); + void currentItemChanged(); + void visibleItemsChanged(); + void countChanged(); + void draggingChanged(); + void movingChanged(); + void contentXChanged(); + void contentWidthChanged(); + void interactiveChanged(); + void scrollDurationChanged(); + void separatorVisibleChanged(); + void firstVisibleItemChanged(); + void lastVisibleItemChanged(); + +private: + static void contentChildren_append(QQmlListProperty *prop, QQuickItem *object); + static int contentChildren_count(QQmlListProperty *prop); + static QQuickItem *contentChildren_at(QQmlListProperty *prop, int index); + static void contentChildren_clear(QQmlListProperty *prop); + + static void contentData_append(QQmlListProperty *prop, QObject *object); + static int contentData_count(QQmlListProperty *prop); + static QObject *contentData_at(QQmlListProperty *prop, int index); + static void contentData_clear(QQmlListProperty *prop); + + + QList m_contentData; + + ContentItem *m_contentItem; + QPointer m_currentItem; + + static QHash m_attachedObjects; + qreal m_oldMouseX = -1.0; + qreal m_startMouseX = -1.0; + int m_currentIndex = -1; + + bool m_interactive = true; + bool m_dragging = false; + bool m_moving = false; + bool m_separatorVisible = true; + bool m_complete = false; +}; + +QML_DECLARE_TYPEINFO(ColumnView, QML_HAS_ATTACHED_PROPERTIES) diff --git a/src/columnview.cpp b/src/columnview.cpp new file mode 100644 --- /dev/null +++ b/src/columnview.cpp @@ -0,0 +1,1276 @@ +/* + * Copyright 2019 Marco Martin + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU Library General Public License as + * published by the Free Software Foundation; either version 2, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details + * + * You should have received a copy of the GNU Library General Public + * License along with this program; if not, write to the + * Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#include "columnview.h" +#include "columnview_p.h" + +#include +#include +#include +#include +#include +#include +#include +#include + + +QHash ColumnView::m_attachedObjects = QHash(); + +class QmlComponentsPoolSingleton +{ +public: + QmlComponentsPoolSingleton() + {} + + QmlComponentsPool self; +}; + +Q_GLOBAL_STATIC(QmlComponentsPoolSingleton, privateQmlComponentsPoolSelf) + + +QmlComponentsPool::QmlComponentsPool(QObject *parent) + : QObject(parent) +{} + +void QmlComponentsPool::initialize(QQmlEngine *engine) +{ + if (!engine || m_instance) { + return; + } + + QQmlComponent *component = new QQmlComponent(engine, this); + + component->setData(QByteArrayLiteral("import QtQuick 2.7\n" + "import org.kde.kirigami 2.7 as Kirigami\n" + "QtObject {\n" + "id: root\n" + "readonly property Kirigami.Units units: Kirigami.Units\n" + "readonly property Component separator: Kirigami.Separator {" + "property Item column\n" + "visible: column.Kirigami.ColumnView.view.contentX < column.x;" + "anchors.top: column.top;" + "anchors.bottom: column.bottom;" + "}" + "}"), QUrl()); + + m_instance = component->create(); + //qWarning()<errors(); + Q_ASSERT(m_instance); + + m_separatorComponent = m_instance->property("separator").value(); + Q_ASSERT(m_separatorComponent); + + m_units = m_instance->property("units").value(); + Q_ASSERT(m_units); + + connect(m_units, SIGNAL(gridUnitChanged()), this, SIGNAL(gridUnitChanged())); + connect(m_units, SIGNAL(longDurationChanged()), this, SIGNAL(longDurationChanged())); +} + +QmlComponentsPool::~QmlComponentsPool() +{} + + +///////// + +ColumnViewAttached::ColumnViewAttached(QObject *parent) + : QObject(parent) +{} + +ColumnViewAttached::~ColumnViewAttached() +{} + +void ColumnViewAttached::setIndex(int index) +{ + if (!m_customFillWidth && m_view) { + const bool oldFillWidth = m_fillWidth; + m_fillWidth = index == m_view->count() - 1; + if (oldFillWidth != m_fillWidth) { + emit fillWidthChanged(); + } + } + + if (index == m_index) { + return; + } + + m_index = index; + emit indexChanged(); +} + +int ColumnViewAttached::index() const +{ + return m_index; +} + +void ColumnViewAttached::setFillWidth(bool fill) +{ + if (m_view) { + disconnect(m_view.data(), &ColumnView::countChanged, this, nullptr); + } + m_customFillWidth = true; + + if (fill == m_fillWidth) { + return; + } + + m_fillWidth = fill; + emit fillWidthChanged(); +} + +bool ColumnViewAttached::fillWidth() const +{ + return m_fillWidth; +} + +qreal ColumnViewAttached::reservedSpace() const +{ + return m_reservedSpace; +} + +void ColumnViewAttached::setReservedSpace(qreal space) +{ + if (m_view) { + disconnect(m_view.data(), &ColumnView::columnWidthChanged, this, nullptr); + } + m_customReservedSpace = true; + + if (qFuzzyCompare(space, m_reservedSpace)) { + return; + } + + m_reservedSpace = space; + emit reservedSpaceChanged(); +} + +ColumnView *ColumnViewAttached::view() +{ + return m_view; +} + +void ColumnViewAttached::setView(ColumnView *view) +{ + if (view == m_view) { + return; + } + + if (m_view) { + disconnect(m_view.data(), nullptr, this, nullptr); + } + m_view = view; + + if (!m_customFillWidth && m_view) { + m_fillWidth = m_index == m_view->count() - 1; + connect(m_view.data(), &ColumnView::countChanged, this, [this]() { + m_fillWidth = m_index == m_view->count() - 1; + emit fillWidthChanged(); + }); + } + if (!m_customReservedSpace && m_view) { + m_reservedSpace = m_view->columnWidth(); + connect(m_view.data(), &ColumnView::columnWidthChanged, this, [this]() { + m_reservedSpace = m_view->columnWidth(); + emit reservedSpaceChanged(); + }); + } + + emit viewChanged(); +} + +QQuickItem *ColumnViewAttached::originalParent() const +{ + return m_originalParent; +} + +void ColumnViewAttached::setOriginalParent(QQuickItem *parent) +{ + m_originalParent = parent; +} + +bool ColumnViewAttached::shouldDeleteOnRemove() const +{ + return m_shouldDeleteOnRemove; +} + +void ColumnViewAttached::setShouldDeleteOnRemove(bool del) +{ + m_shouldDeleteOnRemove = del; +} + +bool ColumnViewAttached::preventStealing() const +{ + return m_preventStealing; +} + +void ColumnViewAttached::setPreventStealing(bool prevent) +{ + if (prevent == m_preventStealing) { + return; + } + + m_preventStealing = prevent; + emit preventStealingChanged(); +} + + + +///////// + +ContentItem::ContentItem(ColumnView *parent) + : QQuickItem(parent), + m_view(parent) +{ + m_slideAnim = new QPropertyAnimation(this); + m_slideAnim->setTargetObject(this); + m_slideAnim->setPropertyName("x"); + //NOTE: the duration will be taked from kirigami units upon classBegin + m_slideAnim->setDuration(0); + m_slideAnim->setEasingCurve(QEasingCurve(QEasingCurve::InOutQuad)); + connect(m_slideAnim, &QPropertyAnimation::finished, this, [this] () { + if (!m_view->currentItem()) { + m_view->setCurrentIndex(m_items.indexOf(m_viewAnchorItem)); + } else { + QRectF mapped = m_view->currentItem()->mapRectToItem(this, QRectF(QPointF(0, 0), m_view->currentItem()->size())); + if (!QRectF(QPointF(0, 0), size()).intersects(mapped)) { + m_view->setCurrentIndex(m_items.indexOf(m_viewAnchorItem)); + } + } + }); +} + +ContentItem::~ContentItem() +{} + +void ContentItem::setBoundedX(qreal x) +{ + if (!parentItem()) { + return; + } + m_slideAnim->stop(); + setX(qRound(qBound(qMin(0.0, -width()+parentItem()->width()), x, 0.0))); +} + +void ContentItem::animateX(qreal newX) +{ + if (!parentItem()) { + return; + } + + const qreal to = qRound(qBound(qMin(0.0, -width()+parentItem()->width()), newX, 0.0)); + + m_slideAnim->stop(); + m_slideAnim->setStartValue(x()); + m_slideAnim->setEndValue(to); + m_slideAnim->start(); +} + +void ContentItem::snapToItem() +{ + QQuickItem *firstItem = childAt(-x(), 0); + if (!firstItem) { + return; + } + QQuickItem *nextItem = childAt(firstItem->x() + firstItem->width() + 1, 0); + + //need to make the last item visible? + if (nextItem && width() - (-x() + m_view->width()) < -x() - firstItem->x()) { + m_viewAnchorItem = nextItem; + animateX(-nextItem->x()); + + //The first one found? + } else if (-x() <= firstItem->x() + firstItem->width()/2 || !nextItem) { + m_viewAnchorItem = firstItem; + animateX(-firstItem->x()); + + //the second? + } else { + m_viewAnchorItem = nextItem; + animateX(-nextItem->x()); + } +} + +qreal ContentItem::childWidth(QQuickItem *child) +{ + if (!parentItem()) { + return 0.0; + } + + ColumnViewAttached *attached = qobject_cast(qmlAttachedPropertiesObject(child, true)); + + if (m_columnResizeMode == ColumnView::SingleColumn) { + return qRound(parentItem()->width()); + + } else if (attached->fillWidth()) { + return qRound(qBound(m_columnWidth, (parentItem()->width() - attached->reservedSpace()), parentItem()->width())); + + } else if (m_columnResizeMode == ColumnView::FixedColumns) { + return qRound(qMin(parentItem()->width(), m_columnWidth)); + + // DynamicColumns + } else { + //TODO:look for Layout size hints + qreal width = child->implicitWidth(); + + if (width < 1.0) { + width = m_columnWidth; + } + + return qRound(qMin(m_view->width(), width)); + } +} + +void ContentItem::layoutItems() +{ + qreal partialWidth = 0; + int i = 0; + for (QQuickItem *child : m_items) { + if (child->isVisible()) { + child->setSize(QSizeF(childWidth(child), height())); + child->setPosition(QPointF(partialWidth, 0.0)); + partialWidth += child->width(); + } + ColumnViewAttached *attached = qobject_cast(qmlAttachedPropertiesObject(child, true)); + attached->setIndex(i++); + } + setWidth(partialWidth); + + const qreal newContentX = m_viewAnchorItem ? -m_viewAnchorItem->x() : 0.0; + if (m_shouldAnimate) { + animateX(newContentX); + } else { + setBoundedX(newContentX); + } + setY(0); + updateVisibleItems(); +} + +void ContentItem::updateVisibleItems() +{ + QList newItems; + + for (auto *item : m_items) { + if (item->isVisible() && item->x() + x() < width() && item->x() + item->width() + x() > 0) { + newItems << item; + } + } + + const QQuickItem *oldFirstVisibleItem = m_visibleItems.isEmpty() ? nullptr : qobject_cast(m_visibleItems.first()); + const QQuickItem *oldLastVisibleItem = m_visibleItems.isEmpty() ? nullptr : qobject_cast(m_visibleItems.last()); + + if (newItems != m_visibleItems) { + m_visibleItems = newItems; + emit m_view->visibleItemsChanged(); + if (!newItems.isEmpty() && m_visibleItems.first() != oldFirstVisibleItem) { + emit m_view->firstVisibleItemChanged(); + } + if (!newItems.isEmpty() && m_visibleItems.last() != oldLastVisibleItem) { + emit m_view->lastVisibleItemChanged(); + } + } +} + +void ContentItem::forgetItem(QQuickItem *item) +{ + if (!m_items.contains(item)) { + return; + } + + ColumnViewAttached *attached = qobject_cast(qmlAttachedPropertiesObject(item, true)); + attached->setView(nullptr); + attached->setIndex(-1); + + disconnect(attached, nullptr, this, nullptr); + disconnect(item, nullptr, this, nullptr); + disconnect(item, nullptr, m_view, nullptr); + + QQuickItem *separatorItem = m_separators.take(item); + if (separatorItem) { + separatorItem->deleteLater(); + } + + const int index = m_items.indexOf(item); + m_items.removeAll(item); + updateVisibleItems(); + m_shouldAnimate = true; + m_view->polish(); + item->setVisible(false); + + if (index <= m_view->currentIndex()) { + m_view->setCurrentIndex(qBound(0, index - 1, m_items.count() - 1)); + } + emit m_view->countChanged(); +} + +QQuickItem *ContentItem::ensureSeparator(QQuickItem *item) +{ + QQuickItem *separatorItem = m_separators.value(item); + + if (!separatorItem) { + separatorItem = qobject_cast(privateQmlComponentsPoolSelf->self.m_separatorComponent->beginCreate(QQmlEngine::contextForObject(item))); + if (separatorItem) { + separatorItem->setParentItem(item); + separatorItem->setZ(9999); + separatorItem->setProperty("column", QVariant::fromValue(item)); + privateQmlComponentsPoolSelf->self.m_separatorComponent->completeCreate(); + m_separators[item] = separatorItem; + } + } + + return separatorItem; +} + +void ContentItem::itemChange(QQuickItem::ItemChange change, const QQuickItem::ItemChangeData &value) +{ + switch (change) { + case QQuickItem::ItemChildAddedChange: { + ColumnViewAttached *attached = qobject_cast(qmlAttachedPropertiesObject(value.item, true)); + attached->setView(m_view); + + //connect(attached, &ColumnViewAttached::fillWidthChanged, m_view, &ColumnView::polish); + connect(attached, &ColumnViewAttached::fillWidthChanged, this, [this, attached](){ + m_view->polish(); + + }); + connect(attached, &ColumnViewAttached::reservedSpaceChanged, m_view, &ColumnView::polish); + + value.item->setVisible(true); + + if (!m_items.contains(value.item)) { + connect(value.item, &QQuickItem::widthChanged, m_view, &ColumnView::polish); + m_items << value.item; + } + + if (m_view->separatorVisible()) { + ensureSeparator(value.item); + } + + m_shouldAnimate = true; + m_view->polish(); + emit m_view->countChanged(); + break; + } + case QQuickItem::ItemChildRemovedChange: { + forgetItem(value.item); + break; + } + case QQuickItem::ItemVisibleHasChanged: + updateVisibleItems(); + break; + default: + break; + } + QQuickItem::itemChange(change, value); +} + +void ContentItem::geometryChanged(const QRectF &newGeometry, const QRectF &oldGeometry) +{ + updateVisibleItems(); + QQuickItem::geometryChanged(newGeometry, oldGeometry); +} + +void ContentItem::syncItemsOrder() +{ + if (m_items == childItems()) { + return; + } + + m_items = childItems(); + polish(); +} + +void ContentItem::updateRepeaterModel() +{ + if (!sender()) { + return; + } + + QObject *modelObj = sender()->property("model").value(); + + if (!modelObj) { + m_models.remove(sender()); + return; + } + + if (m_models[sender()]) { + disconnect(m_models[sender()], nullptr, this, nullptr); + } + + m_models[sender()] = modelObj; + + QAbstractItemModel *qaim = qobject_cast(modelObj); + + if (qaim) { + connect(qaim, &QAbstractItemModel::rowsMoved, this, &ContentItem::syncItemsOrder); + + } else { + connect(modelObj, SIGNAL(childrenChanged()), this, SLOT(syncItemsOrder())); + } +} + + + + + + + +ColumnView::ColumnView(QQuickItem *parent) + : QQuickItem(parent), + m_contentItem(nullptr) +{ + //NOTE: this is to *not* trigger itemChange + m_contentItem = new ContentItem(this); + setAcceptedMouseButtons(Qt::LeftButton); + setFiltersChildMouseEvents(true); + + connect(m_contentItem->m_slideAnim, &QPropertyAnimation::finished, this, [this] () { + m_moving = false; + emit movingChanged(); + }); + connect(m_contentItem, &ContentItem::widthChanged, this, &ColumnView::contentWidthChanged); + connect(m_contentItem, &ContentItem::xChanged, this, &ColumnView::contentXChanged); +} + +ColumnView::~ColumnView() +{ +} + +ColumnView::ColumnResizeMode ColumnView::columnResizeMode() const +{ + return m_contentItem->m_columnResizeMode; +} + +void ColumnView::setColumnResizeMode(ColumnResizeMode mode) +{ + if (m_contentItem->m_columnResizeMode == mode) { + return; + } + + m_contentItem->m_columnResizeMode = mode; + if (mode == SingleColumn && m_currentItem) { + m_contentItem->m_viewAnchorItem = m_currentItem; + } + m_contentItem->m_shouldAnimate = false; + polish(); + emit columnResizeModeChanged(); +} + +qreal ColumnView::columnWidth() const +{ + return m_contentItem->m_columnWidth; +} + +void ColumnView::setColumnWidth(qreal width) +{ + // Always forget the internal binding when the user sets anything, even the same value + disconnect(&privateQmlComponentsPoolSelf->self, &QmlComponentsPool::gridUnitChanged, this, nullptr); + + if (m_contentItem->m_columnWidth == width) { + return; + } + + m_contentItem->m_columnWidth = width; + m_contentItem->m_shouldAnimate = false; + polish(); + emit columnWidthChanged(); +} + +int ColumnView::currentIndex() const +{ + return m_currentIndex; +} + +void ColumnView::setCurrentIndex(int index) +{ + if (m_currentIndex == index || index < -1 || index >= m_contentItem->m_items.count()) { + return; + } + + m_currentIndex = index; + + if (index == -1) { + m_currentItem.clear(); + + } else { + m_currentItem = m_contentItem->m_items[index]; + Q_ASSERT(m_currentItem); + m_currentItem->forceActiveFocus(); + + // If the current item is not on view, scroll + QRectF mappedCurrent = m_currentItem->mapRectToItem(this, + QRectF(QPointF(0, 0), + m_currentItem->size())); + + if (m_contentItem->m_slideAnim->state() == QAbstractAnimation::Running) { + mappedCurrent.moveLeft(mappedCurrent.left() + m_contentItem->x() + m_contentItem->m_slideAnim->endValue().toInt()); + } + + //m_contentItem->m_slideAnim->stop(); + + QRectF contentsRect(QPointF(0, 0), size()); + + m_contentItem->m_shouldAnimate = true; + + if (!contentsRect.contains(mappedCurrent)) { + m_contentItem->m_viewAnchorItem = m_currentItem; + m_contentItem->animateX(-m_currentItem->x()); + } else { + m_contentItem->snapToItem(); + } + } + + emit currentIndexChanged(); + emit currentItemChanged(); +} + +QQuickItem *ColumnView::currentItem() +{ + return m_currentItem; +} + +QListColumnView::visibleItems() const +{ + return m_contentItem->m_visibleItems; +} + +QQuickItem *ColumnView::firstVisibleItem() const +{ + if (m_contentItem->m_visibleItems.isEmpty()) { + return nullptr; + } + + return qobject_cast(m_contentItem->m_visibleItems.first()); +} + +QQuickItem *ColumnView::lastVisibleItem() const +{ + if (m_contentItem->m_visibleItems.isEmpty()) { + return nullptr; + } + + return qobject_cast(m_contentItem->m_visibleItems.last()); +} + +int ColumnView::count() const +{ + return m_contentItem->m_items.count(); +} + +QQuickItem *ColumnView::contentItem() const +{ + return m_contentItem; +} + +int ColumnView::scrollDuration() const +{ + return m_contentItem->m_slideAnim->duration(); +} + +void ColumnView::setScrollDuration(int duration) +{ + disconnect(&privateQmlComponentsPoolSelf->self, &QmlComponentsPool::longDurationChanged, this, nullptr); + + if (m_contentItem->m_slideAnim->duration() == duration) { + return; + } + + m_contentItem->m_slideAnim->setDuration(duration); + emit scrollDurationChanged(); +} + +bool ColumnView::separatorVisible() const +{ + return m_separatorVisible; +} + +void ColumnView::setSeparatorVisible(bool visible) +{ + if (visible == m_separatorVisible) { + return; + } + + m_separatorVisible = visible; + + if (visible) { + for (QQuickItem *item : m_contentItem->m_items) { + QQuickItem *sep = m_contentItem->ensureSeparator(item); + if (sep) { + sep->setVisible(true); + } + } + } else { + for (QQuickItem *sep : m_contentItem->m_separators.values()) { + sep->setVisible(false); + } + } + + emit separatorVisibleChanged(); +} + +bool ColumnView::dragging() const +{ + return m_dragging; +} + +bool ColumnView::moving() const +{ + return m_moving; +} + +qreal ColumnView::contentWidth() const +{ + return m_contentItem->width(); +} + +qreal ColumnView::contentX() const +{ + return -m_contentItem->x(); +} + +void ColumnView::setContentX(qreal x) const +{ + m_contentItem->setX(qRound(-x)); +} + +bool ColumnView::interactive() const +{ + return m_interactive; +} + +void ColumnView::setInteractive(bool interactive) +{ + if (m_interactive != interactive) { + return; + } + + m_interactive = interactive; + + if (!m_interactive) { + if (m_dragging) { + m_dragging = false; + emit draggingChanged(); + } + + m_contentItem->snapToItem(); + setKeepMouseGrab(false); + } + + emit interactiveChanged(); +} + +void ColumnView::addItem(QQuickItem *item) +{ + insertItem(m_contentItem->m_items.length(), item); +} + +void ColumnView::insertItem(int pos, QQuickItem *item) +{ + if (!item || m_contentItem->m_items.contains(item)) { + return; + } + + m_contentItem->m_items.insert(qBound(0, pos, m_contentItem->m_items.length()), item); + + ColumnViewAttached *attached = qobject_cast(qmlAttachedPropertiesObject(item, true)); + attached->setOriginalParent(item->parentItem()); + attached->setShouldDeleteOnRemove(item->parentItem() == nullptr && QQmlEngine::objectOwnership(item) == QQmlEngine::JavaScriptOwnership); + item->setParentItem(m_contentItem); + + item->forceActiveFocus(); + // We layout immediately to be sure all geometries are final after the return of this call + m_contentItem->m_shouldAnimate = false; + m_contentItem->layoutItems(); + emit contentChildrenChanged(); + + // In order to keep the same current item we need to increase the current index if displaced + // NOTE: just updating m_currentIndex does *not* update currentItem (which is what we need atm) while setCurrentIndex will update also currentItem + if (m_currentIndex >= pos) { + ++m_currentIndex; + emit currentIndexChanged(); + } + + emit itemInserted(pos, item); +} + +void ColumnView::moveItem(int from, int to) +{ + if (m_contentItem->m_items.isEmpty() + || from < 0 || from >= m_contentItem->m_items.length() + || to < 0 || to >= m_contentItem->m_items.length()) { + return; + } + + m_contentItem->m_items.move(from, to); + m_contentItem->m_shouldAnimate = true; + + if (from == m_currentIndex) { + m_currentIndex = to; + emit currentIndexChanged(); + } else if (from < m_currentIndex && to > m_currentIndex) { + --m_currentIndex; + emit currentIndexChanged(); + } else if (from > m_currentIndex && to <= m_currentIndex) { + ++m_currentIndex; + emit currentIndexChanged(); + } + + polish(); +} + +QQuickItem *ColumnView::removeItem(const QVariant &item) +{ + if (item.canConvert()) { + return removeItem(item.value()); + } else if (item.canConvert()) { + return removeItem(item.toInt()); + } else { + return nullptr; + } +} + +QQuickItem *ColumnView::removeItem(QQuickItem *item) +{ + if (m_contentItem->m_items.isEmpty() || !m_contentItem->m_items.contains(item)) { + return nullptr; + } + + const int index = m_contentItem->m_items.indexOf(item); + + // In order to keep the same current item we need to increase the current index if displaced + if (m_currentIndex >= index) { + setCurrentIndex(m_currentIndex - 1); + } + + m_contentItem->forgetItem(item); + + ColumnViewAttached *attached = qobject_cast(qmlAttachedPropertiesObject(item, false)); + + if (attached && attached->shouldDeleteOnRemove()) { + item->deleteLater(); + } else { + item->setParentItem(attached ? attached->originalParent() : nullptr); + } + + emit itemRemoved(item); + + return item; +} + +QQuickItem *ColumnView::removeItem(int pos) +{ + if (m_contentItem->m_items.isEmpty() + || pos < 0 || pos >= m_contentItem->m_items.length()) { + return nullptr; + } + + return removeItem(m_contentItem->m_items[pos]); +} + +QQuickItem *ColumnView::pop(QQuickItem *item) +{ + QQuickItem *removed = nullptr; + + while (!m_contentItem->m_items.isEmpty() && m_contentItem->m_items.last() != item) { + removed = removeItem(m_contentItem->m_items.last()); + // if no item has been passed, just pop one + if (!item) { + break; + } + } + return removed; +} + +void ColumnView::clear() +{ + ColumnViewAttached *attached = nullptr; + + for (QQuickItem *item : m_contentItem->m_items) { + attached = qobject_cast(qmlAttachedPropertiesObject(item, false)); + if (attached && attached->shouldDeleteOnRemove()) { + item->deleteLater(); + } else { + item->setParentItem(attached ? attached->originalParent() : nullptr); + } + } + m_contentItem->m_items.clear(); + emit contentChildrenChanged(); +} + +bool ColumnView::containsItem(QQuickItem *item) +{ + return m_contentItem->m_items.contains(item); +} + +ColumnViewAttached *ColumnView::qmlAttachedProperties(QObject *object) +{ + return new ColumnViewAttached(object); +} + +void ColumnView::geometryChanged(const QRectF &newGeometry, const QRectF &oldGeometry) +{ + m_contentItem->setHeight(newGeometry.height()); + m_contentItem->m_shouldAnimate = false; + polish(); + + m_contentItem->updateVisibleItems(); + QQuickItem::geometryChanged(newGeometry, oldGeometry); +} + +bool ColumnView::childMouseEventFilter(QQuickItem *item, QEvent *event) +{ + if (!m_interactive || item == m_contentItem) { + return QQuickItem::childMouseEventFilter(item, event); + } + + switch (event->type()) { + case QEvent::MouseButtonPress: { + m_contentItem->m_slideAnim->stop(); + if (item->property("preventStealing").toBool()) { + m_contentItem->snapToItem(); + return false; + } + QMouseEvent *me = static_cast(event); + m_oldMouseX = m_startMouseX = mapFromItem(item, me->localPos()).x(); + + me->setAccepted(false); + setKeepMouseGrab(false); + + // On press, we set the current index of the view to the root item + QQuickItem *candidateItem = item; + while (candidateItem->parentItem() && candidateItem->parentItem() != m_contentItem) { + candidateItem = candidateItem->parentItem(); + } + if (candidateItem->parentItem() == m_contentItem) { + setCurrentIndex(m_contentItem->m_items.indexOf(candidateItem)); + } + + break; + } + case QEvent::MouseMove: { + if ((!keepMouseGrab() && item->keepMouseGrab()) || item->property("preventStealing").toBool()) { + m_contentItem->snapToItem(); + return false; + } + + QQuickItem *candidateItem = item; + while (candidateItem->parentItem() && candidateItem->parentItem() != m_contentItem) { + candidateItem = candidateItem->parentItem(); + } + if (candidateItem->parentItem() == m_contentItem) { + ColumnViewAttached *attached = qobject_cast(qmlAttachedPropertiesObject(item, true)); + if (attached->preventStealing()) { + return false; + } + } + + QMouseEvent *me = static_cast(event); + const QPointF pos = mapFromItem(item, me->localPos()); + + const bool wasDragging = m_dragging; + // If a drag happened, start to steal all events, use startDragDistance * 2 to give time to widgets to take the mouse grab by themselves + m_dragging = keepMouseGrab() || qAbs(mapFromItem(item, me->localPos()).x() - m_startMouseX) > qApp->styleHints()->startDragDistance() * 3; + + if (m_dragging != wasDragging) { + m_moving = true; + emit movingChanged(); + emit draggingChanged(); + } + + if (m_dragging) { + m_contentItem->setBoundedX(m_contentItem->x() + pos.x() - m_oldMouseX); + } + + m_oldMouseX = pos.x(); + + setKeepMouseGrab(m_dragging); + me->setAccepted(m_dragging); + return m_dragging; + break; + } + case QEvent::MouseButtonRelease: { + m_contentItem->snapToItem(); + if (m_dragging) { + m_dragging = false; + emit draggingChanged(); + } + + if (item->property("preventStealing").toBool()) { + return false; + } + QMouseEvent *me = static_cast(event); + event->accept(); + + //if a drag happened, don't pass the event + const bool block = keepMouseGrab(); + setKeepMouseGrab(false); + + me->setAccepted(block); + return block; + break; + } + default: + break; + } + + return QQuickItem::childMouseEventFilter(item, event); +} + +void ColumnView::mousePressEvent(QMouseEvent *event) +{ + if (!m_interactive) { + return; + } + + m_contentItem->snapToItem(); + m_oldMouseX = event->localPos().x(); + m_startMouseX = event->localPos().x(); + setKeepMouseGrab(false); + event->accept(); +} + +void ColumnView::mouseMoveEvent(QMouseEvent *event) +{ + if (!m_interactive) { + return; + } + + const bool wasDragging = m_dragging; + // Same startDragDistance * 2 as the event filter + m_dragging = keepMouseGrab() || qAbs(event->localPos().x() - m_startMouseX) > qApp->styleHints()->startDragDistance() * 2; + if (m_dragging != wasDragging) { + m_moving = true; + emit movingChanged(); + emit draggingChanged(); + } + + setKeepMouseGrab(m_dragging); + + if (m_dragging) { + m_contentItem->setBoundedX(m_contentItem->x() + event->pos().x() - m_oldMouseX); + } + + m_oldMouseX = event->pos().x(); + event->accept(); +} + +void ColumnView::mouseReleaseEvent(QMouseEvent *event) +{ + if (!m_interactive) { + return; + } + + if (m_dragging) { + m_dragging = false; + emit draggingChanged(); + } + + m_contentItem->snapToItem(); + setKeepMouseGrab(false); + event->accept(); +} + +void ColumnView::mouseUngrabEvent() +{ + if (m_dragging) { + m_dragging = false; + emit draggingChanged(); + } + + m_contentItem->snapToItem(); + setKeepMouseGrab(false); +} + +void ColumnView::classBegin() +{ + privateQmlComponentsPoolSelf->self.initialize(qmlEngine(this)); + + auto syncColumnWidth = [this]() { + m_contentItem->m_columnWidth = privateQmlComponentsPoolSelf->self.m_units->property("gridUnit").toInt() * 20; + emit columnWidthChanged(); + }; + + connect(&privateQmlComponentsPoolSelf->self, &QmlComponentsPool::gridUnitChanged, this, syncColumnWidth); + syncColumnWidth(); + + auto syncDuration = [this]() { + m_contentItem->m_slideAnim->setDuration(privateQmlComponentsPoolSelf->self.m_units->property("longDuration").toInt()); + emit scrollDurationChanged(); + }; + + connect(&privateQmlComponentsPoolSelf->self, &QmlComponentsPool::longDurationChanged, this, syncDuration); + syncDuration(); + + QQuickItem::classBegin(); +} + +void ColumnView::componentComplete() +{ + m_complete = true; + QQuickItem::componentComplete(); +} + +void ColumnView::updatePolish() +{ + m_contentItem->layoutItems(); +} + +void ColumnView::itemChange(QQuickItem::ItemChange change, const QQuickItem::ItemChangeData &value) +{ + switch (change) { + case QQuickItem::ItemChildAddedChange: + if (m_contentItem && value.item != m_contentItem && !value.item->inherits("QQuickRepeater")) { + addItem(value.item); + } + break; + default: + break; + } + QQuickItem::itemChange(change, value); +} + +void ColumnView::contentChildren_append(QQmlListProperty *prop, QQuickItem *item) +{ + // This can only be called from QML + ColumnView *view = static_cast(prop->object); + if (!view) { + return; + } + + view->m_contentItem->m_items.append(item); + + ColumnViewAttached *attached = qobject_cast(qmlAttachedPropertiesObject(item, true)); + attached->setOriginalParent(item->parentItem()); + attached->setShouldDeleteOnRemove(item->parentItem() == nullptr && QQmlEngine::objectOwnership(item) == QQmlEngine::JavaScriptOwnership); + + item->setParentItem(view->m_contentItem); +} + +int ColumnView::contentChildren_count(QQmlListProperty *prop) +{ + ColumnView *view = static_cast(prop->object); + if (!view) { + return 0; + } + + return view->m_contentItem->m_items.count(); +} + +QQuickItem *ColumnView::contentChildren_at(QQmlListProperty *prop, int index) +{ + ColumnView *view = static_cast(prop->object); + if (!view) { + return nullptr; + } + + if (index < 0 || index >= view->m_contentItem->m_items.count()) { + return nullptr; + } + return view->m_contentItem->m_items.value(index); +} + +void ColumnView::contentChildren_clear(QQmlListProperty *prop) +{ + ColumnView *view = static_cast(prop->object); + if (!view) { + return; + } + + return view->m_contentItem->m_items.clear(); +} + +QQmlListProperty ColumnView::contentChildren() +{ + return QQmlListProperty(this, nullptr, + contentChildren_append, + contentChildren_count, + contentChildren_at, + contentChildren_clear); +} + +void ColumnView::contentData_append(QQmlListProperty *prop, QObject *object) +{ + ColumnView *view = static_cast(prop->object); + if (!view) { + return; + } + + view->m_contentData.append(object); + QQuickItem *item = qobject_cast(object); + //exclude repeaters from layout + if (item && item->inherits("QQuickRepeater")) { + item->setParentItem(view); + + connect(item, SIGNAL(modelChanged()), view->m_contentItem, SLOT(updateRepeaterModel())); + + } else if (item) { + view->m_contentItem->m_items.append(item); + + ColumnViewAttached *attached = qobject_cast(qmlAttachedPropertiesObject(item, true)); + attached->setOriginalParent(item->parentItem()); + attached->setShouldDeleteOnRemove(view->m_complete && !item->parentItem() && QQmlEngine::objectOwnership(item) == QQmlEngine::JavaScriptOwnership); + + item->setParentItem(view->m_contentItem); + + } else { + object->setParent(view); + } +} + +int ColumnView::contentData_count(QQmlListProperty *prop) +{ + ColumnView *view = static_cast(prop->object); + if (!view) { + return 0; + } + + return view->m_contentData.count(); +} + +QObject *ColumnView::contentData_at(QQmlListProperty *prop, int index) +{ + ColumnView *view = static_cast(prop->object); + if (!view) { + return nullptr; + } + + if (index < 0 || index >= view->m_contentData.count()) { + return nullptr; + } + return view->m_contentData.value(index); +} + +void ColumnView::contentData_clear(QQmlListProperty *prop) +{ + ColumnView *view = static_cast(prop->object); + if (!view) { + return; + } + + return view->m_contentData.clear(); +} + +QQmlListProperty ColumnView::contentData() +{ + return QQmlListProperty(this, nullptr, + contentData_append, + contentData_count, + contentData_at, + contentData_clear); +} + +#include "moc_columnview.cpp" diff --git a/src/columnview_p.h b/src/columnview_p.h new file mode 100644 --- /dev/null +++ b/src/columnview_p.h @@ -0,0 +1,89 @@ +/* + * Copyright 2019 Marco Martin + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU Library General Public License as + * published by the Free Software Foundation; either version 2, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details + * + * You should have received a copy of the GNU Library General Public + * License along with this program; if not, write to the + * Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#pragma once + +#include "columnview.h" + +#include +#include + +class QPropertyAnimation; +class QQmlComponent; + +class QmlComponentsPool: public QObject { + Q_OBJECT + +public: + QmlComponentsPool(QObject *parent = nullptr); + ~QmlComponentsPool(); + + void initialize(QQmlEngine *engine); + + QQmlComponent *m_separatorComponent = nullptr; + QObject *m_units = nullptr; + +Q_SIGNALS: + void gridUnitChanged(); + void longDurationChanged(); + +private: + QObject *m_instance = nullptr; +}; + +class ContentItem : public QQuickItem { + Q_OBJECT + +public: + ContentItem(ColumnView *parent = nullptr); + ~ContentItem(); + + void layoutItems(); + qreal childWidth(QQuickItem *child); + void updateVisibleItems(); + void forgetItem(QQuickItem *item); + QQuickItem *ensureSeparator(QQuickItem *item); + + void setBoundedX(qreal x); + void animateX(qreal x); + void snapToItem(); + +protected: + void itemChange(QQuickItem::ItemChange change, const QQuickItem::ItemChangeData &value) override; + void geometryChanged(const QRectF &newGeometry, const QRectF &oldGeometry) override; + +private Q_SLOTS: + void syncItemsOrder(); + void updateRepeaterModel(); + +private: + ColumnView *m_view; + QPropertyAnimation *m_slideAnim; + QList m_items; + QList m_visibleItems; + QPointer m_viewAnchorItem; + QHash m_separators; + QHash m_models; + + qreal m_columnWidth = 0; + ColumnView::ColumnResizeMode m_columnResizeMode = ColumnView::FixedColumns; + bool m_shouldAnimate = false; + friend class ColumnView; +}; + diff --git a/src/controls/Page.qml b/src/controls/Page.qml --- a/src/controls/Page.qml +++ b/src/controls/Page.qml @@ -19,7 +19,7 @@ import QtQuick 2.5 import QtQuick.Layouts 1.2 -import org.kde.kirigami 2.4 as Kirigami +import org.kde.kirigami 2.7 as Kirigami import "private" import QtQuick.Templates 2.0 as T2 @@ -318,8 +318,8 @@ globalToolBar.stack = null; globalToolBar.row = null; - if (root.parent.hasOwnProperty("__pageRow")) { - globalToolBar.row = root.parent.__pageRow; + if (root.Kirigami.ColumnView.view) { + globalToolBar.row = root.Kirigami.ColumnView.view.__pageRow; } if (root.T2.StackView.view) { globalToolBar.stack = root.T2.StackView.view; @@ -365,17 +365,9 @@ //TODO: find container reliably, remove assumption {"pageRow": Qt.binding(function() {return row}), "page": root, - "current": Qt.binding(function() {return stack || !root.parent ? true : row.currentIndex === root.parent.level})}); + "current": Qt.binding(function() {return stack || row.currentIndex === root.Kirigami.ColumnView.level})}); } } - - Separator { - z: 999 - anchors.verticalCenter: globalToolBar.verticalCenter - height: globalToolBar.height * 0.6 - visible: !root.T2.StackView.view && globalToolBar.row && root.parent && globalToolBar.row.contentItem.contentX < root.parent.x - globalToolBar.row.globalToolBar.leftReservedSpace - Kirigami.Theme.textColor: globalToolBar.item ? globalToolBar.item.Kirigami.Theme.textColor : undefined - } }, //bottom action buttons diff --git a/src/controls/PageRow.qml b/src/controls/PageRow.qml --- a/src/controls/PageRow.qml +++ b/src/controls/PageRow.qml @@ -22,7 +22,7 @@ import QtQml.Models 2.2 import QtQuick.Templates 2.0 as T import QtQuick.Controls 2.0 as QQC2 -import org.kde.kirigami 2.4 +import org.kde.kirigami 2.7 import "private/globaltoolbar" as GlobalToolBar import "templates" as KT @@ -42,60 +42,60 @@ /** * This property holds the number of items currently pushed onto the view */ - readonly property int depth: popScrollAnim.running && popScrollAnim.pendingDepth > -1 ? popScrollAnim.pendingDepth : pagesLogic.count + property alias depth: columnView.count /** * The last Page in the Row */ - readonly property Item lastItem: pagesLogic.count ? pagesLogic.get(pagesLogic.count - 1).page : null + readonly property Item lastItem: columnView.contentChildren.length > 0 ? columnView.contentChildren[columnView.contentChildren.length - 1] : null /** * The currently visible Item */ - readonly property Item currentItem: mainView.currentItem ? mainView.currentItem.page : null + property alias currentItem: columnView.currentItem /** * the index of the currently visible Item */ - property alias currentIndex: mainView.currentIndex + property alias currentIndex: columnView.currentIndex /** * The initial item when this PageRow is created */ property variant initialPage /** - * The main flickable of this Row + * The main ColumnView of this Row */ - contentItem: mainView + contentItem: columnView /** * items: list * All the items that are present in the PageRow * @since 2.6 */ - readonly property var items: pagesLogic.pages; + property alias items: columnView.contentChildren; /** * visibleItems: list * All pages which are visible in the PageRow, excluding those which are scrolled away * @since 2.6 */ - property var visibleItems: [] + property alias visibleItems: columnView.visibleItems /** * firstVisibleItem: Item * The first at least partially visible page in the PageRow, pages before that one will be out of the viewport * @since 2.6 */ - readonly property Item firstVisibleItem: visibleItems.length > 0 ? visibleItems[0] : null + property alias firstVisibleItem: columnView.firstVisibleItem /** * lastVisibleItem: Item * The last at least partially visible page in the PageRow, pages after that one will be out of the viewport * @since 2.6 */ - readonly property Item lastVisibleItem: visibleItems.length > 0 ? visibleItems[visibleItems.length - 1] : null + property alias lastVisibleItem: columnView.lastVisibleItem /** * The default width for a column @@ -112,22 +112,22 @@ * Otherwise the only way to go back will be programmatically * default: true */ - property alias interactive: mainView.interactive + property alias interactive: columnView.interactive /** * wideMode: bool * If true, the PageRow is wide enough that willshow more than one column at once * @since 5.37 */ - readonly property bool wideMode: root.width >= root.defaultColumnWidth*2 && pagesLogic.count >= 2 + readonly property bool wideMode: root.width >= root.defaultColumnWidth*2 && depth >= 2 /** * separatorVisible: bool * True if the separator between pages should be visible * default: true * @since 5.38 */ - property bool separatorVisible: true + property alias separatorVisible: columnView.separatorVisible /** * globalToolBar: grouped property @@ -160,6 +160,7 @@ * If an item is used then the page will get re-parented. * If a string is used then it is interpreted as a url that is used to load a page * component. + * The last pushed page will become the current item. * * @param page The page can also be given as an array of pages. * In this case all those pages will @@ -172,66 +173,120 @@ * When an array is used the transition animation will only be to the last page. * * @param properties The properties argument is optional and allows defining a - * map of properties to set on the page. - * @return The new created page + * map of properties to set on the page. If page is actually an array of pages, properties should also be an array of key/value maps + * @return The new created page (or the last one if it was an array) */ function push(page, properties) { + var item = insertPage(depth, page, properties); + currentIndex = depth - 1; + return item; + } + + /** + * Inserts a new page or a list of new at an arbitrary position + * The page can be defined as a component, item or string. + * If an item is used then the page will get re-parented. + * If a string is used then it is interpreted as a url that is used to load a page + * component. + * The current Page will not be changed, currentIndex will be adjusted + * accordingly if needed to keep the same current page. + * + * @param page The page can also be given as an array of pages. + * In this case all those pages will + * be pushed onto the stack. The items in the stack can be components, items or + * strings just like for single pages. + * Additionally an object can be used, which specifies a page and an optional + * properties property. + * This can be used to push multiple pages while still giving each of + * them properties. + * When an array is used the transition animation will only be to the last page. + * + * @param properties The properties argument is optional and allows defining a + * map of properties to set on the page. If page is actually an array of pages, properties should also be an array of key/value maps + * @return The new created page (or the last one if it was an array) + * @since 2.7 + */ + function insertPage(position, page, properties) { //don't push again things already there - if (page.createObject === undefined && typeof page != "string" && pagesLogic.containsPage(page)) { + if (page.createObject === undefined && typeof page != "string" && columnView.containsItem(page)) { print("The item " + page + " is already in the PageRow"); - return; + return null; } - if (popScrollAnim.running) { - popScrollAnim.running = false; - popScrollAnim.popPageCleanup(popScrollAnim.pendingPage); - } + position = Math.max(0, Math.min(depth, position)); - popScrollAnim.popPageCleanup(currentItem); + columnView.pop(columnView.currentItem); // figure out if more than one page is being pushed var pages; + var propsArray = []; if (page instanceof Array) { pages = page; page = pages.pop(); + //compatibility with pre-qqc1 api, can probably be removed if (page.createObject === undefined && page.parent === undefined && typeof page != "string") { properties = properties || page.properties; page = page.page; } } + if (properties instanceof Array) { + propsArray = properties; + properties = propsArray.pop(); + } else { + propsArray = [properties]; + } // push any extra defined pages onto the stack if (pages) { var i; for (i = 0; i < pages.length; i++) { var tPage = pages[i]; - var tProps; + var tProps = propsArray[i]; + //compatibility with pre-qqc1 api, can probably be removed if (tPage.createObject === undefined && tPage.parent === undefined && typeof tPage != "string") { - if (pagesLogic.containsPage(tPage)) { + if (columnView.containsItem(tPage)) { print("The item " + page + " is already in the PageRow"); continue; } tProps = tPage.properties; tPage = tPage.page; } - var container = pagesLogic.initPage(tPage, tProps); - pagesLogic.append(container); - pagesLogic.pages.push(tPage); - root.itemsChanged(); + var pageItem = pagesLogic.initAndInsertPage(position, tPage, tProps); + ++position; } } // initialize the page - var container = pagesLogic.initPage(page, properties); - pagesLogic.append(container); - pagesLogic.pages.push(page); - container.visible = container.page.visible = true; - - mainView.currentIndex = container.level; - pagePushed(container.page); - root.itemsChanged(); - return container.page + var pageItem = pagesLogic.initAndInsertPage(position, page, properties); + + pagePushed(pageItem); + + return pageItem; + } + + /** + * Move the page at position fromPos to the new position toPos + * If needed, currentIndex will be adjusted + * in order to keep the same current page. + * @since 2.7 + */ + function movePage(fromPos, toPos) { + columnView.moveItem(fromPos, toPos); + } + + /** + * Remove the given page + * @param page The page can be given both as integer position or by reference + * @return The page that has just been removed + * @since 2.7 + */ + function removePage(page) { + if (depth == 0) { + return null; + } + + return columnView.removeItem(page); } /** @@ -243,32 +298,22 @@ */ function pop(page) { if (depth == 0) { - return; + return null; } - //if a pop was animating, stop it - if (popScrollAnim.running) { - popScrollAnim.running = false; - popScrollAnim.popPageCleanup(popScrollAnim.pendingPage); - //if a push was animating, stop it - } else { - mainView.positionViewAtIndex(mainView.currentIndex, ListView.Beginning); - } - - popScrollAnim.from = mainView.contentX - - if ((!page || !page.parent) && pagesLogic.count > 1) { - page = pagesLogic.get(pagesLogic.count - 2).page; - } - popScrollAnim.to = page && page.parent ? page.parent.x : 0; - popScrollAnim.pendingPage = page; - popScrollAnim.pendingDepth = page && page.parent ? page.parent.level + 1 : 0; - - popScrollAnim.running = true; + return columnView.pop(page); } /** - * Emitted when a page has been pushed + * Emitted when a page has been inserted anywhere + * @param position where the page has been inserted + * @param page the new page + * @since 2.7 + */ + signal pageInserted(int position, Item page) + + /** + * Emitted when a page has been pushed to the bottom * @param page the new page * @since 2.5 */ @@ -282,53 +327,13 @@ */ signal pageRemoved(Item page) - SequentialAnimation { - id: popScrollAnim - property real from - property real to - property var pendingPage - property int pendingDepth: -1 - function popPageCleanup(page) { - if (pagesLogic.count == 0) { - return; - } - if (popScrollAnim.running) { - popScrollAnim.running = false; - } - - var oldPage = pagesLogic.get(pagesLogic.count-1).page; - if (page !== undefined) { - // an unwind target has been specified - pop until we find it - while (page !== oldPage && pagesLogic.count > 1) { - pagesLogic.removePage(oldPage.parent.level); - - oldPage = pagesLogic.get(pagesLogic.count-1).page; - } - } else { - pagesLogic.removePage(pagesLogic.count-1); - } - } - NumberAnimation { - target: mainView - properties: "contentX" - duration: Units.shortDuration - from: popScrollAnim.from - to: popScrollAnim.to - } - ScriptAction { - script: { - //snap - mainView.flick(100, 0) - popScrollAnim.popPageCleanup(popScrollAnim.pendingPage); - } - } - } /** * Replaces a page on the stack. * @param page The page can also be given as an array of pages. * In this case all those pages will * be pushed onto the stack. The items in the stack can be components, items or * strings just like for single pages. + * the current page and all pagest after it in the stack will be removed. * Additionally an object can be used, which specifies a page and an optional * properties property. * This can be used to push multiple pages while still giving each of @@ -339,29 +344,30 @@ * @see push() for details. */ function replace(page, properties) { - if (currentIndex>=1) - popScrollAnim.popPageCleanup(pagesLogic.get(currentIndex-1).page); - else if (currentIndex==0) - popScrollAnim.popPageCleanup(); - else + if (currentIndex >= 1) { + pop(columnView.contentChildren[currentIndex-1]); + } else if (currentIndex == 0) { + pop(); + } else { console.warn("There's no page to replace"); + } return push(page, properties); } /** * Clears the page stack. * Destroy (or reparent) all the pages contained. */ function clear() { - return pagesLogic.clearPages(); + return columnView.clear(); } /** * @return the page at idx * @param idx the depth of the page we want */ function get(idx) { - return pagesLogic.get(idx).page; + return columnView.contentChildren[idx]; } /** @@ -371,20 +377,6 @@ if (depth > 1) { currentIndex = Math.max(0, currentIndex - 1); } - - if (LayoutMirroring.enabled) { - if (!mainView.atEnd) { - mainViewScrollAnim.from = mainView.contentX - mainViewScrollAnim.to = Math.min(mainView.contentWidth - mainView.width, mainView.contentX + defaultColumnWidth) - mainViewScrollAnim.running = true; - } - } else { - if (mainView.contentX - mainView.originX > 0) { - mainViewScrollAnim.from = mainView.contentX - mainViewScrollAnim.to = Math.max(mainView.originX, mainView.contentX - defaultColumnWidth) - mainViewScrollAnim.running = true; - } - } } /** @@ -406,24 +398,6 @@ Keys.forwardTo: [currentItem] - SequentialAnimation { - id: mainViewScrollAnim - property real from - property real to - NumberAnimation { - target: mainView - properties: "contentX" - duration: Units.longDuration - from: mainViewScrollAnim.from - to: mainViewScrollAnim.to - } - ScriptAction { - script: { - mainView.flick(100, 0); - } - } - } - GlobalToolBar.PageRowGlobalToolBarStyleGroup { id: globalToolBar readonly property int leftReservedSpace: globalToolBarUI.item ? globalToolBarUI.item.leftReservedSpace : 0 @@ -554,304 +528,86 @@ source: Qt.resolvedUrl("private/globaltoolbar/PageRowGlobalToolBarUI.qml"); } - ListView { - id: mainView - boundsBehavior: Flickable.StopAtBounds - orientation: Qt.Horizontal - snapMode: ListView.SnapToItem - currentIndex: 0 - property int marginForLast: count > 1 ? pagesLogic.get(count-1).page.width - pagesLogic.get(count-1).width : 0 - leftMargin: LayoutMirroring.enabled ? marginForLast : 0 - rightMargin: LayoutMirroring.enabled ? 0 : marginForLast - preferredHighlightBegin: 0 - preferredHighlightEnd: 0 - highlightMoveDuration: Units.longDuration - highlightFollowsCurrentItem: true - onWidthChanged: updatevisibleItems() - - onContentXChanged: updatevisibleItems() - - function updatevisibleItems() { - var visibleItems = []; - var cont; - var signalChange = false; - for (var i = 0; i < pagesLogic.count; ++i) { - cont = pagesLogic.get(i); - if (cont.x - contentX < width && cont.x + cont.width - contentX > 0) { - visibleItems.push(cont.page); - if (root.visibleItems.indexOf(cont.page) === -1) { - signalChange = true; + QtObject { + id: pagesLogic + readonly property var componentCache: new Array() + + function initAndInsertPage(position, page, properties) { + var pageComp; + + if (page.createObject) { + // page defined as component + pageComp = page; + } else if (typeof page == "string") { + // page defined as string (a url) + pageComp = pagesLogic.componentCache[page]; + if (!pageComp) { + pageComp = pagesLogic.componentCache[page] = Qt.createComponent(page); + } + } + if (pageComp) { + // instantiate page from component + // FIXME: parent directly to columnView or root? + page = pageComp.createObject(null, properties || {}); + columnView.insertItem(position, page); + + if (pageComp.status === Component.Error) { + throw new Error("Error while loading page: " + pageComp.errorString()); + } + } else { + // copy properties to the page + for (var prop in properties) { + if (properties.hasOwnProperty(prop)) { + page[prop] = properties[prop]; } } + columnView.insertItem(position, page); } - signalChange = signalChange || (visibleItems.length != root.visibleItems.length) - - if (signalChange) { - root.visibleItems = visibleItems; - root.visibleItemsChanged(); - } + return page; } - onMovementEnded: currentIndex = Math.max(0, indexAt(contentX, 0)) + } - onFlickEnded: onMovementEnded(); - onCurrentIndexChanged: { - if (currentItem) { - currentItem.page.forceActiveFocus(); - } - } + ColumnView { + id: columnView + anchors.fill: parent + readonly property Item __pageRow: root + columnResizeMode: root.wideMode ? ColumnView.FixedColumns : ColumnView.SingleColumn + columnWidth: root.defaultColumnWidth opacity: layersStack.depth < 2 + + onItemInserted: root.pageInserted(position, item); + onItemRemoved: root.pageRemoved(item); + Behavior on opacity { OpacityAnimator { duration: Units.longDuration easing.type: Easing.InOutQuad } } - - - model: ObjectModel { - id: pagesLogic - readonly property var componentCache: new Array() - readonly property int roundedDefaultColumnWidth: root.width < root.defaultColumnWidth*2 ? root.width : root.defaultColumnWidth - property var pages: [] - - function removePage(id) { - if (id < 0 || id >= count) { - print("Tried to remove an invalid page index:" + id); - return; - } - - var item = pagesLogic.get(id); - if (item.owner) { - item.page.visible = false; - item.page.parent = item.owner; - } - //FIXME: why reparent ing is necessary? - //is destroy just an async deleteLater() that isn't executed immediately or it actually leaks? - pagesLogic.remove(id); - item.parent = root; - root.pageRemoved(item.page); - if (item.page.parent===item) { - item.page.destroy(1) - } - item.destroy(); - pages.splice(id, 1); - root.itemsChanged(); - } - function clearPages () { - popScrollAnim.running = false; - popScrollAnim.pendingDepth = -1; - while (count > 0) { - removePage(count-1); - } - pages = []; - root.itemsChanged(); - } - function initPage(page, properties) { - var container = containerComponent.createObject(mainView, { - "level": pagesLogic.count, - "page": page - }); - - var pageComp; - if (page.createObject) { - // page defined as component - pageComp = page; - } else if (typeof page == "string") { - // page defined as string (a url) - pageComp = pagesLogic.componentCache[page]; - if (!pageComp) { - pageComp = pagesLogic.componentCache[page] = Qt.createComponent(page); - } - } - if (pageComp) { - // instantiate page from component - page = pageComp.createObject(container.pageParent, properties || {}); - - if (pageComp.status === Component.Error) { - throw new Error("Error while loading page: " + pageComp.errorString()); - } - } else { - // copy properties to the page - for (var prop in properties) { - if (properties.hasOwnProperty(prop)) { - page[prop] = properties[prop]; - } - } - } - - container.page = page; - if (page.parent === null || page.parent === container.pageParent) { - container.owner = null; - } - - // the page has to be reparented - if (page.parent !== container) { - page.parent = container; - } + } - return container; - } - function containsPage(page) { - for (var i = 0; i < pagesLogic.count; ++i) { - var candidate = pagesLogic.get(i); - if (candidate.page === page) { - print("The item " + page + " is already in the PageRow"); - return; - } - } - } + Rectangle { + anchors.bottom: columnView.bottom + height: Units.smallSpacing + x: (columnView.width - width) * (columnView.contentX / (columnView.contentWidth - columnView.width)) + width: columnView.width * (columnView.width/columnView.contentWidth) + color: Theme.textColor + opacity: 0 + onXChanged: { + opacity = 0.3 + scrollIndicatorTimer.restart(); } - T.ScrollIndicator.horizontal: T.ScrollIndicator { - anchors { - left: parent.left - right: parent.right - bottom: parent.bottom - } - height: Units.smallSpacing - contentItem: Rectangle { - height: Units.smallSpacing - width: Units.smallSpacing - color: Theme.textColor - opacity: 0 - onXChanged: { - opacity = 0.3 - scrollIndicatorTimer.restart(); - } - Behavior on opacity { - OpacityAnimator { - duration: Units.longDuration - easing.type: Easing.InOutQuad - } - } - Timer { - id: scrollIndicatorTimer - interval: Units.longDuration * 4 - onTriggered: parent.opacity = 0; - } + Behavior on opacity { + OpacityAnimator { + duration: Units.longDuration + easing.type: Easing.InOutQuad } } - - onContentWidthChanged: mainView.positionViewAtIndex(root.currentIndex, ListView.Contain) - } - - Component { - id: containerComponent - - MouseArea { - id: container - height: mainView.height - width: root.width - state: page - ? (page.visible ? (!root.wideMode ? "vertical" : (container.level >= pagesLogic.count - 1 ? "last" : "middle")) : "hidden") - : ""; - acceptedButtons: Qt.LeftButton | Qt.BackButton | Qt.ForwardButton - - property int level - - readonly property int hint: page && page.implicitWidth ? page.implicitWidth : root.defaultColumnWidth - readonly property int roundedHint: Math.floor(root.width/hint) > 0 ? root.width/Math.floor(root.width/hint) : root.width - property T.Control __pageRow: root - - property Item footer - - property Item page - onPageChanged: { - if (page) { - owner = page.parent; - page.parent = container; - page.anchors.left = container.left; - page.anchors.top = container.top; - page.anchors.right = container.right; - page.anchors.bottom = container.bottom; - page.anchors.topMargin = Qt.binding(function() { - if (!wideMode && (page.globalToolBarStyle == ApplicationHeaderStyle.ToolBar || page.globalToolBarStyle == ApplicationHeaderStyle.Titles)) { - return 0; - } - return globalToolBar.actualStyle == ApplicationHeaderStyle.TabBar || globalToolBar.actualStyle == ApplicationHeaderStyle.Breadcrumb ? globalToolBarUI.height : 0; - }); - } else { - pagesLogic.remove(level); - } - } - property Item owner - drag.filterChildren: true - onPressed: { - switch (mouse.button) { - case Qt.BackButton: - root.flickBack(); - break; - case Qt.ForwardButton: - root.currentIndex = Math.min(root.depth, root.currentIndex + 1); - break; - default: - root.currentIndex = level; - break; - } - mouse.accepted = false; - } - onFocusChanged: { - if (focus) { - root.currentIndex = level; - } - } - - //TODO: move in Page itself? - Separator { - z: 999 - anchors { - top: page ? page.top : parent.top - bottom: parent.bottom - left: parent.left - //ensure a sharp angle - topMargin: -width + (globalToolBar.actualStyle == ApplicationHeaderStyle.ToolBar || globalToolBar.actualStyle == ApplicationHeaderStyle.Titles ? globalToolBarUI.height : 0) - } - visible: root.separatorVisible && mainView.contentX < container.x - } - states: [ - State { - name: "vertical" - PropertyChanges { - target: container - width: root.width - } - PropertyChanges { - target: container.page ? container.page.anchors : null - rightMargin: 0 - } - }, - State { - name: "last" - PropertyChanges { - target: container - width: pagesLogic.roundedDefaultColumnWidth - } - PropertyChanges { - target: container.page.anchors - rightMargin: { - return -(root.width - pagesLogic.roundedDefaultColumnWidth*2); - } - } - }, - State { - name: "middle" - PropertyChanges { - target: container - width: pagesLogic.roundedDefaultColumnWidth - } - PropertyChanges { - target: container.page.anchors - rightMargin: 0 - } - }, - State { - name: "hidden" - PropertyChanges { - target: container - width: 0 - } - } - ] + Timer { + id: scrollIndicatorTimer + interval: Units.longDuration * 4 + onTriggered: parent.opacity = 0; } } } diff --git a/src/controls/private/ActionButton.qml b/src/controls/private/ActionButton.qml --- a/src/controls/private/ActionButton.qml +++ b/src/controls/private/ActionButton.qml @@ -21,7 +21,7 @@ import QtQuick.Layouts 1.2 import QtQuick.Controls 2.0 as Controls import QtGraphicalEffects 1.0 -import org.kde.kirigami 2.4 +import org.kde.kirigami 2.7 import "../templates/private" @@ -128,10 +128,10 @@ onPressed: { //search if we have a page to set to current - if (root.hasApplicationWindow && applicationWindow().pageStack.currentIndex !== undefined && root.page.parent.level !== undefined) { + if (root.hasApplicationWindow && applicationWindow().pageStack.currentIndex !== undefined && root.page.ColumnView.level !== undefined) { //search the button parent's parent, that is the page parent //this will make the context drawer open for the proper page - applicationWindow().pageStack.currentIndex = root.page.parent.level; + applicationWindow().pageStack.currentIndex = root.page.ColumnView.level; } downTimestamp = (new Date()).getTime(); startX = button.x + button.width/2; @@ -402,7 +402,7 @@ } visible: root.page.actions && root.page.actions.contextualActions.length > 0 && (applicationWindow === undefined || applicationWindow().wideScreen) //using internal pagerow api - && (root.page && root.page.parent ? root.page.parent.level < applicationWindow().pageStack.depth-1 : false) + && (root.page && root.page.parent ? root.page.ColumnView.level < applicationWindow().pageStack.depth-1 : false) width: Units.iconSizes.smallMedium + Units.smallSpacing*2 height: width diff --git a/src/kirigamiplugin.cpp b/src/kirigamiplugin.cpp --- a/src/kirigamiplugin.cpp +++ b/src/kirigamiplugin.cpp @@ -20,6 +20,7 @@ */ #include "kirigamiplugin.h" +#include "columnview.h" #include "enums.h" #include "desktopicon.h" #include "settings.h" @@ -178,6 +179,7 @@ qmlRegisterType(componentUrl(QStringLiteral("UrlButton.qml")), uri, 2, 6, "UrlButton"); //2.7 + qmlRegisterType(uri, 2, 7, "ColumnView"); qmlRegisterType(componentUrl(QStringLiteral("ActionTextField.qml")), uri, 2, 7, "ActionTextField"); qmlProtectModule(uri, 2);