on f110847 help sign in
unity8/plugins/DashViews/verticaljournal.cpp
554 lines 17 kB history raw
1/*
2 * Copyright (C) 2013 Canonical, Ltd.
3 *
4 * This program is free software; you can redistribute it and/or modify
5 * it under the terms of the GNU General Public License as published by
6 * the Free Software Foundation; version 3.
7 *
8 * This program is distributed in the hope that it will be useful,
9 * but WITHOUT ANY WARRANTY; without even the implied warranty of
10 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 * GNU General Public License for more details.
12 *
13 * You should have received a copy of the GNU General Public License
14 * along with this program. If not, see <http://www.gnu.org/licenses/>.
15 */
17/*
18 * The implementation is centered around m_columnVisibleItems
19 * that holds a vector of lists. There's a list for each of the
20 * columns the view has. In the list the items of the column are
21 * ordered as they appear topdown in the view. m_indexColumnMap is
22 * used when re-building the list up since given a position
23 * in the middle of the list and the need to create the previous does
24 * not give us enough information to know in which column we have
25 * to position the item so that when we reach the item the view is
26 * correctly layouted at 0 for all the columns
27 */
28#include "verticaljournal.h"
30#include <qqmlengine.h>
31#pragma GCC diagnostic push
32#pragma GCC diagnostic ignored "-pedantic"
33#if (QT_VERSION < QT_VERSION_CHECK(5, 1, 0))
34#include <private/qquickvisualdatamodel_p.h>
35#else
36#include <private/qqmldelegatemodel_p.h>
37#include <qqmlinfo.h>
38#endif
39#include <private/qquickitem_p.h>
40#pragma GCC diagnostic pop
42static const qreal bufferRatio = 0.5;
44VerticalJournal::VerticalJournal()
45 : m_delegateModel(nullptr)
46 , m_asyncRequestedIndex(-1)
47 , m_columnWidth(0)
48 , m_horizontalSpacing(0)
49 , m_verticalSpacing(0)
50 , m_delegateCreationBegin(0)
51 , m_delegateCreationEnd(0)
52 , m_delegateCreationBeginValid(false)
53 , m_delegateCreationEndValid(false)
54 , m_needsRelayout(false)
55 , m_delegateValidated(false)
56 , m_implicitHeightDirty(false)
58 connect(this, SIGNAL(widthChanged()), this, SLOT(relayout()));
59 connect(this, SIGNAL(heightChanged()), this, SLOT(onHeightChanged()));
62QAbstractItemModel *VerticalJournal::model() const
64 return m_delegateModel ? m_delegateModel->model().value<QAbstractItemModel *>() : nullptr;
67void VerticalJournal::setModel(QAbstractItemModel *model)
69 if (model != this->model()) {
70 if (!m_delegateModel) {
71 createDelegateModel();
72 }
73 m_delegateModel->setModel(QVariant::fromValue<QAbstractItemModel *>(model));
75 cleanupExistingItems();
77 Q_EMIT modelChanged();
78 polish();
79 }
82QQmlComponent *VerticalJournal::delegate() const
84 return m_delegateModel ? m_delegateModel->delegate() : nullptr;
87void VerticalJournal::setDelegate(QQmlComponent *delegate)
89 if (delegate != this->delegate()) {
90 if (!m_delegateModel) {
91 createDelegateModel();
92 }
94 cleanupExistingItems();
96 m_delegateModel->setDelegate(delegate);
98 Q_EMIT delegateChanged();
99 m_delegateValidated = false;
100 polish();
101 }
104qreal VerticalJournal::columnWidth() const
106 return m_columnWidth;
109void VerticalJournal::setColumnWidth(qreal columnWidth)
111 if (columnWidth != m_columnWidth) {
112 m_columnWidth = columnWidth;
113 Q_EMIT columnWidthChanged();
115 if (isComponentComplete()) {
116 Q_FOREACH(const auto &column, m_columnVisibleItems) {
117 Q_FOREACH(const ViewItem &item, column) {
118 item.m_item->setWidth(columnWidth);
119 }
120 }
121 relayout();
122 }
123 }
126qreal VerticalJournal::horizontalSpacing() const
128 return m_horizontalSpacing;
131void VerticalJournal::setHorizontalSpacing(qreal horizontalSpacing)
133 if (horizontalSpacing != m_horizontalSpacing) {
134 m_horizontalSpacing = horizontalSpacing;
135 Q_EMIT horizontalSpacingChanged();
137 if (isComponentComplete()) {
138 relayout();
139 }
140 }
143qreal VerticalJournal::verticalSpacing() const
145 return m_verticalSpacing;
148void VerticalJournal::setVerticalSpacing(qreal verticalSpacing)
150 if (verticalSpacing != m_verticalSpacing) {
151 m_verticalSpacing = verticalSpacing;
152 Q_EMIT verticalSpacingChanged();
154 if (isComponentComplete()) {
155 relayout();
156 }
157 }
160qreal VerticalJournal::delegateCreationBegin() const
162 return m_delegateCreationBegin;
165void VerticalJournal::setDelegateCreationBegin(qreal begin)
167 m_delegateCreationBeginValid = true;
168 if (m_delegateCreationBegin == begin)
169 return;
170 m_delegateCreationBegin = begin;
171 if (isComponentComplete()) {
172 polish();
173 }
174 emit delegateCreationBeginChanged();
177void VerticalJournal::resetDelegateCreationBegin()
179 m_delegateCreationBeginValid = false;
180 if (m_delegateCreationBegin == 0)
181 return;
182 m_delegateCreationBegin = 0;
183 if (isComponentComplete()) {
184 polish();
185 }
186 emit delegateCreationBeginChanged();
189qreal VerticalJournal::delegateCreationEnd() const
191 return m_delegateCreationEnd;
194void VerticalJournal::setDelegateCreationEnd(qreal end)
196 m_delegateCreationEndValid = true;
197 if (m_delegateCreationEnd == end)
198 return;
199 m_delegateCreationEnd = end;
200 if (isComponentComplete()) {
201 polish();
202 }
203 emit delegateCreationEndChanged();
206void VerticalJournal::resetDelegateCreationEnd()
208 m_delegateCreationEndValid = false;
209 if (m_delegateCreationEnd == 0)
210 return;
211 m_delegateCreationEnd = 0;
212 if (isComponentComplete()) {
213 polish();
214 }
215 emit delegateCreationEndChanged();
218void VerticalJournal::createDelegateModel()
220#if (QT_VERSION < QT_VERSION_CHECK(5, 1, 0))
221 m_delegateModel = new QQuickVisualDataModel(qmlContext(this), this);
222 connect(m_delegateModel, SIGNAL(createdItem(int,QQuickItem*)), this, SLOT(itemCreated(int,QQuickItem*)));
223#else
224 m_delegateModel = new QQmlDelegateModel(qmlContext(this), this);
225 connect(m_delegateModel, SIGNAL(createdItem(int,QObject*)), this, SLOT(itemCreated(int,QObject*)));
226#endif
227 if (isComponentComplete())
228 m_delegateModel->componentComplete();
231void VerticalJournal::refill()
233 if (!isComponentComplete()) {
234 return;
235 }
237 const bool delegateRangesValid = m_delegateCreationBeginValid && m_delegateCreationEndValid;
238 const qreal from = delegateRangesValid ? m_delegateCreationBegin : 0;
239 const qreal to = delegateRangesValid ? m_delegateCreationEnd : from + height();
240 const qreal buffer = (to - from) * bufferRatio;
241 const qreal bufferFrom = from - buffer;
242 const qreal bufferTo = to + buffer;
244 bool added = addVisibleItems(from, to, false);
245 bool removed = removeNonVisibleItems(bufferFrom, bufferTo);
246 added |= addVisibleItems(bufferFrom, bufferTo, true);
248 if (added || removed) {
249 m_implicitHeightDirty = true;
250 }
253void VerticalJournal::findBottomModelIndexToAdd(int *modelIndex, double *yPos)
255 *modelIndex = 0;
256 *yPos = INT_MAX;
258 Q_FOREACH(const auto &column, m_columnVisibleItems) {
259 if (!column.isEmpty()) {
260 const ViewItem &item = column.last();
261 *yPos = qMin(*yPos, static_cast<double>(item.y() + item.height() + m_verticalSpacing));
262 *modelIndex = qMax(*modelIndex, item.m_modelIndex + 1);
263 } else {
264 *yPos = 0;
265 }
266 }
269void VerticalJournal::findTopModelIndexToAdd(int *modelIndex, double *yPos)
271 *modelIndex = INT_MAX;
272 *yPos = INT_MIN;
273 int columnToAddTo = -1;
275 // Find the topmost free column
276 for (int i = 0; i < m_columnVisibleItems.count(); ++i) {
277 const auto &column = m_columnVisibleItems[i];
278 if (!column.isEmpty()) {
279 const ViewItem &item = column.first();
280 const auto itemTopPos = item.y() - m_verticalSpacing;
281 if (itemTopPos > *yPos) {
282 *yPos = itemTopPos;
283 *modelIndex = item.m_modelIndex - 1;
284 columnToAddTo = i;
285 }
286 }
287 }
289 if (*modelIndex > 0) {
290 Q_ASSERT(m_indexColumnMap.contains(*modelIndex));
291 while (m_indexColumnMap[*modelIndex] != columnToAddTo) {
292 // We found out that we have to add to columnToAddTo
293 // and thought that we had to add *modelIndex, but history tells
294 // it is not correct, so find up from *modelIndex until we found the index
295 // that has to end up in columnToAddTo
296 *modelIndex = *modelIndex - 1;
297 Q_ASSERT(m_indexColumnMap.contains(*modelIndex));
298 }
299 }
302bool VerticalJournal::addVisibleItems(qreal fillFrom, qreal fillTo, bool asynchronous)
304 if (!delegate())
305 return false;
307 if (m_delegateModel->count() == 0)
308 return false;
310 int modelIndex;
311 double yPos;
312 findBottomModelIndexToAdd(&modelIndex, &yPos);
313 bool changed = false;
314 while (modelIndex < m_delegateModel->count() && yPos <= fillTo) {
315 if (!createItem(modelIndex, asynchronous))
316 break;
318 changed = true;
319 findBottomModelIndexToAdd(&modelIndex, &yPos);
320 }
322 findTopModelIndexToAdd(&modelIndex, &yPos);
323 while (modelIndex >= 0 && yPos > fillFrom) {
324 if (!createItem(modelIndex, asynchronous))
325 break;
327 changed = true;
328 findTopModelIndexToAdd(&modelIndex, &yPos);
329 }
331 return changed;
334bool VerticalJournal::removeNonVisibleItems(qreal bufferFrom, qreal bufferTo)
336 bool changed = false;
338 for (int i = 0; i < m_columnVisibleItems.count(); ++i) {
339 QList<ViewItem> &column = m_columnVisibleItems[i];
340 while (!column.isEmpty() && column.first().y() + column.first().height() < bufferFrom) {
341 releaseItem(column.takeFirst());
342 changed = true;
343 }
345 while (!column.isEmpty() && column.last().y() > bufferTo) {
346 releaseItem(column.takeLast());
347 changed = true;
348 }
349 }
351 return changed;
354QQuickItem *VerticalJournal::createItem(int modelIndex, bool asynchronous)
356 if (asynchronous && m_asyncRequestedIndex != -1)
357 return nullptr;
359 m_asyncRequestedIndex = -1;
360#if (QT_VERSION < QT_VERSION_CHECK(5, 1, 0))
361 QQuickItem *item = m_delegateModel->item(modelIndex, asynchronous);
362#else
363 QObject* object = m_delegateModel->object(modelIndex, asynchronous);
364 QQuickItem *item = qmlobject_cast<QQuickItem*>(object);
365#endif
366 if (!item) {
367#if (QT_VERSION < QT_VERSION_CHECK(5, 1, 0))
368 m_asyncRequestedIndex = modelIndex;
369#else
370 if (object) {
371 m_delegateModel->release(object);
372 if (!m_delegateValidated) {
373 m_delegateValidated = true;
374 QObject* delegateObj = delegate();
375 qmlInfo(delegateObj ? delegateObj : this) << "Delegate must be of Item type";
376 }
377 } else {
378 m_asyncRequestedIndex = modelIndex;
379 }
380#endif
381 return nullptr;
382 } else {
383 if (item->width() != m_columnWidth) {
384 qWarning() << "Item" << modelIndex << "width is not the one that the columnWidth mandates, resetting it";
385 item->setWidth(m_columnWidth);
386 }
387 positionItem(modelIndex, item);
388 return item;
389 }
392void VerticalJournal::positionItem(int modelIndex, QQuickItem *item)
394 // Check if we add it to the bottom of existing column items
395 qreal columnToAddY = !m_columnVisibleItems[0].isEmpty() ? m_columnVisibleItems[0].last().y() + m_columnVisibleItems[0].last().height() : 0;
396 int columnToAddTo = 0;
397 for (int i = 1; i < m_columnVisibleItems.count(); ++i) {
398 const qreal iY = !m_columnVisibleItems[i].isEmpty() ? m_columnVisibleItems[i].last().y() + m_columnVisibleItems[i].last().height() : 0;
399 if (iY < columnToAddY) {
400 columnToAddTo = i;
401 columnToAddY = iY;
402 }
403 }
404 if (m_columnVisibleItems[columnToAddTo].isEmpty() || m_columnVisibleItems[columnToAddTo].last().m_modelIndex < modelIndex) {
405 item->setX(columnToAddTo * m_columnWidth + m_horizontalSpacing * (columnToAddTo + 1));
406 item->setY(columnToAddY + m_verticalSpacing);
408 m_columnVisibleItems[columnToAddTo] << ViewItem(item, modelIndex);
409 m_indexColumnMap[modelIndex] = columnToAddTo;
410 } else {
411 Q_ASSERT(m_indexColumnMap.contains(modelIndex));
412 columnToAddTo = m_indexColumnMap[modelIndex];
413 columnToAddY = m_columnVisibleItems[columnToAddTo].first().y();
415 item->setX(columnToAddTo * m_columnWidth + m_horizontalSpacing * (columnToAddTo + 1));
416 item->setY(columnToAddY - m_verticalSpacing - item->height());
418 m_columnVisibleItems[columnToAddTo].prepend(ViewItem(item, modelIndex));
419 }
422void VerticalJournal::cleanupExistingItems()
424 // Cleanup the existing items
425 for (int i = 0; i < m_columnVisibleItems.count(); ++i) {
426 QList<ViewItem> &column = m_columnVisibleItems[i];
427 Q_FOREACH(const ViewItem &item, column)
428 releaseItem(item);
429 column.clear();
430 }
431 m_indexColumnMap.clear();
434void VerticalJournal::releaseItem(const ViewItem &item)
436#if (QT_VERSION < QT_VERSION_CHECK(5, 1, 0))
437 QQuickVisualModel::ReleaseFlags flags = m_delegateModel->release(item.m_item);
438 if (flags & QQuickVisualModel::Destroyed) {
439#else
440 QQmlDelegateModel::ReleaseFlags flags = m_delegateModel->release(item.m_item);
441 if (flags & QQmlDelegateModel::Destroyed) {
442#endif
443 item.m_item->setParentItem(nullptr);
444 }
447#if (QT_VERSION < QT_VERSION_CHECK(5, 1, 0))
448void VerticalJournal::itemCreated(int modelIndex, QQuickItem *item)
450#else
451void VerticalJournal::itemCreated(int modelIndex, QObject *object)
453 QQuickItem *item = qmlobject_cast<QQuickItem*>(object);
454 if (!item) {
455 qWarning() << "ListViewWithPageHeader::itemCreated got a non item for index" << modelIndex;
456 return;
457 }
458#endif
459 item->setParentItem(this);
460 if (modelIndex == m_asyncRequestedIndex) {
461 createItem(modelIndex, false);
462 m_implicitHeightDirty = true;
463 polish();
464 }
467void VerticalJournal::relayout()
469 m_needsRelayout = true;
470 polish();
473void VerticalJournal::onHeightChanged()
475 polish();
478void VerticalJournal::updatePolish()
480 if (!model())
481 return;
483 if (m_needsRelayout) {
484 QList<ViewItem> allItems;
485 Q_FOREACH(const auto &column, m_columnVisibleItems)
486 allItems << column;
488 qSort(allItems);
490 const int nColumns = qMax(1., floor((double)(width() - m_horizontalSpacing) / (m_columnWidth + m_horizontalSpacing)));
491 m_columnVisibleItems.resize(nColumns);
492 m_indexColumnMap.clear();
493 for (int i = 0; i < nColumns; ++i)
494 m_columnVisibleItems[i].clear();
496 // If the first of allItems doesn't contain index 0 we need to drop them
497 // all since we can't consistently relayout without the first item being there
499 if (!allItems.isEmpty()) {
500 if (allItems.first().m_modelIndex == 0) {
501 Q_FOREACH(const ViewItem &item, allItems)
502 positionItem(item.m_modelIndex, item.m_item);
503 } else {
504 Q_FOREACH(const ViewItem &item, allItems)
505 releaseItem(item);
506 }
507 }
509 m_needsRelayout = false;
510 }
512 refill();
514 const bool delegateRangesValid = m_delegateCreationBeginValid && m_delegateCreationEndValid;
515 const qreal from = delegateRangesValid ? m_delegateCreationBegin : 0;
516 const qreal to = delegateRangesValid ? m_delegateCreationEnd : from + height();
517 Q_FOREACH(const auto &column, m_columnVisibleItems) {
518 Q_FOREACH(const ViewItem &item, column) {
519 QQuickItemPrivate::get(item.m_item)->setCulled(item.y() + item.height() <= from || item.y() >= to);
520 }
521 }
523 if (m_implicitHeightDirty) {
524 m_implicitHeightDirty = false;
526 int lastModelIndex = -1;
527 qreal bottomMostY = 0;
528 Q_FOREACH(const auto &column, m_columnVisibleItems) {
529 if (!column.isEmpty()) {
530 const ViewItem &item = column.last();
531 lastModelIndex = qMax(lastModelIndex, item.m_modelIndex);
532 bottomMostY = qMax(bottomMostY, item.y() + item.height() + m_verticalSpacing);
533 }
534 }
535 if (lastModelIndex >= 0) {
536 const double averageHeight = bottomMostY / (lastModelIndex + 1);
537 setImplicitHeight(bottomMostY + averageHeight * (model()->rowCount() - lastModelIndex - 1));
538 } else {
539 setImplicitHeight(0);
540 }
541 }
544void VerticalJournal::componentComplete()
546 if (m_delegateModel)
547 m_delegateModel->componentComplete();
549 QQuickItem::componentComplete();
551 m_needsRelayout = true;
553 polish();