diff --git a/src/columnsview.cpp b/src/columnsview.cpp index ae149ec1..b3e239b8 100644 --- a/src/columnsview.cpp +++ b/src/columnsview.cpp @@ -1,1056 +1,1056 @@ /* * 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 "columnsview.h" #include "columnsview_p.h" #include #include #include #include #include #include #include QHash ColumnsView::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.ColumnsView.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() {} ///////// ColumnsViewAttached::ColumnsViewAttached(QObject *parent) : QObject(parent) {} ColumnsViewAttached::~ColumnsViewAttached() {} void ColumnsViewAttached::setLevel(int level) { if (!m_customFillWidth && m_view) { m_fillWidth = level == m_view->depth() - 1; } if (level == m_level) { return; } m_level = level; emit levelChanged(); } int ColumnsViewAttached::level() const { return m_level; } void ColumnsViewAttached::setFillWidth(bool fill) { if (m_view) { disconnect(m_view.data(), &ColumnsView::depthChanged, this, nullptr); } m_customFillWidth = true; if (fill == m_fillWidth) { return; } m_fillWidth = fill; emit fillWidthChanged(); } bool ColumnsViewAttached::fillWidth() const { return m_fillWidth; } qreal ColumnsViewAttached::reservedSpace() const { return m_reservedSpace; } void ColumnsViewAttached::setReservedSpace(qreal space) { if (m_view) { disconnect(m_view.data(), &ColumnsView::columnWidthChanged, this, nullptr); } m_customReservedSpace = true; if (qFuzzyCompare(space, m_reservedSpace)) { return; } m_reservedSpace = space; emit reservedSpaceChanged(); } ColumnsView *ColumnsViewAttached::view() { return m_view; } void ColumnsViewAttached::setView(ColumnsView *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_level == m_view->depth() - 1; connect(m_view.data(), &ColumnsView::depthChanged, this, [this]() { m_fillWidth = m_level == m_view->depth() - 1; emit fillWidthChanged(); }); } if (!m_customReservedSpace && m_view) { m_reservedSpace = m_view->columnWidth(); connect(m_view.data(), &ColumnsView::columnWidthChanged, this, [this]() { m_reservedSpace = m_view->columnWidth(); emit reservedSpaceChanged(); }); } emit viewChanged(); } ContentItem::ContentItem(ColumnsView *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(parentItem(), QRectF(m_view->currentItem()->position(), 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->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; } ColumnsViewAttached *attached = qobject_cast(qmlAttachedPropertiesObject(child, true)); if (m_columnResizeMode == ColumnsView::SingleColumn) { return qRound(parentItem()->width()); } else if (attached->fillWidth()) { return qRound(qBound(m_columnWidth, (parentItem()->width() - attached->reservedSpace()), parentItem()->width())); } else if (m_columnResizeMode == ColumnsView::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(); } ColumnsViewAttached *attached = qobject_cast(qmlAttachedPropertiesObject(child, true)); attached->setLevel(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; } } if (newItems != m_visibleItems) { m_visibleItems = newItems; emit m_view->visibleItemsChanged(); } } void ContentItem::forgetItem(QQuickItem *item) { if (!m_items.contains(item)) { return; } ColumnsViewAttached *attached = qobject_cast(qmlAttachedPropertiesObject(item, true)); attached->setView(nullptr); attached->setLevel(-1); disconnect(attached, nullptr, this, nullptr); disconnect(item, nullptr, this, nullptr); QQuickItem *separatorItem = m_separators.take(item); if (separatorItem) { separatorItem->deleteLater(); } const int index = m_items.indexOf(item); m_items.removeAll(item); m_shouldAnimate = true; m_view->polish(); if (index <= m_view->currentIndex()) { m_view->setCurrentIndex(qBound(0, index - 1, m_items.count() - 1)); } emit m_view->depthChanged(); } 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: { ColumnsViewAttached *attached = qobject_cast(qmlAttachedPropertiesObject(value.item, true)); attached->setView(m_view); connect(attached, &ColumnsViewAttached::fillWidthChanged, this, &ContentItem::layoutItems); connect(attached, &ColumnsViewAttached::reservedSpaceChanged, this, &ContentItem::layoutItems); if (!m_items.contains(value.item)) { connect(value.item, &QQuickItem::widthChanged, this, &ContentItem::layoutItems); m_items << value.item; } - if (m_view->separatorsVisible()) { + if (m_view->separatorVisible()) { ensureSeparator(value.item); } m_shouldAnimate = true; m_view->polish(); emit m_view->depthChanged(); 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); } ColumnsView::ColumnsView(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, &ColumnsView::contentWidthChanged); connect(m_contentItem, &ContentItem::xChanged, this, &ColumnsView::contentXChanged); } ColumnsView::~ColumnsView() { } ColumnsView::ColumnResizeMode ColumnsView::columnResizeMode() const { return m_contentItem->m_columnResizeMode; } void ColumnsView::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 ColumnsView::columnWidth() const { return m_contentItem->m_columnWidth; } void ColumnsView::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 ColumnsView::currentIndex() const { return m_currentIndex; } void ColumnsView::setCurrentIndex(int index) { if (!parentItem() || 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 mapped = m_currentItem->mapRectToItem(parentItem(), QRectF(m_currentItem->position(), m_currentItem->size())); if (!QRectF(QPointF(0, 0), parentItem()->size()).intersects(mapped)) { m_contentItem->m_viewAnchorItem = m_currentItem; m_contentItem->animateX(-m_currentItem->x()); } } emit currentIndexChanged(); emit currentItemChanged(); } QQuickItem *ColumnsView::currentItem() { return m_currentItem; } QListColumnsView::visibleItems() const { return m_contentItem->m_visibleItems; } int ColumnsView::depth() const { return m_contentItem->m_items.count(); } QQuickItem *ColumnsView::contentItem() const { return m_contentItem; } int ColumnsView::scrollDuration() const { return m_contentItem->m_slideAnim->duration(); } void ColumnsView::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 ColumnsView::separatorsVisible() const +bool ColumnsView::separatorVisible() const { - return m_separatorsVisible; + return m_separatorVisible; } -void ColumnsView::setSeparatorsVisible(bool visible) +void ColumnsView::setSeparatorVisible(bool visible) { - if (visible == m_separatorsVisible) { + if (visible == m_separatorVisible) { return; } - m_separatorsVisible = visible; + 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 separatorsVisibleChanged(); + emit separatorVisibleChanged(); } bool ColumnsView::dragging() const { return m_dragging; } bool ColumnsView::moving() const { return m_moving; } qreal ColumnsView::contentWidth() const { return m_contentItem->width(); } qreal ColumnsView::contentX() const { return -m_contentItem->x(); } void ColumnsView::setContentX(qreal x) const { m_contentItem->setX(qRound(-x)); } bool ColumnsView::interactive() const { return m_interactive; } void ColumnsView::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 ColumnsView::addItem(QQuickItem *item) { insertItem(m_contentItem->m_items.length(), item); } void ColumnsView::insertItem(int pos, QQuickItem *item) { if (m_contentItem->m_items.contains(item)) { return; } m_contentItem->m_items.insert(qBound(0, pos, m_contentItem->m_items.length()), item); m_contentItem->m_viewAnchorItem = item; setCurrentIndex(pos); item->setParentItem(m_contentItem); item->forceActiveFocus(); emit contentChildrenChanged(); } void ColumnsView::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; polish(); } void ColumnsView::removeItem(const QVariant &item) { if (item.canConvert()) { removeItem(item.value()); } else if (item.canConvert()) { removeItem(item.toInt()); } } void ColumnsView::removeItem(QQuickItem *item) { if (m_contentItem->m_items.isEmpty() || !m_contentItem->m_items.contains(item)) { return; } m_contentItem->forgetItem(item); if (QQmlEngine::objectOwnership(item) == QQmlEngine::JavaScriptOwnership) { item->deleteLater(); } else { item->setParentItem(nullptr); } } void ColumnsView::removeItem(int pos) { if (m_contentItem->m_items.isEmpty() || pos < 0 || pos >= m_contentItem->m_items.length()) { return; } removeItem(m_contentItem->m_items[pos]); } void ColumnsView::pop(QQuickItem *item) { while (!m_contentItem->m_items.isEmpty() && m_contentItem->m_items.last() != item) { removeItem(m_contentItem->m_items.last()); // if no item has been passed, just pop one if (!item) { break; } } } void ColumnsView::clear() { for (QQuickItem *item : m_contentItem->m_items) { if (QQmlEngine::objectOwnership(item) == QQmlEngine::JavaScriptOwnership) { item->deleteLater(); } else { item->setParentItem(nullptr); } } m_contentItem->m_items.clear(); emit contentChildrenChanged(); } bool ColumnsView::contains(QQuickItem *item) { return m_contentItem->m_items.contains(item); } ColumnsViewAttached *ColumnsView::qmlAttachedProperties(QObject *object) { return new ColumnsViewAttached(object); } void ColumnsView::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 ColumnsView::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; } 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() * 2; 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 ColumnsView::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 ColumnsView::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 ColumnsView::mouseReleaseEvent(QMouseEvent *event) { if (!m_interactive) { return; } if (m_dragging) { m_dragging = false; emit draggingChanged(); } m_contentItem->snapToItem(); setKeepMouseGrab(false); event->accept(); } void ColumnsView::mouseUngrabEvent() { if (m_dragging) { m_dragging = false; emit draggingChanged(); } m_contentItem->snapToItem(); setKeepMouseGrab(false); } void ColumnsView::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(); } void ColumnsView::updatePolish() { m_contentItem->layoutItems(); } void ColumnsView::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 ColumnsView::contentChildren_append(QQmlListProperty *prop, QQuickItem *item) { ColumnsView *view = static_cast(prop->object); if (!view) { return; } view->m_contentItem->m_items.append(item); item->setParentItem(view->m_contentItem); } int ColumnsView::contentChildren_count(QQmlListProperty *prop) { ColumnsView *view = static_cast(prop->object); if (!view) { return 0; } return view->m_contentItem->m_items.count(); } QQuickItem *ColumnsView::contentChildren_at(QQmlListProperty *prop, int index) { ColumnsView *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 ColumnsView::contentChildren_clear(QQmlListProperty *prop) { ColumnsView *view = static_cast(prop->object); if (!view) { return; } return view->m_contentItem->m_items.clear(); } QQmlListProperty ColumnsView::contentChildren() { return QQmlListProperty(this, nullptr, contentChildren_append, contentChildren_count, contentChildren_at, contentChildren_clear); } void ColumnsView::contentData_append(QQmlListProperty *prop, QObject *object) { ColumnsView *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); } else if (item) { view->addItem(item); } else { object->setParent(view); } } int ColumnsView::contentData_count(QQmlListProperty *prop) { ColumnsView *view = static_cast(prop->object); if (!view) { return 0; } return view->m_contentData.count(); } QObject *ColumnsView::contentData_at(QQmlListProperty *prop, int index) { ColumnsView *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 ColumnsView::contentData_clear(QQmlListProperty *prop) { ColumnsView *view = static_cast(prop->object); if (!view) { return; } return view->m_contentData.clear(); } QQmlListProperty ColumnsView::contentData() { return QQmlListProperty(this, nullptr, contentData_append, contentData_count, contentData_at, contentData_clear); } #include "moc_columnsview.cpp" diff --git a/src/columnsview.h b/src/columnsview.h index 998b3ffa..943deaab 100644 --- a/src/columnsview.h +++ b/src/columnsview.h @@ -1,219 +1,219 @@ /* * 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 ColumnsView; class ColumnsViewAttached : public QObject { Q_OBJECT Q_PROPERTY(int level READ level WRITE setLevel NOTIFY levelChanged) Q_PROPERTY(bool fillWidth READ fillWidth WRITE setFillWidth NOTIFY fillWidthChanged) Q_PROPERTY(qreal reservedSpace READ reservedSpace WRITE setReservedSpace NOTIFY reservedSpaceChanged) Q_PROPERTY(ColumnsView *view READ view NOTIFY viewChanged) public: ColumnsViewAttached(QObject *parent = nullptr); ~ColumnsViewAttached(); void setLevel(int level); int level() const; void setFillWidth(bool fill); bool fillWidth() const; qreal reservedSpace() const; void setReservedSpace(qreal space); ColumnsView *view(); void setView(ColumnsView *view); Q_SIGNALS: void levelChanged(); void fillWidthChanged(); void reservedSpaceChanged(); void viewChanged(); private: int m_level = -1; bool m_fillWidth = false; qreal m_reservedSpace = 0; QPointer m_view; bool m_customFillWidth = false; bool m_customReservedSpace = false; }; class ColumnsView : public QQuickItem { Q_OBJECT Q_PROPERTY(ColumnResizeMode columnResizeMode READ columnResizeMode WRITE setColumnResizeMode NOTIFY columnResizeModeChanged) Q_PROPERTY(qreal columnWidth READ columnWidth WRITE setColumnWidth NOTIFY columnWidthChanged) Q_PROPERTY(int depth READ depth NOTIFY depthChanged) Q_PROPERTY(int currentIndex READ currentIndex WRITE setCurrentIndex NOTIFY currentIndexChanged) Q_PROPERTY(QQuickItem *currentItem READ currentItem NOTIFY currentItemChanged) Q_PROPERTY(QQuickItem *contentItem READ contentItem CONSTANT) Q_PROPERTY(qreal contentX READ contentX WRITE setContentX NOTIFY contentXChanged) Q_PROPERTY(qreal contentWidth READ contentWidth NOTIFY contentWidthChanged) Q_PROPERTY(int scrollDuration READ scrollDuration WRITE setScrollDuration NOTIFY scrollDurationChanged) - Q_PROPERTY(bool separatorsVisible READ separatorsVisible WRITE setSeparatorsVisible NOTIFY separatorsVisibleChanged) + Q_PROPERTY(bool separatorVisible READ separatorVisible WRITE setSeparatorVisible NOTIFY separatorVisibleChanged) Q_PROPERTY(QList visibleItems READ visibleItems NOTIFY visibleItemsChanged) // Properties to make it similar to Flickable Q_PROPERTY(bool dragging READ dragging NOTIFY draggingChanged) Q_PROPERTY(bool moving READ moving NOTIFY movingChanged) Q_PROPERTY(bool interactive READ interactive WRITE setInteractive NOTIFY interactiveChanged) // Default properties Q_PROPERTY(QQmlListProperty contentChildren READ contentChildren NOTIFY contentChildrenChanged FINAL) Q_PROPERTY(QQmlListProperty contentData READ contentData FINAL) Q_CLASSINFO("DefaultProperty", "contentData") Q_ENUMS(ColumnResizeMode) public: enum ColumnResizeMode { FixedColumns = 0, DynamicColumns, SingleColumn }; ColumnsView(QQuickItem *parent = nullptr); ~ColumnsView(); // 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 separatorsVisible() const; - void setSeparatorsVisible(bool visible); + bool separatorVisible() const; + void setSeparatorVisible(bool visible); int depth() const; QQuickItem *currentItem(); QListvisibleItems() 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 void removeItem(QQuickItem *item); void removeItem(int item); // QML attached property static ColumnsViewAttached *qmlAttachedProperties(QObject *object); public Q_SLOTS: void addItem(QQuickItem *item); void insertItem(int pos, QQuickItem *item); void moveItem(int from, int to); void removeItem(const QVariant &item); void pop(QQuickItem *item); void clear(); bool contains(QQuickItem *item); protected: void classBegin() 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: void contentChildrenChanged(); void columnResizeModeChanged(); void columnWidthChanged(); void currentIndexChanged(); void currentItemChanged(); void visibleItemsChanged(); void depthChanged(); void draggingChanged(); void movingChanged(); void contentXChanged(); void contentWidthChanged(); void interactiveChanged(); void scrollDurationChanged(); - void separatorsVisibleChanged(); + void separatorVisibleChanged(); 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_separatorsVisible = true; + bool m_separatorVisible = true; }; QML_DECLARE_TYPEINFO(ColumnsView, QML_HAS_ATTACHED_PROPERTIES) diff --git a/src/controls/PageRow.qml b/src/controls/PageRow.qml index 0b3c640f..25b95640 100644 --- a/src/controls/PageRow.qml +++ b/src/controls/PageRow.qml @@ -1,545 +1,545 @@ /* * Copyright 2016 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 Library 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. */ import QtQuick 2.5 import QtQuick.Layouts 1.2 import QtQml.Models 2.2 import QtQuick.Templates 2.0 as T import QtQuick.Controls 2.0 as QQC2 import org.kde.kirigami 2.7 import "private/globaltoolbar" as GlobalToolBar import "templates" as KT /** * PageRow implements a row-based navigation model, which can be used * with a set of interlinked information pages. Items are pushed in the * back of the row and the view scrolls until that row is visualized. * A PageRowcan show a single page or a multiple set of columns, depending * on the window width: on a phone a single column should be fullscreen, * while on a tablet or a desktop more than one column should be visible. * @inherit QtQuick.Templates.Control */ T.Control { id: root //BEGIN PROPERTIES /** * This property holds the number of items currently pushed onto the view */ property alias depth: columnsView.depth /** * The last Page in the Row */ readonly property Item lastItem: columnsView.contentChildren.length > 0 ? columnsView.contentChildren[columnsView.contentChildren.length - 1] : null /** * The currently visible Item */ property alias currentItem: columnsView.currentItem /** * the index of the currently visible Item */ property alias currentIndex: columnsView.currentIndex /** * The initial item when this PageRow is created */ property variant initialPage /** * The main flickable of this Row */ contentItem: columnsView /** * items: list * All the items that are present in the PageRow * @since 2.6 */ readonly property var items: pagesLogic.pages; /** * visibleItems: list * All pages which are visible in the PageRow, excluding those which are scrolled away * @since 2.6 */ property var 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 /** * 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 /** * The default width for a column * default is wide enough for 30 grid units. * Pages can override it with their Layout.fillWidth, * implicitWidth Layout.minimumWidth etc. */ property int defaultColumnWidth: Units.gridUnit * 20 /** * interactive: bool * If true it will be possible to go back/forward by dragging the * content themselves with a gesture. * Otherwise the only way to go back will be programmatically * default: true */ property alias interactive: columnsView.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 && 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: columnsView.separatorVisible /** * globalToolBar: grouped property * Controls the appearance of an optional global toolbar for the whole PageRow. * It's a grouped property comprised of the following properties: * * style: (Kirigami.ApplicationHeaderStyle) can have the following values: * ** Auto: depending on application formfactor, it can behave automatically like other values, such as a Breadcrumb on mobile and ToolBar on desktop * ** Breadcrumb: it will show a breadcrumb of all the page titles in the stack, for easy navigation * ** Titles: each page will only have its own tile on top * ** TabBar: the global toolbar will look like a TabBar to select the pages * ** ToolBar: each page will have the title on top together buttons and menus to represent all of the page actions: not available on Mobile systems. * ** None: no global toolbar will be shown * * * actualStyle: this will represent the actual style of the toolbar: it can be different from style in the case style is Auto * * showNavigationButtons: if true, forward and backward navigation buttons will be shown on the left of the toolbar * * minimumHeight: (int) minimum height of the header, which will be resized when scrolling, only in Mobile mode (default: preferredHeight, sliding but no scaling) property int preferredHeight: (int) the height the toolbar will usually have property int maximumHeight: (int) The height the toolbar will have in mobile mode when the app is in reachable mode (default: preferredHeight * 1.5) * * leftReservedSpace: (int, readonly) how many pixels are reserved at the left of the page toolBar (for navigation buttons or drawer handle) property int rightReservedSpace: (int, readonly) how many pixels are reserved at the right of the page toolbar (drawer handle) * @since 5.48 */ readonly property alias globalToolBar: globalToolBar //END PROPERTIES //BEGIN FUNCTIONS /** * Pushes a page on the stack. * 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. * * @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. * @return The new created page */ function push(page, properties) { //don't push again things already there if (page.createObject === undefined && typeof page != "string" && columnsView.contains(page)) { print("The item " + page + " is already in the PageRow"); return; } columnsView.pop(columnsView.currentItem); // figure out if more than one page is being pushed var pages; if (page instanceof Array) { pages = page; page = pages.pop(); if (page.createObject === undefined && page.parent === undefined && typeof page != "string") { properties = properties || page.properties; page = page.page; } } // 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; if (tPage.createObject === undefined && tPage.parent === undefined && typeof tPage != "string") { if (pagesLogic.containsPage(tPage)) { print("The item " + page + " is already in the PageRow"); continue; } tProps = tPage.properties; tPage = tPage.page; } var pageItem = pagesLogic.initPage(tPage, tProps); root.itemsChanged(); } } // initialize the page var pageItem = pagesLogic.initPage(page, properties); pagePushed(pageItem); return pageItem; } /** * Pops a page off the stack. * @param page If page is specified then the stack is unwound to that page, * to unwind to the first page specify * page as null. * @return The page instance that was popped off the stack. */ function pop(page) { if (depth == 0) { return; } columnsView.pop(page) pageRemoved(page) } /** * Emitted when a page has been pushed * @param page the new page * @since 2.5 */ signal pagePushed(Item page) /** * Emitted when a page has been removed from the row. * @param page the page that has been removed: at this point it's still valid, * but may be auto deleted soon. * @since 2.5 */ signal pageRemoved(Item page) /** * 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. * 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. * @see push() for details. */ function replace(page, properties) { if (currentIndex >= 1) { pop(columnsView.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 columnsView.clear(); } /** * @return the page at idx * @param idx the depth of the page we want */ function get(idx) { return columnsView.contentChildren[idx]; } /** * go back to the previous index and scroll to the left to show one more column */ function flickBack() { if (depth > 1) { currentIndex = Math.max(0, currentIndex - 1); } } /** * layers: QtQuick.Controls.PageStack * Access to the modal layers. * Sometimes an application needs a modal page that always covers all the rows. * For instance the full screen image of an image viewer or a settings page. * @since 5.38 */ property alias layers: layersStack //END FUNCTIONS onInitialPageChanged: { if (initialPage) { clear(); push(initialPage, null) } } Keys.forwardTo: [currentItem] GlobalToolBar.PageRowGlobalToolBarStyleGroup { id: globalToolBar readonly property int leftReservedSpace: globalToolBarUI.item ? globalToolBarUI.item.leftReservedSpace : 0 readonly property int rightReservedSpace: globalToolBarUI.item ? globalToolBarUI.item.rightReservedSpace : 0 readonly property int height: globalToolBarUI.height readonly property Item leftHandleAnchor: globalToolBarUI.item ? globalToolBarUI.item.leftHandleAnchor : null readonly property Item rightHandleAnchor: globalToolBarUI.item ? globalToolBarUI.item.rightHandleAnchor : null } QQC2.StackView { id: layersStack z: 99 visible: depth > 1 || busy anchors { fill: parent } //placeholder as initial item initialItem: Item {} function clear () { //don't let it kill the main page row var d = root.depth; for (var i = 1; i < d; ++i) { pop(); } } popEnter: Transition { OpacityAnimator { from: 0 to: 1 duration: Units.longDuration easing.type: Easing.InOutCubic } } popExit: Transition { ParallelAnimation { OpacityAnimator { from: 1 to: 0 duration: Units.longDuration easing.type: Easing.InOutCubic } YAnimator { from: 0 to: height/2 duration: Units.longDuration easing.type: Easing.InCubic } } } pushEnter: Transition { ParallelAnimation { //NOTE: It's a PropertyAnimation instead of an Animator because with an animator the item will be visible for an instant before starting to fade PropertyAnimation { property: "opacity" from: 0 to: 1 duration: Units.longDuration easing.type: Easing.InOutCubic } YAnimator { from: height/2 to: 0 duration: Units.longDuration easing.type: Easing.OutCubic } } } pushExit: Transition { OpacityAnimator { from: 1 to: 0 duration: Units.longDuration easing.type: Easing.InOutCubic } } replaceEnter: Transition { ParallelAnimation { OpacityAnimator { from: 0 to: 1 duration: Units.longDuration easing.type: Easing.InOutCubic } YAnimator { from: height/2 to: 0 duration: Units.longDuration easing.type: Easing.OutCubic } } } replaceExit: Transition { ParallelAnimation { OpacityAnimator { from: 1 to: 0 duration: Units.longDuration easing.type: Easing.InCubic } YAnimator { from: 0 to: -height/2 duration: Units.longDuration easing.type: Easing.InOutCubic } } } } Loader { id: globalToolBarUI anchors { left: parent.left top: parent.top right: parent.right } z: 100 active: globalToolBar.actualStyle != ApplicationHeaderStyle.None visible: active height: active ? implicitHeight : 0 source: Qt.resolvedUrl("private/globaltoolbar/PageRowGlobalToolBarUI.qml"); } QtObject { id: pagesLogic readonly property var componentCache: new Array() readonly property int roundedDefaultColumnWidth: root.width < root.defaultColumnWidth*2 ? root.width : root.defaultColumnWidth function initPage(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); } root.itemsChanged(); } if (pageComp) { // instantiate page from component - // FIXME: parent directly to columnsview or root? + // FIXME: parent directly to columnsView or root? page = pageComp.createObject(null, properties || {}); columnsView.addItem(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]; } } columnsView.addItem(page); } columnsView.currentIndex = page.ColumnsView.level; return page; } 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; } } } } ColumnsView { id: columnsView anchors.fill: parent readonly property Item __pageRow: root columnResizeMode: depth < 2 || width < columnWidth * 2 ? ColumnsView.SingleColumn : ColumnsView.FixedColumns columnWidth: root.defaultColumnWidth opacity: layersStack.depth < 2 Behavior on opacity { OpacityAnimator { duration: Units.longDuration easing.type: Easing.InOutQuad } } } Rectangle { anchors.bottom: columnsView.bottom height: Units.smallSpacing x: (columnsView.width - width) * (columnsView.contentX / (columnsView.contentWidth - columnsView.width)) width: columnsView.width * (columnsView.width/columnsView.contentWidth) 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; } } }