/*
* Copyright (C) 2013, 2016 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 "verticaljournal.h"
#include
#include
VerticalJournal::VerticalJournal()
: m_columnWidth(0)
{
}
qreal VerticalJournal::columnWidth() const
{
return m_columnWidth;
}
void VerticalJournal::setColumnWidth(qreal columnWidth)
{
if (columnWidth != m_columnWidth) {
m_columnWidth = columnWidth;
Q_EMIT columnWidthChanged();
if (isComponentComplete()) {
Q_FOREACH(const auto &column, m_columnVisibleItems) {
Q_FOREACH(const ViewItem item, column) {
item.m_item->setWidth(columnWidth);
}
}
relayout();
}
}
}
void VerticalJournal::findBottomModelIndexToAdd(int *modelIndex, qreal *yPos)
{
*modelIndex = 0;
*yPos = std::numeric_limits::max();
Q_FOREACH(const auto &column, m_columnVisibleItems) {
if (!column.isEmpty()) {
const ViewItem &item = column.last();
*yPos = qMin(*yPos, item.y() + item.height() + rowSpacing());
*modelIndex = qMax(*modelIndex, item.m_modelIndex + 1);
} else {
*yPos = 0;
}
}
}
void VerticalJournal::findTopModelIndexToAdd(int *modelIndex, qreal *yPos)
{
*modelIndex = 0;
*yPos = std::numeric_limits::lowest();
int columnToAddTo = -1;
// Find the topmost free column
for (int i = 0; i < m_columnVisibleItems.count(); ++i) {
const auto &column = m_columnVisibleItems[i];
if (!column.isEmpty()) {
const ViewItem &item = column.first();
const auto itemTopPos = item.y() - rowSpacing();
if (itemTopPos > *yPos) {
*yPos = itemTopPos;
*modelIndex = item.m_modelIndex - 1;
columnToAddTo = i;
}
}
}
if (*modelIndex > 0) {
Q_ASSERT(m_indexColumnMap.contains(*modelIndex));
while (*modelIndex > 0 && m_indexColumnMap[*modelIndex] != columnToAddTo) {
// We found out that we have to add to columnToAddTo
// and thought that we had to add *modelIndex, but history tells
// it is not correct, so find up from *modelIndex until we found the index
// that has to end up in columnToAddTo
*modelIndex = *modelIndex - 1;
Q_ASSERT(m_indexColumnMap.contains(*modelIndex));
}
}
}
bool VerticalJournal::removeNonVisibleItems(qreal bufferFromY, qreal bufferToY)
{
bool changed = false;
for (int i = 0; i < m_columnVisibleItems.count(); ++i) {
QList &column = m_columnVisibleItems[i];
while (!column.isEmpty() && column.first().y() + column.first().height() < bufferFromY) {
releaseItem(column.takeFirst().m_item);
changed = true;
}
while (!column.isEmpty() && column.last().y() > bufferToY) {
releaseItem(column.takeLast().m_item);
changed = true;
}
}
return changed;
}
void VerticalJournal::addItemToView(int modelIndex, QQuickItem *item)
{
if (item->width() != m_columnWidth) {
qWarning() << "Item" << modelIndex << "width is not the one that the columnWidth mandates, resetting it";
item->setWidth(m_columnWidth);
}
// Check if we add it to the bottom of existing column items
const QList &firstColumn = m_columnVisibleItems[0];
qreal columnToAddY = !firstColumn.isEmpty() ? firstColumn.last().y() + firstColumn.last().height() : -rowSpacing();
int columnToAddTo = 0;
for (int i = 1; i < m_columnVisibleItems.count(); ++i) {
const QList &column = m_columnVisibleItems[i];
const qreal iY = !column.isEmpty() ? column.last().y() + column.last().height() : -rowSpacing();
if (iY < columnToAddY) {
columnToAddTo = i;
columnToAddY = iY;
}
}
const QList &columnToAdd = m_columnVisibleItems[columnToAddTo];
if (columnToAdd.isEmpty() || columnToAdd.last().m_modelIndex < modelIndex) {
item->setX(columnToAddTo * (m_columnWidth + columnSpacing()));
item->setY(columnToAddY + rowSpacing());
m_columnVisibleItems[columnToAddTo] << ViewItem(item, modelIndex);
m_indexColumnMap[modelIndex] = columnToAddTo;
} else {
Q_ASSERT(m_indexColumnMap.contains(modelIndex));
columnToAddTo = m_indexColumnMap[modelIndex];
columnToAddY = m_columnVisibleItems[columnToAddTo].first().y();
item->setX(columnToAddTo * (m_columnWidth + columnSpacing()));
item->setY(columnToAddY - rowSpacing() - item->height());
m_columnVisibleItems[columnToAddTo].prepend(ViewItem(item, modelIndex));
}
}
void VerticalJournal::cleanupExistingItems()
{
// Cleanup the existing items
for (int i = 0; i < m_columnVisibleItems.count(); ++i) {
QList &column = m_columnVisibleItems[i];
Q_FOREACH(const ViewItem item, column)
releaseItem(item.m_item);
column.clear();
}
m_indexColumnMap.clear();
setImplicitHeightDirty();
}
void VerticalJournal::calculateImplicitHeight()
{
int lastModelIndex = -1;
qreal bottomMostY = 0;
Q_FOREACH(const auto &column, m_columnVisibleItems) {
if (!column.isEmpty()) {
const ViewItem &item = column.last();
lastModelIndex = qMax(lastModelIndex, item.m_modelIndex);
bottomMostY = qMax(bottomMostY, item.y() + item.height());
}
}
if (lastModelIndex >= 0) {
const double averageHeight = bottomMostY / (lastModelIndex + 1);
setImplicitHeight(bottomMostY + averageHeight * (model()->rowCount() - lastModelIndex - 1));
} else {
setImplicitHeight(0);
}
}
void VerticalJournal::doRelayout()
{
QList allItems;
Q_FOREACH(const auto &column, m_columnVisibleItems)
allItems << column;
qSort(allItems);
const int nColumns = qMax(1., floor((double)(width() + columnSpacing()) / (m_columnWidth + columnSpacing())));
m_columnVisibleItems.resize(nColumns);
m_indexColumnMap.clear();
for (int i = 0; i < nColumns; ++i)
m_columnVisibleItems[i].clear();
// If the first of allItems doesn't contain index 0 we need to drop them
// all since we can't consistently relayout without the first item being there
if (!allItems.isEmpty()) {
if (allItems.first().m_modelIndex == 0) {
Q_FOREACH(const ViewItem item, allItems)
addItemToView(item.m_modelIndex, item.m_item);
} else {
Q_FOREACH(const ViewItem item, allItems)
releaseItem(item.m_item);
}
}
}
void VerticalJournal::updateItemCulling(qreal visibleFromY, qreal visibleToY)
{
Q_FOREACH(const auto &column, m_columnVisibleItems) {
Q_FOREACH(const ViewItem item, column) {
const bool cull = item.y() + item.height() <= visibleFromY || item.y() >= visibleToY;
QQuickItemPrivate::get(item.m_item)->setCulled(cull);
}
}
}
void VerticalJournal::processModelRemoves(const QVector &removes)
{
Q_FOREACH(const QQmlChangeSet::Change remove, removes) {
for (int i = remove.count - 1; i >= 0; --i) {
const int indexToRemove = remove.index + i;
// Since we only support removing from the end, indexToRemove
// must refer to the last item of one of the columns or
// be bigger than them (because it's not in the viewport and
// thus we have not created a delegate for it)
bool found = false;
int lastCreatedIndex = INT_MIN;
for (int i = 0; !found && i < m_columnVisibleItems.count(); ++i) {
QList &column = m_columnVisibleItems[i];
if (!column.isEmpty()) {
const int lastColumnIndex = column.last().m_modelIndex;
if (lastColumnIndex == indexToRemove) {
releaseItem(column.takeLast().m_item);
found = true;
}
lastCreatedIndex = qMax(lastCreatedIndex, lastColumnIndex);
}
}
if (!found) {
if (indexToRemove < lastCreatedIndex) {
qDebug() << "VerticalJournal only supports removal from the end of the model, resetting instead";
cleanupExistingItems();
break;
} else {
setImplicitHeightDirty();
}
}
}
}
}
void VerticalJournal::itemGeometryChanged(QQuickItem * /*item*/, const QRectF &newGeometry, const QRectF &oldGeometry)
{
const qreal heightDiff = newGeometry.height() - oldGeometry.height();
if (heightDiff != 0) {
relayout();
}
}