on cdea66a help sign in
unity8/plugins/DashViews/verticaljournal.cpp
558 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));
74 // Cleanup the existing items
75 for (int i = 0; i < m_columnVisibleItems.count(); ++i)
76 {
77 QList<ViewItem> &column = m_columnVisibleItems[i];
78 Q_FOREACH(const ViewItem &item, column)
79 releaseItem(item);
80 column.clear();
81 }
82 m_indexColumnMap.clear();
84 Q_EMIT modelChanged();
85 polish();
86 }
89QQmlComponent *VerticalJournal::delegate() const
91 return m_delegateModel ? m_delegateModel->delegate() : nullptr;
94void VerticalJournal::setDelegate(QQmlComponent *delegate)
96 if (delegate != this->delegate()) {
97 if (!m_delegateModel) {
98 createDelegateModel();
99 }
101 // Cleanup the existing items
102 for (int i = 0; i < m_columnVisibleItems.count(); ++i)
103 {
104 QList<ViewItem> &column = m_columnVisibleItems[i];
105 Q_FOREACH(const ViewItem &item, column)
106 releaseItem(item);
107 column.clear();
108 }
109 m_indexColumnMap.clear();
111 m_delegateModel->setDelegate(delegate);
113 Q_EMIT delegateChanged();
114 m_delegateValidated = false;
115 polish();
116 }
119qreal VerticalJournal::columnWidth() const
121 return m_columnWidth;
124void VerticalJournal::setColumnWidth(qreal columnWidth)
126 if (columnWidth != m_columnWidth) {
127 m_columnWidth = columnWidth;
128 Q_EMIT columnWidthChanged();
130 if (isComponentComplete()) {
131 Q_FOREACH(const auto &column, m_columnVisibleItems) {
132 Q_FOREACH(const ViewItem &item, column) {
133 item.m_item->setWidth(columnWidth);
134 }
135 }
136 relayout();
137 }
138 }
141qreal VerticalJournal::horizontalSpacing() const
143 return m_horizontalSpacing;
146void VerticalJournal::setHorizontalSpacing(qreal horizontalSpacing)
148 if (horizontalSpacing != m_horizontalSpacing) {
149 m_horizontalSpacing = horizontalSpacing;
150 Q_EMIT horizontalSpacingChanged();
152 if (isComponentComplete()) {
153 relayout();
154 }
155 }
158qreal VerticalJournal::verticalSpacing() const
160 return m_verticalSpacing;
163void VerticalJournal::setVerticalSpacing(qreal verticalSpacing)
165 if (verticalSpacing != m_verticalSpacing) {
166 m_verticalSpacing = verticalSpacing;
167 Q_EMIT verticalSpacingChanged();
169 if (isComponentComplete()) {
170 relayout();
171 }
172 }
175qreal VerticalJournal::delegateCreationBegin() const
177 return m_delegateCreationBegin;
180void VerticalJournal::setDelegateCreationBegin(qreal begin)
182 m_delegateCreationBeginValid = true;
183 if (m_delegateCreationBegin == begin)
184 return;
185 m_delegateCreationBegin = begin;
186 if (isComponentComplete()) {
187 polish();
188 }
189 emit delegateCreationBeginChanged();
192void VerticalJournal::resetDelegateCreationBegin()
194 m_delegateCreationBeginValid = false;
195 if (m_delegateCreationBegin == 0)
196 return;
197 m_delegateCreationBegin = 0;
198 if (isComponentComplete()) {
199 polish();
200 }
201 emit delegateCreationBeginChanged();
204qreal VerticalJournal::delegateCreationEnd() const
206 return m_delegateCreationEnd;
209void VerticalJournal::setDelegateCreationEnd(qreal end)
211 m_delegateCreationEndValid = true;
212 if (m_delegateCreationEnd == end)
213 return;
214 m_delegateCreationEnd = end;
215 if (isComponentComplete()) {
216 polish();
217 }
218 emit delegateCreationEndChanged();
221void VerticalJournal::resetDelegateCreationEnd()
223 m_delegateCreationEndValid = false;
224 if (m_delegateCreationEnd == 0)
225 return;
226 m_delegateCreationEnd = 0;
227 if (isComponentComplete()) {
228 polish();
229 }
230 emit delegateCreationEndChanged();
233void VerticalJournal::createDelegateModel()
235#if (QT_VERSION < QT_VERSION_CHECK(5, 1, 0))
236 m_delegateModel = new QQuickVisualDataModel(qmlContext(this), this);
237 connect(m_delegateModel, SIGNAL(createdItem(int,QQuickItem*)), this, SLOT(itemCreated(int,QQuickItem*)));
238#else
239 m_delegateModel = new QQmlDelegateModel(qmlContext(this), this);
240 connect(m_delegateModel, SIGNAL(createdItem(int,QObject*)), this, SLOT(itemCreated(int,QObject*)));
241#endif
242 if (isComponentComplete())
243 m_delegateModel->componentComplete();
246void VerticalJournal::refill()
248 if (!isComponentComplete()) {
249 return;
250 }
252 const bool delegateRangesValid = m_delegateCreationBeginValid && m_delegateCreationEndValid;
253 const qreal from = delegateRangesValid ? m_delegateCreationBegin : 0;
254 const qreal to = delegateRangesValid ? m_delegateCreationEnd : from + height();
255 const qreal buffer = (to - from) * bufferRatio;
256 const qreal bufferFrom = from - buffer;
257 const qreal bufferTo = to + buffer;
259 bool added = addVisibleItems(from, to, false);
260 bool removed = removeNonVisibleItems(bufferFrom, bufferTo);
261 added |= addVisibleItems(bufferFrom, bufferTo, true);
263 if (added || removed) {
264 m_implicitHeightDirty = true;
265 }
268void VerticalJournal::findBottomModelIndexToAdd(int *modelIndex, double *yPos)
270 *modelIndex = 0;
271 *yPos = INT_MAX;
273 Q_FOREACH(const auto &column, m_columnVisibleItems) {
274 if (!column.isEmpty()) {
275 const ViewItem &item = column.last();
276 *yPos = qMin(*yPos, static_cast<double>(item.y() + item.height() + m_verticalSpacing));
277 *modelIndex = qMax(*modelIndex, item.m_modelIndex + 1);
278 } else {
279 *yPos = 0;
280 }
281 }
284void VerticalJournal::findTopModelIndexToAdd(int *modelIndex, double *yPos)
286 *modelIndex = INT_MAX;
287 *yPos = INT_MIN;
288 int columnToAddTo = -1;
290 // Find the topmost free column
291 for (int i = 0; i < m_columnVisibleItems.count(); ++i) {
292 const auto &column = m_columnVisibleItems[i];
293 if (!column.isEmpty()) {
294 const ViewItem &item = column.first();
295 const auto itemTopPos = item.y() - m_verticalSpacing;
296 if (itemTopPos > *yPos) {
297 *yPos = itemTopPos;
298 *modelIndex = item.m_modelIndex - 1;
299 columnToAddTo = i;
300 }
301 }
302 }
304 if (*modelIndex > 0) {
305 Q_ASSERT(m_indexColumnMap.contains(*modelIndex));
306 while (m_indexColumnMap[*modelIndex] != columnToAddTo) {
307 // We found out that we have to add to columnToAddTo
308 // and thought that we had to add *modelIndex, but history tells
309 // it is not correct, so find up from *modelIndex until we found the index
310 // that has to end up in columnToAddTo
311 *modelIndex = *modelIndex - 1;
312 Q_ASSERT(m_indexColumnMap.contains(*modelIndex));
313 }
314 }
317bool VerticalJournal::addVisibleItems(qreal fillFrom, qreal fillTo, bool asynchronous)
319 if (!delegate())
320 return false;
322 if (m_delegateModel->count() == 0)
323 return false;
325 int modelIndex;
326 double yPos;
327 findBottomModelIndexToAdd(&modelIndex, &yPos);
328 bool changed = false;
329 while (modelIndex < m_delegateModel->count() && yPos <= fillTo) {
330 if (!createItem(modelIndex, asynchronous))
331 break;
333 changed = true;
334 findBottomModelIndexToAdd(&modelIndex, &yPos);
335 }
337 findTopModelIndexToAdd(&modelIndex, &yPos);
338 while (modelIndex >= 0 && yPos > fillFrom) {
339 if (!createItem(modelIndex, asynchronous))
340 break;
342 changed = true;
343 findTopModelIndexToAdd(&modelIndex, &yPos);
344 }
346 return changed;
349bool VerticalJournal::removeNonVisibleItems(qreal bufferFrom, qreal bufferTo)
351 bool changed = false;
353 for (int i = 0; i < m_columnVisibleItems.count(); ++i) {
354 QList<ViewItem> &column = m_columnVisibleItems[i];
355 while (!column.isEmpty() && column.first().y() + column.first().height() < bufferFrom) {
356 releaseItem(column.takeFirst());
357 changed = true;
358 }
360 while (!column.isEmpty() && column.last().y() > bufferTo) {
361 releaseItem(column.takeLast());
362 changed = true;
363 }
364 }
366 return changed;
369QQuickItem *VerticalJournal::createItem(int modelIndex, bool asynchronous)
371 if (asynchronous && m_asyncRequestedIndex != -1)
372 return nullptr;
374 m_asyncRequestedIndex = -1;
375#if (QT_VERSION < QT_VERSION_CHECK(5, 1, 0))
376 QQuickItem *item = m_delegateModel->item(modelIndex, asynchronous);
377#else
378 QObject* object = m_delegateModel->object(modelIndex, asynchronous);
379 QQuickItem *item = qmlobject_cast<QQuickItem*>(object);
380#endif
381 if (!item) {
382#if (QT_VERSION < QT_VERSION_CHECK(5, 1, 0))
383 m_asyncRequestedIndex = modelIndex;
384#else
385 if (object) {
386 m_delegateModel->release(object);
387 if (!m_delegateValidated) {
388 m_delegateValidated = true;
389 QObject* delegateObj = delegate();
390 qmlInfo(delegateObj ? delegateObj : this) << "Delegate must be of Item type";
391 }
392 } else {
393 m_asyncRequestedIndex = modelIndex;
394 }
395#endif
396 return nullptr;
397 } else {
398 if (item->width() != m_columnWidth) {
399 qWarning() << "Item" << modelIndex << "width is not the one that the columnWidth mandates, resetting it";
400 item->setWidth(m_columnWidth);
401 }
402 positionItem(modelIndex, item);
403 return item;
404 }
407void VerticalJournal::positionItem(int modelIndex, QQuickItem *item)
409 // Check if we add it to the bottom of existing column items
410 qreal columnToAddY = !m_columnVisibleItems[0].isEmpty() ? m_columnVisibleItems[0].last().y() + m_columnVisibleItems[0].last().height() : 0;
411 int columnToAddTo = 0;
412 for (int i = 1; i < m_columnVisibleItems.count(); ++i) {
413 const qreal iY = !m_columnVisibleItems[i].isEmpty() ? m_columnVisibleItems[i].last().y() + m_columnVisibleItems[i].last().height() : 0;
414 if (iY < columnToAddY) {
415 columnToAddTo = i;
416 columnToAddY = iY;
417 }
418 }
419 if (m_columnVisibleItems[columnToAddTo].isEmpty() || m_columnVisibleItems[columnToAddTo].last().m_modelIndex < modelIndex) {
420 item->setX(columnToAddTo * m_columnWidth + m_horizontalSpacing * (columnToAddTo + 1));
421 item->setY(columnToAddY + m_verticalSpacing);
423 m_columnVisibleItems[columnToAddTo] << ViewItem(item, modelIndex);
424 m_indexColumnMap[modelIndex] = columnToAddTo;
425 } else {
426 Q_ASSERT(m_indexColumnMap.contains(modelIndex));
427 columnToAddTo = m_indexColumnMap[modelIndex];
428 columnToAddY = m_columnVisibleItems[columnToAddTo].first().y();
430 item->setX(columnToAddTo * m_columnWidth + m_horizontalSpacing * (columnToAddTo + 1));
431 item->setY(columnToAddY - m_verticalSpacing - item->height());
433 m_columnVisibleItems[columnToAddTo].prepend(ViewItem(item, modelIndex));
434 }
437void VerticalJournal::releaseItem(const ViewItem &item)
439#if (QT_VERSION < QT_VERSION_CHECK(5, 1, 0))
440 QQuickVisualModel::ReleaseFlags flags = m_delegateModel->release(item.m_item);
441 if (flags & QQuickVisualModel::Destroyed) {
442#else
443 QQmlDelegateModel::ReleaseFlags flags = m_delegateModel->release(item.m_item);
444 if (flags & QQmlDelegateModel::Destroyed) {
445#endif
446 item.m_item->setParentItem(nullptr);
447 }
450#if (QT_VERSION < QT_VERSION_CHECK(5, 1, 0))
451void VerticalJournal::itemCreated(int modelIndex, QQuickItem *item)
453#else
454void VerticalJournal::itemCreated(int modelIndex, QObject *object)
456 QQuickItem *item = qmlobject_cast<QQuickItem*>(object);
457 if (!item) {
458 qWarning() << "ListViewWithPageHeader::itemCreated got a non item for index" << modelIndex;
459 return;
460 }
461#endif
462 item->setParentItem(this);
463 if (modelIndex == m_asyncRequestedIndex) {
464 createItem(modelIndex, false);
465 m_implicitHeightDirty = true;
466 polish();
467 }
470void VerticalJournal::relayout()
472 m_needsRelayout = true;
473 polish();
476void VerticalJournal::onHeightChanged()
478 polish();
481void VerticalJournal::updatePolish()
483 if (!model())
484 return;
486 if (m_needsRelayout) {
487 QList<ViewItem> allItems;
488 Q_FOREACH(const auto &column, m_columnVisibleItems)
489 allItems << column;
491 qSort(allItems);
493 const int nColumns = qMax(1., floor((double)(width() - m_horizontalSpacing) / (m_columnWidth + m_horizontalSpacing)));
494 m_columnVisibleItems.resize(nColumns);
495 m_indexColumnMap.clear();
496 for (int i = 0; i < nColumns; ++i)
497 m_columnVisibleItems[i].clear();
499 // If the first of allItems doesn't contain index 0 we need to drop them
500 // all since we can't consistently relayout without the first item being there
502 if (!allItems.isEmpty()) {
503 if (allItems.first().m_modelIndex == 0) {
504 Q_FOREACH(const ViewItem &item, allItems)
505 positionItem(item.m_modelIndex, item.m_item);
506 } else {
507 Q_FOREACH(const ViewItem &item, allItems)
508 releaseItem(item);
509 }
510 }
512 m_needsRelayout = false;
513 }
515 refill();
517 const bool delegateRangesValid = m_delegateCreationBeginValid && m_delegateCreationEndValid;
518 const qreal from = delegateRangesValid ? m_delegateCreationBegin : 0;
519 const qreal to = delegateRangesValid ? m_delegateCreationEnd : from + height();
520 Q_FOREACH(const auto &column, m_columnVisibleItems) {
521 Q_FOREACH(const ViewItem &item, column) {
522 QQuickItemPrivate::get(item.m_item)->setCulled(item.y() + item.height() <= from || item.y() >= to);
523 }
524 }
526 if (m_implicitHeightDirty) {
527 m_implicitHeightDirty = false;
529 int lastModelIndex = -1;
530 qreal bottomMostY = 0;
531 Q_FOREACH(const auto &column, m_columnVisibleItems) {
532 if (!column.isEmpty()) {
533 const ViewItem &item = column.last();
534 lastModelIndex = qMax(lastModelIndex, item.m_modelIndex);
535 bottomMostY = qMax(bottomMostY, item.y() + item.height() + m_verticalSpacing);
536 }
537 }
538 if (lastModelIndex >= 0) {
539 const double averageHeight = bottomMostY / (lastModelIndex + 1);
540 setImplicitHeight(bottomMostY + averageHeight * (model()->rowCount() - lastModelIndex - 1));
541 } else {
542 setImplicitHeight(0);
543 }
544 }
547void VerticalJournal::componentComplete()
549 if (m_delegateModel)
550 m_delegateModel->componentComplete();
552 QQuickItem::componentComplete();
554 const int nColumns = qMax(1., floor((double)(width() - m_horizontalSpacing) / (m_columnWidth + m_horizontalSpacing)));
555 m_columnVisibleItems.resize(nColumns);
557 polish();