on 0263412 help sign in
unity8/plugins/DashViews/verticaljournal.cpp
551 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#include "verticaljournal.h"
19#include <qqmlengine.h>
20#pragma GCC diagnostic push
21#pragma GCC diagnostic ignored "-pedantic"
22#if (QT_VERSION < QT_VERSION_CHECK(5, 1, 0))
23#include <private/qquickvisualdatamodel_p.h>
24#else
25#include <private/qqmldelegatemodel_p.h>
26#include <qqmlinfo.h>
27#endif
28#include <private/qquickitem_p.h>
29#pragma GCC diagnostic pop
31static const qreal bufferRatio = 0.5;
33VerticalJournal::VerticalJournal()
34 : m_delegateModel(nullptr)
35 , m_asyncRequestedIndex(-1)
36 , m_columnWidth(0)
37 , m_horizontalSpacing(0)
38 , m_verticalSpacing(0)
39 , m_delegateCreationBegin(0)
40 , m_delegateCreationEnd(0)
41 , m_delegateCreationBeginValid(false)
42 , m_delegateCreationEndValid(false)
43 , m_needsRelayout(false)
44 , m_delegateValidated(false)
45 , m_implicitHeightDirty(false)
47 connect(this, SIGNAL(widthChanged()), this, SLOT(relayout()));
48 connect(this, SIGNAL(heightChanged()), this, SLOT(onHeightChanged()));
51QAbstractItemModel *VerticalJournal::model() const
53 return m_delegateModel ? m_delegateModel->model().value<QAbstractItemModel *>() : nullptr;
56void VerticalJournal::setModel(QAbstractItemModel *model)
58 if (model != this->model()) {
59 if (!m_delegateModel) {
60 createDelegateModel();
61 }
62#if (QT_VERSION < QT_VERSION_CHECK(5, 1, 0))
63 m_delegateModel->setModel(QVariant::fromValue<QAbstractItemModel *>(model));
64#else
65 m_delegateModel->setModel(QVariant::fromValue<QAbstractItemModel *>(model));
66#endif
67 // Cleanup the existing items
68 for (int i = 0; i < m_columnVisibleItems.count(); ++i)
69 {
70 QList<ViewItem> &column = m_columnVisibleItems[i];
71 Q_FOREACH(const ViewItem &item, column)
72 releaseItem(item);
73 column.clear();
74 }
75 m_indexColumnMap.clear();
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 // Cleanup the existing items
95 for (int i = 0; i < m_columnVisibleItems.count(); ++i)
96 {
97 QList<ViewItem> &column = m_columnVisibleItems[i];
98 Q_FOREACH(const ViewItem &item, column)
99 releaseItem(item);
100 column.clear();
101 }
102 m_indexColumnMap.clear();
104 m_delegateModel->setDelegate(delegate);
106 Q_EMIT delegateChanged();
107 m_delegateValidated = false;
108 polish();
109 }
112qreal VerticalJournal::columnWidth() const
114 return m_columnWidth;
117void VerticalJournal::setColumnWidth(qreal columnWidth)
119 if (columnWidth != m_columnWidth) {
120 m_columnWidth = columnWidth;
121 Q_EMIT columnWidthChanged();
123 if (isComponentComplete()) {
124 Q_FOREACH(const auto &column, m_columnVisibleItems) {
125 Q_FOREACH(const ViewItem &item, column) {
126 item.m_item->setWidth(columnWidth);
127 }
128 }
129 relayout();
130 }
131 }
134qreal VerticalJournal::horizontalSpacing() const
136 return m_horizontalSpacing;
139void VerticalJournal::setHorizontalSpacing(qreal horizontalSpacing)
141 if (horizontalSpacing != m_horizontalSpacing) {
142 m_horizontalSpacing = horizontalSpacing;
143 Q_EMIT horizontalSpacingChanged();
145 if (isComponentComplete()) {
146 relayout();
147 }
148 }
151qreal VerticalJournal::verticalSpacing() const
153 return m_verticalSpacing;
156void VerticalJournal::setVerticalSpacing(qreal verticalSpacing)
158 if (verticalSpacing != m_verticalSpacing) {
159 m_verticalSpacing = verticalSpacing;
160 Q_EMIT verticalSpacingChanged();
162 if (isComponentComplete()) {
163 relayout();
164 }
165 }
168qreal VerticalJournal::delegateCreationBegin() const
170 return m_delegateCreationBegin;
173void VerticalJournal::setDelegateCreationBegin(qreal begin)
175 m_delegateCreationBeginValid = true;
176 if (m_delegateCreationBegin == begin)
177 return;
178 m_delegateCreationBegin = begin;
179 if (isComponentComplete()) {
180 polish();
181 }
182 emit delegateCreationBeginChanged();
185void VerticalJournal::resetDelegateCreationBegin()
187 m_delegateCreationBeginValid = false;
188 if (m_delegateCreationBegin == 0)
189 return;
190 m_delegateCreationBegin = 0;
191 if (isComponentComplete()) {
192 polish();
193 }
194 emit delegateCreationBeginChanged();
197qreal VerticalJournal::delegateCreationEnd() const
199 return m_delegateCreationEnd;
202void VerticalJournal::setDelegateCreationEnd(qreal end)
204 m_delegateCreationEndValid = true;
205 if (m_delegateCreationEnd == end)
206 return;
207 m_delegateCreationEnd = end;
208 if (isComponentComplete()) {
209 polish();
210 }
211 emit delegateCreationEndChanged();
214void VerticalJournal::resetDelegateCreationEnd()
216 m_delegateCreationEndValid = false;
217 if (m_delegateCreationEnd == 0)
218 return;
219 m_delegateCreationEnd = 0;
220 if (isComponentComplete()) {
221 polish();
222 }
223 emit delegateCreationEndChanged();
226void VerticalJournal::createDelegateModel()
228#if (QT_VERSION < QT_VERSION_CHECK(5, 1, 0))
229 m_delegateModel = new QQuickVisualDataModel(qmlContext(this), this);
230 connect(m_delegateModel, SIGNAL(createdItem(int,QQuickItem*)), this, SLOT(itemCreated(int,QQuickItem*)));
231#else
232 m_delegateModel = new QQmlDelegateModel(qmlContext(this), this);
233 connect(m_delegateModel, SIGNAL(createdItem(int,QObject*)), this, SLOT(itemCreated(int,QObject*)));
234#endif
235 if (isComponentComplete())
236 m_delegateModel->componentComplete();
239void VerticalJournal::refill()
241 if (!isComponentComplete()) {
242 return;
243 }
245 const bool delegateRangesValid = m_delegateCreationBeginValid && m_delegateCreationEndValid;
246 const qreal from = delegateRangesValid ? m_delegateCreationBegin : 0;
247 const qreal to = delegateRangesValid ? m_delegateCreationEnd : from + height();
248 const qreal buffer = (to - from) * bufferRatio;
249 const qreal bufferFrom = from - buffer;
250 const qreal bufferTo = to + buffer;
252 bool added = addVisibleItems(from, to, false);
253 bool removed = removeNonVisibleItems(bufferFrom, bufferTo);
254 added |= addVisibleItems(bufferFrom, bufferTo, true);
256 if (added || removed) {
257 m_implicitHeightDirty = true;
258 }
261void VerticalJournal::findBottomModelIndexToAdd(int *modelIndex, double *yPos)
263 *modelIndex = 0;
264 *yPos = INT_MAX;
266 Q_FOREACH(const auto &column, m_columnVisibleItems) {
267 if (!column.isEmpty()) {
268 const ViewItem &item = column.last();
269 *yPos = qMin(*yPos, item.y() + item.height() + m_verticalSpacing);
270 *modelIndex = qMax(*modelIndex, item.m_modelIndex + 1);
271 } else {
272 *yPos = 0;
273 }
274 }
277void VerticalJournal::findTopModelIndexToAdd(int *modelIndex, double *yPos)
279 *modelIndex = INT_MAX;
280 *yPos = INT_MIN;
281 int columnToAddTo = -1;
283 // Find the topmost free column
284 for (int i = 0; i < m_columnVisibleItems.count(); ++i) {
285 const auto &column = m_columnVisibleItems[i];
286 if (!column.isEmpty()) {
287 const ViewItem &item = column.first();
288 const auto itemTopPos = item.y() - m_verticalSpacing;
289 if (itemTopPos > *yPos) {
290 *yPos = itemTopPos;
291 *modelIndex = item.m_modelIndex - 1;
292 columnToAddTo = i;
293 }
294 }
295 }
297 if (*modelIndex > 0) {
298 Q_ASSERT(m_indexColumnMap.contains(*modelIndex));
299 while (m_indexColumnMap[*modelIndex] != columnToAddTo) {
300 // We found out that we have to add to columnToAddTo
301 // and thought that we had to add *modelIndex, but history tells
302 // it is not correct, so find up from *modelIndex until we found the index
303 // that has to end up in columnToAddTo
304 *modelIndex = *modelIndex - 1;
305 Q_ASSERT(m_indexColumnMap.contains(*modelIndex));
306 }
307 }
310bool VerticalJournal::addVisibleItems(qreal fillFrom, qreal fillTo, bool asynchronous)
312 if (!delegate())
313 return false;
315 if (m_delegateModel->count() == 0)
316 return false;
318 int modelIndex;
319 double yPos;
320 findBottomModelIndexToAdd(&modelIndex, &yPos);
321 bool changed = false;
322 while (modelIndex < m_delegateModel->count() && yPos <= fillTo) {
323 if (!createItem(modelIndex, asynchronous))
324 break;
326 changed = true;
327 findBottomModelIndexToAdd(&modelIndex, &yPos);
328 }
330 findTopModelIndexToAdd(&modelIndex, &yPos);
331 while (modelIndex >= 0 && yPos > fillFrom) {
332 if (!createItem(modelIndex, asynchronous))
333 break;
335 changed = true;
336 findTopModelIndexToAdd(&modelIndex, &yPos);
337 }
339 return changed;
342bool VerticalJournal::removeNonVisibleItems(qreal bufferFrom, qreal bufferTo)
344 bool changed = false;
346 for (int i = 0; i < m_columnVisibleItems.count(); ++i) {
347 QList<ViewItem> &column = m_columnVisibleItems[i];
348 while (!column.isEmpty() && column.first().y() + column.first().height() < bufferFrom) {
349 releaseItem(column.takeFirst());
350 changed = true;
351 }
353 while (!column.isEmpty() && column.last().y() > bufferTo) {
354 releaseItem(column.takeLast());
355 changed = true;
356 }
357 }
359 return changed;
362QQuickItem *VerticalJournal::createItem(int modelIndex, bool asynchronous)
364 if (asynchronous && m_asyncRequestedIndex != -1)
365 return nullptr;
367 m_asyncRequestedIndex = -1;
368#if (QT_VERSION < QT_VERSION_CHECK(5, 1, 0))
369 QQuickItem *item = m_delegateModel->item(modelIndex, asynchronous);
370#else
371 QObject* object = m_delegateModel->object(modelIndex, asynchronous);
372 QQuickItem *item = qmlobject_cast<QQuickItem*>(object);
373#endif
374 if (!item) {
375#if (QT_VERSION < QT_VERSION_CHECK(5, 1, 0))
376 m_asyncRequestedIndex = modelIndex;
377#else
378 if (object) {
379 m_delegateModel->release(object);
380 if (!m_delegateValidated) {
381 m_delegateValidated = true;
382 QObject* delegateObj = delegate();
383 qmlInfo(delegateObj ? delegateObj : this) << "Delegate must be of Item type";
384 }
385 } else {
386 m_asyncRequestedIndex = modelIndex;
387 }
388#endif
389 return nullptr;
390 } else {
391 if (item->width() != m_columnWidth) {
392 qWarning() << "Item" << modelIndex << "width is not the one that the columnWidth mandates, resetting it";
393 item->setWidth(m_columnWidth);
394 }
395 positionItem(modelIndex, item);
396 return item;
397 }
400void VerticalJournal::positionItem(int modelIndex, QQuickItem *item)
402 // Check if we add it to the bottom of existing column items
403 qreal columnToAddY = !m_columnVisibleItems[0].isEmpty() ? m_columnVisibleItems[0].last().y() + m_columnVisibleItems[0].last().height() : 0;
404 int columnToAddTo = 0;
405 for (int i = 1; i < m_columnVisibleItems.count(); ++i) {
406 const qreal iY = !m_columnVisibleItems[i].isEmpty() ? m_columnVisibleItems[i].last().y() + m_columnVisibleItems[i].last().height() : 0;
407 if (iY < columnToAddY) {
408 columnToAddTo = i;
409 columnToAddY = iY;
410 }
411 }
412 if (m_columnVisibleItems[columnToAddTo].isEmpty() || m_columnVisibleItems[columnToAddTo].last().m_modelIndex < modelIndex) {
413 item->setX(columnToAddTo * m_columnWidth + m_horizontalSpacing * (columnToAddTo + 1));
414 item->setY(columnToAddY + m_verticalSpacing);
416 m_columnVisibleItems[columnToAddTo] << ViewItem(item, modelIndex);
417 m_indexColumnMap[modelIndex] = columnToAddTo;
418 } else {
419 Q_ASSERT(m_indexColumnMap.contains(modelIndex));
420 columnToAddTo = m_indexColumnMap[modelIndex];
421 columnToAddY = m_columnVisibleItems[columnToAddTo].first().y();
423 item->setX(columnToAddTo * m_columnWidth + m_horizontalSpacing * (columnToAddTo + 1));
424 item->setY(columnToAddY - m_verticalSpacing - item->height());
426 m_columnVisibleItems[columnToAddTo].prepend(ViewItem(item, modelIndex));
427 }
430void VerticalJournal::releaseItem(const ViewItem &item)
432#if (QT_VERSION < QT_VERSION_CHECK(5, 1, 0))
433 QQuickVisualModel::ReleaseFlags flags = m_delegateModel->release(item.m_item);
434 if (flags & QQuickVisualModel::Destroyed) {
435#else
436 QQmlDelegateModel::ReleaseFlags flags = m_delegateModel->release(item.m_item);
437 if (flags & QQmlDelegateModel::Destroyed) {
438#endif
439 item.m_item->setParentItem(nullptr);
440 }
443#if (QT_VERSION < QT_VERSION_CHECK(5, 1, 0))
444void VerticalJournal::itemCreated(int modelIndex, QQuickItem *item)
446#else
447void VerticalJournal::itemCreated(int modelIndex, QObject *object)
449 QQuickItem *item = qmlobject_cast<QQuickItem*>(object);
450 if (!item) {
451 qWarning() << "ListViewWithPageHeader::itemCreated got a non item for index" << modelIndex;
452 return;
453 }
454#endif
455 item->setParentItem(this);
456 if (modelIndex == m_asyncRequestedIndex) {
457 createItem(modelIndex, false);
458 m_implicitHeightDirty = true;
459 polish();
460 }
463void VerticalJournal::relayout()
465 m_needsRelayout = true;
466 polish();
469void VerticalJournal::onHeightChanged()
471 polish();
474void VerticalJournal::updatePolish()
476 if (!model())
477 return;
479 if (m_needsRelayout) {
480 QList<ViewItem> allItems;
481 Q_FOREACH(const auto &column, m_columnVisibleItems)
482 allItems << column;
484 qSort(allItems);
486 const int nColumns = qMax(1., floor((double)(width() - m_horizontalSpacing) / (m_columnWidth + m_horizontalSpacing)));
487 m_columnVisibleItems.resize(nColumns);
488 m_indexColumnMap.clear();
489 for (int i = 0; i < nColumns; ++i)
490 m_columnVisibleItems[i].clear();
492 // If the first of allItems doesn't contain index 0 we need to drop them
493 // all since we can't consistently relayout without the first item being there
495 if (!allItems.isEmpty()) {
496 if (allItems.first().m_modelIndex == 0) {
497 Q_FOREACH(const ViewItem &item, allItems)
498 positionItem(item.m_modelIndex, item.m_item);
499 } else {
500 Q_FOREACH(const ViewItem &item, allItems)
501 releaseItem(item);
502 }
503 }
505 m_needsRelayout = false;
506 }
508 refill();
510 const bool delegateRangesValid = m_delegateCreationBeginValid && m_delegateCreationEndValid;
511 const qreal from = delegateRangesValid ? m_delegateCreationBegin : 0;
512 const qreal to = delegateRangesValid ? m_delegateCreationEnd : from + height();
513 Q_FOREACH(const auto &column, m_columnVisibleItems) {
514 Q_FOREACH(const ViewItem &item, column) {
515 QQuickItemPrivate::get(item.m_item)->setCulled(item.y() + item.height() <= from || item.y() >= to);
516 }
517 }
519 if (m_implicitHeightDirty) {
520 m_implicitHeightDirty = false;
522 int lastModelIndex = -1;
523 qreal bottomMostY = 0;
524 Q_FOREACH(const auto &column, m_columnVisibleItems) {
525 if (!column.isEmpty()) {
526 const ViewItem &item = column.last();
527 lastModelIndex = qMax(lastModelIndex, item.m_modelIndex);
528 bottomMostY = qMax(bottomMostY, item.y() + item.height() + m_verticalSpacing);
529 }
530 }
531 if (lastModelIndex >= 0) {
532 const double averageHeight = bottomMostY / (lastModelIndex + 1);
533 setImplicitHeight(bottomMostY + averageHeight * (model()->rowCount() - lastModelIndex - 1));
534 } else {
535 setImplicitHeight(0);
536 }
537 }
540void VerticalJournal::componentComplete()
542 if (m_delegateModel)
543 m_delegateModel->componentComplete();
545 QQuickItem::componentComplete();
547 const int nColumns = ceil((double)(width() - m_horizontalSpacing) / (m_columnWidth + m_horizontalSpacing));
548 m_columnVisibleItems.resize(nColumns);
550 polish();