/* * Copyright (C) 2013 Canonical, Ltd. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * 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 General Public License * along with this program. If not, see . */ /* * The implementation is centered around m_columnVisibleItems * that holds a vector of lists. There's a list for each of the * columns the view has. In the list the items of the column are * ordered as they appear topdown in the view. m_indexColumnMap is * used when re-building the list up since given a position * in the middle of the list and the need to create the previous does * not give us enough information to know in which column we have * to position the item so that when we reach the item the view is * correctly layouted at 0 for all the columns */ #include "horizontaljournal.h" #include #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-pedantic" #if (QT_VERSION < QT_VERSION_CHECK(5, 1, 0)) #include #else #include #include #endif #include #pragma GCC diagnostic pop static const qreal bufferRatio = 0.5; HorizontalJournal::HorizontalJournal() : m_delegateModel(nullptr) , m_asyncRequestedIndex(-1) , m_firstVisibleIndex(-1) , m_rowHeight(0) , m_horizontalSpacing(0) , m_verticalSpacing(0) , m_delegateCreationBegin(0) , m_delegateCreationEnd(0) , m_delegateCreationBeginValid(false) , m_delegateCreationEndValid(false) , m_needsRelayout(false) , m_delegateValidated(false) , m_implicitHeightDirty(false) { connect(this, SIGNAL(widthChanged()), this, SLOT(relayout())); connect(this, SIGNAL(heightChanged()), this, SLOT(onHeightChanged())); } QAbstractItemModel *HorizontalJournal::model() const { return m_delegateModel ? m_delegateModel->model().value() : nullptr; } void HorizontalJournal::setModel(QAbstractItemModel *model) { if (model != this->model()) { if (!m_delegateModel) { createDelegateModel(); } #if (QT_VERSION < QT_VERSION_CHECK(5, 1, 0)) m_delegateModel->setModel(QVariant::fromValue(model)); #else m_delegateModel->setModel(QVariant::fromValue(model)); #endif // Cleanup the existing items Q_FOREACH(QQuickItem *item, m_visibleItems) releaseItem(item); m_visibleItems.clear(); m_lastInRowIndexPosition.clear(); m_firstVisibleIndex = 0; Q_EMIT modelChanged(); polish(); } } QQmlComponent *HorizontalJournal::delegate() const { return m_delegateModel ? m_delegateModel->delegate() : nullptr; } void HorizontalJournal::setDelegate(QQmlComponent *delegate) { if (delegate != this->delegate()) { if (!m_delegateModel) { createDelegateModel(); } // Cleanup the existing items Q_FOREACH(QQuickItem *item, m_visibleItems) releaseItem(item); m_visibleItems.clear(); m_lastInRowIndexPosition.clear(); m_firstVisibleIndex = 0; m_delegateModel->setDelegate(delegate); Q_EMIT delegateChanged(); m_delegateValidated = false; polish(); } } qreal HorizontalJournal::rowHeight() const { return m_rowHeight; } void HorizontalJournal::setRowHeight(qreal rowHeight) { if (rowHeight != m_rowHeight) { m_rowHeight = rowHeight; Q_EMIT rowHeightChanged(); if (isComponentComplete()) { Q_FOREACH(QQuickItem *item, m_visibleItems) { item->setHeight(rowHeight); } relayout(); } } } qreal HorizontalJournal::horizontalSpacing() const { return m_horizontalSpacing; } void HorizontalJournal::setHorizontalSpacing(qreal horizontalSpacing) { if (horizontalSpacing != m_horizontalSpacing) { m_horizontalSpacing = horizontalSpacing; Q_EMIT horizontalSpacingChanged(); if (isComponentComplete()) { relayout(); } } } qreal HorizontalJournal::verticalSpacing() const { return m_verticalSpacing; } void HorizontalJournal::setVerticalSpacing(qreal verticalSpacing) { if (verticalSpacing != m_verticalSpacing) { m_verticalSpacing = verticalSpacing; Q_EMIT verticalSpacingChanged(); if (isComponentComplete()) { relayout(); } } } qreal HorizontalJournal::delegateCreationBegin() const { return m_delegateCreationBegin; } void HorizontalJournal::setDelegateCreationBegin(qreal begin) { m_delegateCreationBeginValid = true; if (m_delegateCreationBegin == begin) return; m_delegateCreationBegin = begin; if (isComponentComplete()) { polish(); } emit delegateCreationBeginChanged(); } void HorizontalJournal::resetDelegateCreationBegin() { m_delegateCreationBeginValid = false; if (m_delegateCreationBegin == 0) return; m_delegateCreationBegin = 0; if (isComponentComplete()) { polish(); } emit delegateCreationBeginChanged(); } qreal HorizontalJournal::delegateCreationEnd() const { return m_delegateCreationEnd; } void HorizontalJournal::setDelegateCreationEnd(qreal end) { m_delegateCreationEndValid = true; if (m_delegateCreationEnd == end) return; m_delegateCreationEnd = end; if (isComponentComplete()) { polish(); } emit delegateCreationEndChanged(); } void HorizontalJournal::resetDelegateCreationEnd() { m_delegateCreationEndValid = false; if (m_delegateCreationEnd == 0) return; m_delegateCreationEnd = 0; if (isComponentComplete()) { polish(); } emit delegateCreationEndChanged(); } void HorizontalJournal::createDelegateModel() { #if (QT_VERSION < QT_VERSION_CHECK(5, 1, 0)) m_delegateModel = new QQuickVisualDataModel(qmlContext(this), this); connect(m_delegateModel, SIGNAL(createdItem(int,QQuickItem*)), this, SLOT(itemCreated(int,QQuickItem*))); #else m_delegateModel = new QQmlDelegateModel(qmlContext(this), this); connect(m_delegateModel, SIGNAL(createdItem(int,QObject*)), this, SLOT(itemCreated(int,QObject*))); #endif if (isComponentComplete()) m_delegateModel->componentComplete(); } void HorizontalJournal::refill() { if (!isComponentComplete()) { return; } const bool delegateRangesValid = m_delegateCreationBeginValid && m_delegateCreationEndValid; const qreal from = delegateRangesValid ? m_delegateCreationBegin : 0; const qreal to = delegateRangesValid ? m_delegateCreationEnd : from + height(); const qreal buffer = (to - from) * bufferRatio; const qreal bufferFrom = from - buffer; const qreal bufferTo = to + buffer; bool added = addVisibleItems(from, to, false); bool removed = removeNonVisibleItems(bufferFrom, bufferTo); added |= addVisibleItems(bufferFrom, bufferTo, true); if (added || removed) { m_implicitHeightDirty = true; } } void HorizontalJournal::findBottomModelIndexToAdd(int *modelIndex, double *yPos) { if (m_visibleItems.isEmpty()) { *modelIndex = 0; *yPos = m_verticalSpacing; } else { *modelIndex = m_firstVisibleIndex + m_visibleItems.count(); if (m_lastInRowIndexPosition.contains(*modelIndex - 1)) { *yPos = m_visibleItems.last()->y() + m_rowHeight + m_verticalSpacing; } else { *yPos = m_visibleItems.last()->y(); } } } void HorizontalJournal::findTopModelIndexToAdd(int *modelIndex, double *yPos) { if (m_visibleItems.isEmpty()) { *modelIndex = -1; *yPos = INT_MIN; } else { *modelIndex = m_firstVisibleIndex - 1; if (m_lastInRowIndexPosition.contains(*modelIndex)) { *yPos = m_visibleItems.first()->y() - m_verticalSpacing - m_rowHeight; } else { *yPos = m_visibleItems.first()->y(); } } } bool HorizontalJournal::addVisibleItems(qreal fillFrom, qreal fillTo, bool asynchronous) { if (!delegate()) return false; if (m_delegateModel->count() == 0) return false; int modelIndex; double yPos; findBottomModelIndexToAdd(&modelIndex, &yPos); bool changed = false; while (modelIndex < m_delegateModel->count() && yPos <= fillTo) { if (!createItem(modelIndex, asynchronous)) break; changed = true; findBottomModelIndexToAdd(&modelIndex, &yPos); } findTopModelIndexToAdd(&modelIndex, &yPos); while (modelIndex >= 0 && yPos > fillFrom) { if (!createItem(modelIndex, asynchronous)) break; changed = true; findTopModelIndexToAdd(&modelIndex, &yPos); } return changed; } bool HorizontalJournal::removeNonVisibleItems(qreal bufferFrom, qreal bufferTo) { bool changed = false; while (!m_visibleItems.isEmpty() && m_visibleItems.first()->y() + m_rowHeight < bufferFrom) { releaseItem(m_visibleItems.takeFirst()); changed = true; m_firstVisibleIndex++; } while (!m_visibleItems.isEmpty() && m_visibleItems.last()->y() > bufferTo) { releaseItem(m_visibleItems.takeLast()); changed = true; } if (m_visibleItems.isEmpty()) { m_firstVisibleIndex = -1; } return changed; } QQuickItem *HorizontalJournal::createItem(int modelIndex, bool asynchronous) { if (asynchronous && m_asyncRequestedIndex != -1) return nullptr; m_asyncRequestedIndex = -1; #if (QT_VERSION < QT_VERSION_CHECK(5, 1, 0)) QQuickItem *item = m_delegateModel->item(modelIndex, asynchronous); #else QObject* object = m_delegateModel->object(modelIndex, asynchronous); QQuickItem *item = qmlobject_cast(object); #endif if (!item) { #if (QT_VERSION < QT_VERSION_CHECK(5, 1, 0)) m_asyncRequestedIndex = modelIndex; #else if (object) { m_delegateModel->release(object); if (!m_delegateValidated) { m_delegateValidated = true; QObject* delegateObj = delegate(); qmlInfo(delegateObj ? delegateObj : this) << "Delegate must be of Item type"; } } else { m_asyncRequestedIndex = modelIndex; } #endif return nullptr; } else { if (item->height() != m_rowHeight) { qWarning() << "Item" << modelIndex << "height is not the one that the rowHeight mandates, resetting it"; item->setHeight(m_rowHeight); } positionItem(modelIndex, item); return item; } } void HorizontalJournal::positionItem(int modelIndex, QQuickItem *item) { if (m_visibleItems.isEmpty()) { Q_ASSERT(modelIndex == 0); item->setY(m_verticalSpacing); item->setX(m_horizontalSpacing); m_visibleItems << item; m_firstVisibleIndex = 0; } else { // modelIndex has to be either m_firstVisibleIndex - 1 or m_firstVisibleIndex + m_visibleItems.count() if (modelIndex == m_firstVisibleIndex + m_visibleItems.count()) { QQuickItem *lastItem = m_visibleItems.last(); if (lastItem->x() + lastItem->width() + m_horizontalSpacing + item->width() + m_horizontalSpacing <= width()) { // Fits in the row item->setY(lastItem->y()); item->setX(lastItem->x() + lastItem->width() + m_horizontalSpacing); } else { // Starts a new row item->setY(lastItem->y() + m_rowHeight + m_verticalSpacing); item->setX(m_horizontalSpacing); m_lastInRowIndexPosition[modelIndex - 1] = lastItem->x(); } m_visibleItems << item; } else if (modelIndex == m_firstVisibleIndex - 1) { QQuickItem *firstItem = m_visibleItems.first(); if (m_lastInRowIndexPosition.contains(modelIndex)) { // It is the last item of its row, so start a new one since we're going back item->setY(firstItem->y() - m_verticalSpacing - m_rowHeight); item->setX(m_lastInRowIndexPosition[modelIndex]); } else { item->setY(firstItem->y()); item->setX(firstItem->x() - m_horizontalSpacing - item->width()); } m_firstVisibleIndex = modelIndex; m_visibleItems.prepend(item); } else { qWarning() << "HorizontalJournal::positionItem - Got unexpected modelIndex" << modelIndex << m_firstVisibleIndex << m_visibleItems.count(); } } } void HorizontalJournal::releaseItem(QQuickItem *item) { #if (QT_VERSION < QT_VERSION_CHECK(5, 1, 0)) QQuickVisualModel::ReleaseFlags flags = m_delegateModel->release(item); if (flags & QQuickVisualModel::Destroyed) { #else QQmlDelegateModel::ReleaseFlags flags = m_delegateModel->release(item); if (flags & QQmlDelegateModel::Destroyed) { #endif item->setParentItem(nullptr); } } #if (QT_VERSION < QT_VERSION_CHECK(5, 1, 0)) void HorizontalJournal::itemCreated(int modelIndex, QQuickItem *item) { #else void HorizontalJournal::itemCreated(int modelIndex, QObject *object) { QQuickItem *item = qmlobject_cast(object); if (!item) { qWarning() << "ListViewWithPageHeader::itemCreated got a non item for index" << modelIndex; return; } #endif item->setParentItem(this); if (modelIndex == m_asyncRequestedIndex) { createItem(modelIndex, false); m_implicitHeightDirty = true; polish(); } } void HorizontalJournal::relayout() { m_needsRelayout = true; polish(); } void HorizontalJournal::onHeightChanged() { polish(); } void HorizontalJournal::updatePolish() { if (!model()) return; if (m_needsRelayout) { // If m_firstVisibleIndex is not 0 we need to drop all the delegates // since we can't consistently relayout without the first item being there if (m_firstVisibleIndex == 0) { int i = 0; Q_FOREACH(QQuickItem *item, m_visibleItems) { positionItem(i, item); ++i; } } else { Q_FOREACH(QQuickItem *item, m_visibleItems) { releaseItem(item); } m_visibleItems.clear(); m_lastInRowIndexPosition.clear(); m_firstVisibleIndex = 0; } m_needsRelayout = false; } refill(); const bool delegateRangesValid = m_delegateCreationBeginValid && m_delegateCreationEndValid; const qreal from = delegateRangesValid ? m_delegateCreationBegin : 0; const qreal to = delegateRangesValid ? m_delegateCreationEnd : from + height(); Q_FOREACH(QQuickItem *item, m_visibleItems) { QQuickItemPrivate::get(item)->setCulled(item->y() + m_rowHeight <= from || item->y() >= to); } if (m_implicitHeightDirty) { m_implicitHeightDirty = false; if (m_firstVisibleIndex >= 0) { const int nIndexes = m_firstVisibleIndex + m_visibleItems.count(); const double bottomMostY = m_visibleItems.last()->y() + m_rowHeight + m_verticalSpacing; const double averageHeight = bottomMostY / nIndexes; setImplicitHeight(bottomMostY + averageHeight * (model()->rowCount() - nIndexes)); } else { setImplicitHeight(0); } } } void HorizontalJournal::componentComplete() { if (m_delegateModel) m_delegateModel->componentComplete(); QQuickItem::componentComplete(); polish(); }