on 7271d00 help sign in
unity8/plugins/DashViews/verticaljournal.cpp
562 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#if (QT_VERSION < QT_VERSION_CHECK(5, 1, 0))
74 m_delegateModel->setModel(QVariant::fromValue<QAbstractItemModel *>(model));
75#else
76 m_delegateModel->setModel(QVariant::fromValue<QAbstractItemModel *>(model));
77#endif
78 // Cleanup the existing items
79 for (int i = 0; i < m_columnVisibleItems.count(); ++i)
80 {
81 QList<ViewItem> &column = m_columnVisibleItems[i];
82 Q_FOREACH(const ViewItem &item, column)
83 releaseItem(item);
84 column.clear();
85 }
86 m_indexColumnMap.clear();
88 Q_EMIT modelChanged();
89 polish();
90 }
93QQmlComponent *VerticalJournal::delegate() const
95 return m_delegateModel ? m_delegateModel->delegate() : nullptr;
98void VerticalJournal::setDelegate(QQmlComponent *delegate)
100 if (delegate != this->delegate()) {
101 if (!m_delegateModel) {
102 createDelegateModel();
103 }
105 // Cleanup the existing items
106 for (int i = 0; i < m_columnVisibleItems.count(); ++i)
107 {
108 QList<ViewItem> &column = m_columnVisibleItems[i];
109 Q_FOREACH(const ViewItem &item, column)
110 releaseItem(item);
111 column.clear();
112 }
113 m_indexColumnMap.clear();
115 m_delegateModel->setDelegate(delegate);
117 Q_EMIT delegateChanged();
118 m_delegateValidated = false;
119 polish();
120 }
123qreal VerticalJournal::columnWidth() const
125 return m_columnWidth;
128void VerticalJournal::setColumnWidth(qreal columnWidth)
130 if (columnWidth != m_columnWidth) {
131 m_columnWidth = columnWidth;
132 Q_EMIT columnWidthChanged();
134 if (isComponentComplete()) {
135 Q_FOREACH(const auto &column, m_columnVisibleItems) {
136 Q_FOREACH(const ViewItem &item, column) {
137 item.m_item->setWidth(columnWidth);
138 }
139 }
140 relayout();
141 }
142 }
145qreal VerticalJournal::horizontalSpacing() const
147 return m_horizontalSpacing;
150void VerticalJournal::setHorizontalSpacing(qreal horizontalSpacing)
152 if (horizontalSpacing != m_horizontalSpacing) {
153 m_horizontalSpacing = horizontalSpacing;
154 Q_EMIT horizontalSpacingChanged();
156 if (isComponentComplete()) {
157 relayout();
158 }
159 }
162qreal VerticalJournal::verticalSpacing() const
164 return m_verticalSpacing;
167void VerticalJournal::setVerticalSpacing(qreal verticalSpacing)
169 if (verticalSpacing != m_verticalSpacing) {
170 m_verticalSpacing = verticalSpacing;
171 Q_EMIT verticalSpacingChanged();
173 if (isComponentComplete()) {
174 relayout();
175 }
176 }
179qreal VerticalJournal::delegateCreationBegin() const
181 return m_delegateCreationBegin;
184void VerticalJournal::setDelegateCreationBegin(qreal begin)
186 m_delegateCreationBeginValid = true;
187 if (m_delegateCreationBegin == begin)
188 return;
189 m_delegateCreationBegin = begin;
190 if (isComponentComplete()) {
191 polish();
192 }
193 emit delegateCreationBeginChanged();
196void VerticalJournal::resetDelegateCreationBegin()
198 m_delegateCreationBeginValid = false;
199 if (m_delegateCreationBegin == 0)
200 return;
201 m_delegateCreationBegin = 0;
202 if (isComponentComplete()) {
203 polish();
204 }
205 emit delegateCreationBeginChanged();
208qreal VerticalJournal::delegateCreationEnd() const
210 return m_delegateCreationEnd;
213void VerticalJournal::setDelegateCreationEnd(qreal end)
215 m_delegateCreationEndValid = true;
216 if (m_delegateCreationEnd == end)
217 return;
218 m_delegateCreationEnd = end;
219 if (isComponentComplete()) {
220 polish();
221 }
222 emit delegateCreationEndChanged();
225void VerticalJournal::resetDelegateCreationEnd()
227 m_delegateCreationEndValid = false;
228 if (m_delegateCreationEnd == 0)
229 return;
230 m_delegateCreationEnd = 0;
231 if (isComponentComplete()) {
232 polish();
233 }
234 emit delegateCreationEndChanged();
237void VerticalJournal::createDelegateModel()
239#if (QT_VERSION < QT_VERSION_CHECK(5, 1, 0))
240 m_delegateModel = new QQuickVisualDataModel(qmlContext(this), this);
241 connect(m_delegateModel, SIGNAL(createdItem(int,QQuickItem*)), this, SLOT(itemCreated(int,QQuickItem*)));
242#else
243 m_delegateModel = new QQmlDelegateModel(qmlContext(this), this);
244 connect(m_delegateModel, SIGNAL(createdItem(int,QObject*)), this, SLOT(itemCreated(int,QObject*)));
245#endif
246 if (isComponentComplete())
247 m_delegateModel->componentComplete();
250void VerticalJournal::refill()
252 if (!isComponentComplete()) {
253 return;
254 }
256 const bool delegateRangesValid = m_delegateCreationBeginValid && m_delegateCreationEndValid;
257 const qreal from = delegateRangesValid ? m_delegateCreationBegin : 0;
258 const qreal to = delegateRangesValid ? m_delegateCreationEnd : from + height();
259 const qreal buffer = (to - from) * bufferRatio;
260 const qreal bufferFrom = from - buffer;
261 const qreal bufferTo = to + buffer;
263 bool added = addVisibleItems(from, to, false);
264 bool removed = removeNonVisibleItems(bufferFrom, bufferTo);
265 added |= addVisibleItems(bufferFrom, bufferTo, true);
267 if (added || removed) {
268 m_implicitHeightDirty = true;
269 }
272void VerticalJournal::findBottomModelIndexToAdd(int *modelIndex, double *yPos)
274 *modelIndex = 0;
275 *yPos = INT_MAX;
277 Q_FOREACH(const auto &column, m_columnVisibleItems) {
278 if (!column.isEmpty()) {
279 const ViewItem &item = column.last();
280 *yPos = qMin(*yPos, static_cast<double>(item.y() + item.height() + m_verticalSpacing));
281 *modelIndex = qMax(*modelIndex, item.m_modelIndex + 1);
282 } else {
283 *yPos = 0;
284 }
285 }
288void VerticalJournal::findTopModelIndexToAdd(int *modelIndex, double *yPos)
290 *modelIndex = INT_MAX;
291 *yPos = INT_MIN;
292 int columnToAddTo = -1;
294 // Find the topmost free column
295 for (int i = 0; i < m_columnVisibleItems.count(); ++i) {
296 const auto &column = m_columnVisibleItems[i];
297 if (!column.isEmpty()) {
298 const ViewItem &item = column.first();
299 const auto itemTopPos = item.y() - m_verticalSpacing;
300 if (itemTopPos > *yPos) {
301 *yPos = itemTopPos;
302 *modelIndex = item.m_modelIndex - 1;
303 columnToAddTo = i;
304 }
305 }
306 }
308 if (*modelIndex > 0) {
309 Q_ASSERT(m_indexColumnMap.contains(*modelIndex));
310 while (m_indexColumnMap[*modelIndex] != columnToAddTo) {
311 // We found out that we have to add to columnToAddTo
312 // and thought that we had to add *modelIndex, but history tells
313 // it is not correct, so find up from *modelIndex until we found the index
314 // that has to end up in columnToAddTo
315 *modelIndex = *modelIndex - 1;
316 Q_ASSERT(m_indexColumnMap.contains(*modelIndex));
317 }
318 }
321bool VerticalJournal::addVisibleItems(qreal fillFrom, qreal fillTo, bool asynchronous)
323 if (!delegate())
324 return false;
326 if (m_delegateModel->count() == 0)
327 return false;
329 int modelIndex;
330 double yPos;
331 findBottomModelIndexToAdd(&modelIndex, &yPos);
332 bool changed = false;
333 while (modelIndex < m_delegateModel->count() && yPos <= fillTo) {
334 if (!createItem(modelIndex, asynchronous))
335 break;
337 changed = true;
338 findBottomModelIndexToAdd(&modelIndex, &yPos);
339 }
341 findTopModelIndexToAdd(&modelIndex, &yPos);
342 while (modelIndex >= 0 && yPos > fillFrom) {
343 if (!createItem(modelIndex, asynchronous))
344 break;
346 changed = true;
347 findTopModelIndexToAdd(&modelIndex, &yPos);
348 }
350 return changed;
353bool VerticalJournal::removeNonVisibleItems(qreal bufferFrom, qreal bufferTo)
355 bool changed = false;
357 for (int i = 0; i < m_columnVisibleItems.count(); ++i) {
358 QList<ViewItem> &column = m_columnVisibleItems[i];
359 while (!column.isEmpty() && column.first().y() + column.first().height() < bufferFrom) {
360 releaseItem(column.takeFirst());
361 changed = true;
362 }
364 while (!column.isEmpty() && column.last().y() > bufferTo) {
365 releaseItem(column.takeLast());
366 changed = true;
367 }
368 }
370 return changed;
373QQuickItem *VerticalJournal::createItem(int modelIndex, bool asynchronous)
375 if (asynchronous && m_asyncRequestedIndex != -1)
376 return nullptr;
378 m_asyncRequestedIndex = -1;
379#if (QT_VERSION < QT_VERSION_CHECK(5, 1, 0))
380 QQuickItem *item = m_delegateModel->item(modelIndex, asynchronous);
381#else
382 QObject* object = m_delegateModel->object(modelIndex, asynchronous);
383 QQuickItem *item = qmlobject_cast<QQuickItem*>(object);
384#endif
385 if (!item) {
386#if (QT_VERSION < QT_VERSION_CHECK(5, 1, 0))
387 m_asyncRequestedIndex = modelIndex;
388#else
389 if (object) {
390 m_delegateModel->release(object);
391 if (!m_delegateValidated) {
392 m_delegateValidated = true;
393 QObject* delegateObj = delegate();
394 qmlInfo(delegateObj ? delegateObj : this) << "Delegate must be of Item type";
395 }
396 } else {
397 m_asyncRequestedIndex = modelIndex;
398 }
399#endif
400 return nullptr;
401 } else {
402 if (item->width() != m_columnWidth) {
403 qWarning() << "Item" << modelIndex << "width is not the one that the columnWidth mandates, resetting it";
404 item->setWidth(m_columnWidth);
405 }
406 positionItem(modelIndex, item);
407 return item;
408 }
411void VerticalJournal::positionItem(int modelIndex, QQuickItem *item)
413 // Check if we add it to the bottom of existing column items
414 qreal columnToAddY = !m_columnVisibleItems[0].isEmpty() ? m_columnVisibleItems[0].last().y() + m_columnVisibleItems[0].last().height() : 0;
415 int columnToAddTo = 0;
416 for (int i = 1; i < m_columnVisibleItems.count(); ++i) {
417 const qreal iY = !m_columnVisibleItems[i].isEmpty() ? m_columnVisibleItems[i].last().y() + m_columnVisibleItems[i].last().height() : 0;
418 if (iY < columnToAddY) {
419 columnToAddTo = i;
420 columnToAddY = iY;
421 }
422 }
423 if (m_columnVisibleItems[columnToAddTo].isEmpty() || m_columnVisibleItems[columnToAddTo].last().m_modelIndex < modelIndex) {
424 item->setX(columnToAddTo * m_columnWidth + m_horizontalSpacing * (columnToAddTo + 1));
425 item->setY(columnToAddY + m_verticalSpacing);
427 m_columnVisibleItems[columnToAddTo] << ViewItem(item, modelIndex);
428 m_indexColumnMap[modelIndex] = columnToAddTo;
429 } else {
430 Q_ASSERT(m_indexColumnMap.contains(modelIndex));
431 columnToAddTo = m_indexColumnMap[modelIndex];
432 columnToAddY = m_columnVisibleItems[columnToAddTo].first().y();
434 item->setX(columnToAddTo * m_columnWidth + m_horizontalSpacing * (columnToAddTo + 1));
435 item->setY(columnToAddY - m_verticalSpacing - item->height());
437 m_columnVisibleItems[columnToAddTo].prepend(ViewItem(item, modelIndex));
438 }
441void VerticalJournal::releaseItem(const ViewItem &item)
443#if (QT_VERSION < QT_VERSION_CHECK(5, 1, 0))
444 QQuickVisualModel::ReleaseFlags flags = m_delegateModel->release(item.m_item);
445 if (flags & QQuickVisualModel::Destroyed) {
446#else
447 QQmlDelegateModel::ReleaseFlags flags = m_delegateModel->release(item.m_item);
448 if (flags & QQmlDelegateModel::Destroyed) {
449#endif
450 item.m_item->setParentItem(nullptr);
451 }
454#if (QT_VERSION < QT_VERSION_CHECK(5, 1, 0))
455void VerticalJournal::itemCreated(int modelIndex, QQuickItem *item)
457#else
458void VerticalJournal::itemCreated(int modelIndex, QObject *object)
460 QQuickItem *item = qmlobject_cast<QQuickItem*>(object);
461 if (!item) {
462 qWarning() << "ListViewWithPageHeader::itemCreated got a non item for index" << modelIndex;
463 return;
464 }
465#endif
466 item->setParentItem(this);
467 if (modelIndex == m_asyncRequestedIndex) {
468 createItem(modelIndex, false);
469 m_implicitHeightDirty = true;
470 polish();
471 }
474void VerticalJournal::relayout()
476 m_needsRelayout = true;
477 polish();
480void VerticalJournal::onHeightChanged()
482 polish();
485void VerticalJournal::updatePolish()
487 if (!model())
488 return;
490 if (m_needsRelayout) {
491 QList<ViewItem> allItems;
492 Q_FOREACH(const auto &column, m_columnVisibleItems)
493 allItems << column;
495 qSort(allItems);
497 const int nColumns = qMax(1., floor((double)(width() - m_horizontalSpacing) / (m_columnWidth + m_horizontalSpacing)));
498 m_columnVisibleItems.resize(nColumns);
499 m_indexColumnMap.clear();
500 for (int i = 0; i < nColumns; ++i)
501 m_columnVisibleItems[i].clear();
503 // If the first of allItems doesn't contain index 0 we need to drop them
504 // all since we can't consistently relayout without the first item being there
506 if (!allItems.isEmpty()) {
507 if (allItems.first().m_modelIndex == 0) {
508 Q_FOREACH(const ViewItem &item, allItems)
509 positionItem(item.m_modelIndex, item.m_item);
510 } else {
511 Q_FOREACH(const ViewItem &item, allItems)
512 releaseItem(item);
513 }
514 }
516 m_needsRelayout = false;
517 }
519 refill();
521 const bool delegateRangesValid = m_delegateCreationBeginValid && m_delegateCreationEndValid;
522 const qreal from = delegateRangesValid ? m_delegateCreationBegin : 0;
523 const qreal to = delegateRangesValid ? m_delegateCreationEnd : from + height();
524 Q_FOREACH(const auto &column, m_columnVisibleItems) {
525 Q_FOREACH(const ViewItem &item, column) {
526 QQuickItemPrivate::get(item.m_item)->setCulled(item.y() + item.height() <= from || item.y() >= to);
527 }
528 }
530 if (m_implicitHeightDirty) {
531 m_implicitHeightDirty = false;
533 int lastModelIndex = -1;
534 qreal bottomMostY = 0;
535 Q_FOREACH(const auto &column, m_columnVisibleItems) {
536 if (!column.isEmpty()) {
537 const ViewItem &item = column.last();
538 lastModelIndex = qMax(lastModelIndex, item.m_modelIndex);
539 bottomMostY = qMax(bottomMostY, item.y() + item.height() + m_verticalSpacing);
540 }
541 }
542 if (lastModelIndex >= 0) {
543 const double averageHeight = bottomMostY / (lastModelIndex + 1);
544 setImplicitHeight(bottomMostY + averageHeight * (model()->rowCount() - lastModelIndex - 1));
545 } else {
546 setImplicitHeight(0);
547 }
548 }
551void VerticalJournal::componentComplete()
553 if (m_delegateModel)
554 m_delegateModel->componentComplete();
556 QQuickItem::componentComplete();
558 const int nColumns = ceil((double)(width() - m_horizontalSpacing) / (m_columnWidth + m_horizontalSpacing));
559 m_columnVisibleItems.resize(nColumns);
561 polish();