@@ -0,0 +1,574 @@
1+/*
2+ * Copyright (C) 2014 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+ */
16+
17+#include "organicgrid.h"
18+
19+#pragma GCC diagnostic push
20+#pragma GCC diagnostic ignored "-pedantic"
21+#include <private/qquickitem_p.h>
22+#pragma GCC diagnostic pop
23+
24+static const qreal bufferRatio = 0.5;
25+
26+OrganicGrid::OrganicGrid()
27+ : m_delegateModel(nullptr)
28+ , m_asyncRequestedIndex(-1)
29+ , m_columnSpacing(0)
30+ , m_rowSpacing(0)
31+ , m_delegateCreationBegin(0)
32+ , m_delegateCreationEnd(0)
33+ , m_delegateCreationBeginValid(false)
34+ , m_delegateCreationEndValid(false)
35+ , m_needsRelayout(false)
36+ , m_delegateValidated(false)
37+ , m_implicitHeightDirty(false)
38+ , m_firstVisibleIndex(-1)
39+ , m_numberOfModulesPerRow(-1)
40+{
41+ connect(this, SIGNAL(widthChanged()), this, SLOT(relayout()));
42+ connect(this, SIGNAL(heightChanged()), this, SLOT(onHeightChanged()));
43+}
44+
45+QAbstractItemModel *OrganicGrid::model() const
46+{
47+ return m_delegateModel ? m_delegateModel->model().value<QAbstractItemModel *>() : nullptr;
48+}
49+
50+void OrganicGrid::setModel(QAbstractItemModel *model)
51+{
52+ if (model != this->model()) {
53+ if (!m_delegateModel) {
54+ createDelegateModel();
55+ } else {
56+#if (QT_VERSION < QT_VERSION_CHECK(5, 1, 0))
57+ disconnect(m_delegateModel, SIGNAL(modelUpdated(QQuickChangeSet,bool)), this, SLOT(onModelUpdated(QQuickChangeSet,bool)));
58+ }
59+ m_delegateModel->setModel(QVariant::fromValue<QAbstractItemModel *>(model));
60+ connect(m_delegateModel, SIGNAL(modelUpdated(QQuickChangeSet,bool)), this, SLOT(onModelUpdated(QQuickChangeSet,bool)));
61+#else
62+ disconnect(m_delegateModel, SIGNAL(modelUpdated(QQmlChangeSet,bool)), this, SLOT(onModelUpdated(QQmlChangeSet,bool)));
63+ }
64+ m_delegateModel->setModel(QVariant::fromValue<QAbstractItemModel *>(model));
65+ connect(m_delegateModel, SIGNAL(modelUpdated(QQmlChangeSet,bool)), this, SLOT(onModelUpdated(QQmlChangeSet,bool)));
66+#endif
67+
68+ cleanupExistingItems();
69+
70+ Q_EMIT modelChanged();
71+ polish();
72+ }
73+}
74+
75+QQmlComponent *OrganicGrid::delegate() const
76+{
77+ return m_delegateModel ? m_delegateModel->delegate() : nullptr;
78+}
79+
80+void OrganicGrid::setDelegate(QQmlComponent *delegate)
81+{
82+ if (delegate != this->delegate()) {
83+ if (!m_delegateModel) {
84+ createDelegateModel();
85+ }
86+
87+ cleanupExistingItems();
88+
89+ m_delegateModel->setDelegate(delegate);
90+
91+ Q_EMIT delegateChanged();
92+ m_delegateValidated = false;
93+ polish();
94+ }
95+}
96+
97+qreal OrganicGrid::columnSpacing() const
98+{
99+ return m_columnSpacing;
100+}
101+
102+void OrganicGrid::setColumnSpacing(qreal columnSpacing)
103+{
104+ if (columnSpacing != m_columnSpacing) {
105+ m_columnSpacing = columnSpacing;
106+ Q_EMIT columnSpacingChanged();
107+
108+ if (isComponentComplete()) {
109+ relayout();
110+ }
111+ }
112+}
113+
114+qreal OrganicGrid::rowSpacing() const
115+{
116+ return m_rowSpacing;
117+}
118+
119+void OrganicGrid::setRowSpacing(qreal rowSpacing)
120+{
121+ if (rowSpacing != m_rowSpacing) {
122+ m_rowSpacing = rowSpacing;
123+ Q_EMIT rowSpacingChanged();
124+
125+ if (isComponentComplete()) {
126+ relayout();
127+ }
128+ }
129+}
130+
131+QSizeF OrganicGrid::smallDelegateSize() const
132+{
133+ return m_smallDelegateSize;
134+}
135+
136+void OrganicGrid::setSmallDelegateSize(const QSizeF &size)
137+{
138+ if (m_smallDelegateSize != size) {
139+ m_smallDelegateSize = size;
140+ Q_EMIT smallDelegateSizeChanged();
141+
142+ if (isComponentComplete()) {
143+ relayout();
144+ }
145+ }
146+}
147+
148+QSizeF OrganicGrid::bigDelegateSize() const
149+{
150+ return m_bigDelegateSize;
151+}
152+
153+void OrganicGrid::setBigDelegateSize(const QSizeF &size)
154+{
155+ if (m_bigDelegateSize != size) {
156+ m_bigDelegateSize = size;
157+ Q_EMIT bigDelegateSizeChanged();
158+
159+ if (isComponentComplete()) {
160+ relayout();
161+ }
162+ }
163+}
164+
165+qreal OrganicGrid::delegateCreationBegin() const
166+{
167+ return m_delegateCreationBegin;
168+}
169+
170+void OrganicGrid::setDelegateCreationBegin(qreal begin)
171+{
172+ m_delegateCreationBeginValid = true;
173+ if (m_delegateCreationBegin == begin)
174+ return;
175+ m_delegateCreationBegin = begin;
176+ if (isComponentComplete()) {
177+ polish();
178+ }
179+ emit delegateCreationBeginChanged();
180+}
181+
182+void OrganicGrid::resetDelegateCreationBegin()
183+{
184+ m_delegateCreationBeginValid = false;
185+ if (m_delegateCreationBegin == 0)
186+ return;
187+ m_delegateCreationBegin = 0;
188+ if (isComponentComplete()) {
189+ polish();
190+ }
191+ emit delegateCreationBeginChanged();
192+}
193+
194+qreal OrganicGrid::delegateCreationEnd() const
195+{
196+ return m_delegateCreationEnd;
197+}
198+
199+void OrganicGrid::setDelegateCreationEnd(qreal end)
200+{
201+ m_delegateCreationEndValid = true;
202+ if (m_delegateCreationEnd == end)
203+ return;
204+ m_delegateCreationEnd = end;
205+ if (isComponentComplete()) {
206+ polish();
207+ }
208+ emit delegateCreationEndChanged();
209+}
210+
211+void OrganicGrid::resetDelegateCreationEnd()
212+{
213+ m_delegateCreationEndValid = false;
214+ if (m_delegateCreationEnd == 0)
215+ return;
216+ m_delegateCreationEnd = 0;
217+ if (isComponentComplete()) {
218+ polish();
219+ }
220+ emit delegateCreationEndChanged();
221+}
222+
223+void OrganicGrid::createDelegateModel()
224+{
225+#if (QT_VERSION < QT_VERSION_CHECK(5, 1, 0))
226+ m_delegateModel = new QQuickVisualDataModel(qmlContext(this), this);
227+ connect(m_delegateModel, SIGNAL(createdItem(int,QQuickItem*)), this, SLOT(itemCreated(int,QQuickItem*)));
228+#else
229+ m_delegateModel = new QQmlDelegateModel(qmlContext(this), this);
230+ connect(m_delegateModel, SIGNAL(createdItem(int,QObject*)), this, SLOT(itemCreated(int,QObject*)));
231+#endif
232+ if (isComponentComplete())
233+ m_delegateModel->componentComplete();
234+}
235+
236+void OrganicGrid::refill()
237+{
238+ if (!isComponentComplete()) {
239+ return;
240+ }
241+
242+ const bool delegateRangesValid = m_delegateCreationBeginValid && m_delegateCreationEndValid;
243+ const qreal from = delegateRangesValid ? m_delegateCreationBegin : 0;
244+ const qreal to = delegateRangesValid ? m_delegateCreationEnd : from + height();
245+ const qreal buffer = (to - from) * bufferRatio;
246+ const qreal bufferFrom = from - buffer;
247+ const qreal bufferTo = to + buffer;
248+
249+ bool added = addVisibleItems(from, to, false);
250+ bool removed = removeNonVisibleItems(bufferFrom, bufferTo);
251+ added |= addVisibleItems(bufferFrom, bufferTo, true);
252+
253+ if (added || removed) {
254+ m_implicitHeightDirty = true;
255+ polish();
256+ }
257+}
258+
259+bool OrganicGrid::addVisibleItems(qreal fillFromY, qreal fillToY, bool asynchronous)
260+{
261+ if (!delegate())
262+ return false;
263+
264+ if (m_delegateModel->count() == 0)
265+ return false;
266+
267+ int modelIndex;
268+ qreal yPos;
269+ findBottomModelIndexToAdd(&modelIndex, &yPos);
270+ bool changed = false;
271+ while (modelIndex < m_delegateModel->count() && yPos <= fillToY) {
272+ if (!createItem(modelIndex, asynchronous))
273+ break;
274+
275+ changed = true;
276+ findBottomModelIndexToAdd(&modelIndex, &yPos);
277+ }
278+
279+ findTopModelIndexToAdd(&modelIndex, &yPos);
280+ while (modelIndex >= 0 && yPos > fillFromY) {
281+ if (!createItem(modelIndex, asynchronous))
282+ break;
283+
284+ changed = true;
285+ findTopModelIndexToAdd(&modelIndex, &yPos);
286+ }
287+
288+ return changed;
289+}
290+
291+QQuickItem *OrganicGrid::createItem(int modelIndex, bool asynchronous)
292+{
293+ if (asynchronous && m_asyncRequestedIndex != -1)
294+ return nullptr;
295+
296+ m_asyncRequestedIndex = -1;
297+#if (QT_VERSION < QT_VERSION_CHECK(5, 1, 0))
298+ QQuickItem *item = m_delegateModel->item(modelIndex, asynchronous);
299+#else
300+ QObject* object = m_delegateModel->object(modelIndex, asynchronous);
301+ QQuickItem *item = qmlobject_cast<QQuickItem*>(object);
302+#endif
303+ if (!item) {
304+#if (QT_VERSION < QT_VERSION_CHECK(5, 1, 0))
305+ m_asyncRequestedIndex = modelIndex;
306+#else
307+ if (object) {
308+ m_delegateModel->release(object);
309+ if (!m_delegateValidated) {
310+ m_delegateValidated = true;
311+ QObject* delegateObj = delegate();
312+ qmlInfo(delegateObj ? delegateObj : this) << "Delegate must be of Item type";
313+ }
314+ } else {
315+ m_asyncRequestedIndex = modelIndex;
316+ }
317+#endif
318+ return nullptr;
319+ } else {
320+ positionItem(modelIndex, item);
321+ return item;
322+ }
323+}
324+
325+void OrganicGrid::releaseItem(QQuickItem *item)
326+{
327+#if (QT_VERSION < QT_VERSION_CHECK(5, 1, 0))
328+ QQuickVisualModel::ReleaseFlags flags = m_delegateModel->release(item);
329+ if (flags & QQuickVisualModel::Destroyed) {
330+#else
331+ QQmlDelegateModel::ReleaseFlags flags = m_delegateModel->release(item);
332+ if (flags & QQmlDelegateModel::Destroyed) {
333+#endif
334+ item->setParentItem(nullptr);
335+ }
336+}
337+
338+void OrganicGrid::setImplicitHeightDirty()
339+{
340+ m_implicitHeightDirty = true;
341+}
342+
343+#if (QT_VERSION < QT_VERSION_CHECK(5, 1, 0))
344+void OrganicGrid::itemCreated(int modelIndex, QQuickItem *item)
345+{
346+#else
347+void OrganicGrid::itemCreated(int modelIndex, QObject *object)
348+{
349+ QQuickItem *item = qmlobject_cast<QQuickItem*>(object);
350+ if (!item) {
351+ qWarning() << "OrganicGrid::itemCreated got a non item for index" << modelIndex;
352+ return;
353+ }
354+#endif
355+ item->setParentItem(this);
356+
357+ // We only need to call createItem if we are here because of an asynchronous generation
358+ // otherwise we are in this slot because createItem is creating the item sync
359+ // and thus we don't need to call createItem again, nor need to set m_implicitHeightDirty
360+ // and call polish because the sync createItem was called from addVisibleItems that
361+ // is called from refill that will already do those if an item was added
362+ if (modelIndex == m_asyncRequestedIndex) {
363+ createItem(modelIndex, false);
364+ m_implicitHeightDirty = true;
365+ polish();
366+ }
367+}
368+
369+#if (QT_VERSION < QT_VERSION_CHECK(5, 1, 0))
370+void OrganicGrid::onModelUpdated(const QQuickChangeSet &changeSet, bool reset)
371+#else
372+void OrganicGrid::onModelUpdated(const QQmlChangeSet &changeSet, bool reset)
373+#endif
374+{
375+ if (reset) {
376+ cleanupExistingItems();
377+ } else {
378+ processModelRemoves(changeSet.removes());
379+ }
380+ polish();
381+}
382+
383+
384+void OrganicGrid::relayout()
385+{
386+ m_needsRelayout = true;
387+ polish();
388+}
389+
390+void OrganicGrid::onHeightChanged()
391+{
392+ polish();
393+}
394+
395+void OrganicGrid::updatePolish()
396+{
397+ if (!model())
398+ return;
399+
400+ if (m_needsRelayout) {
401+ doRelayout();
402+ m_needsRelayout = false;
403+ m_implicitHeightDirty = true;
404+ }
405+
406+ refill();
407+
408+ const bool delegateRangesValid = m_delegateCreationBeginValid && m_delegateCreationEndValid;
409+ const qreal from = delegateRangesValid ? m_delegateCreationBegin : 0;
410+ const qreal to = delegateRangesValid ? m_delegateCreationEnd : from + height();
411+ updateItemCulling(from, to);
412+
413+ if (m_implicitHeightDirty) {
414+ calculateImplicitHeight();
415+ m_implicitHeightDirty = false;
416+ }
417+}
418+
419+void OrganicGrid::componentComplete()
420+{
421+ if (m_delegateModel)
422+ m_delegateModel->componentComplete();
423+
424+ QQuickItem::componentComplete();
425+
426+ m_needsRelayout = true;
427+
428+ polish();
429+}
430+
431+QPointF OrganicGrid::positionForIndex(int modelIndex)
432+{
433+ const qreal moduleHeight = m_smallDelegateSize.height() + rowSpacing() + m_bigDelegateSize.height();
434+ const qreal moduleWidth = m_smallDelegateSize.width() * 2 + columnSpacing() * 2 + m_bigDelegateSize.width();
435+ const int rowIndex = floor(modelIndex / (m_numberOfModulesPerRow * 6));
436+ const int columnIndex = floor((modelIndex - rowIndex * m_numberOfModulesPerRow * 6) / 6);
437+
438+ qreal yPos = (moduleHeight + rowSpacing()) * rowIndex;
439+ const int moduleIndex = modelIndex % 6;
440+ if (moduleIndex == 2) {
441+ yPos += m_smallDelegateSize.height() + rowSpacing();
442+ } else if (moduleIndex == 3 || moduleIndex == 5) {
443+ yPos += m_bigDelegateSize.height() + rowSpacing();
444+ }
445+
446+ qreal xPos = (moduleWidth + columnSpacing()) * columnIndex;
447+ if (moduleIndex == 1) {
448+ xPos += m_smallDelegateSize.width() + columnSpacing();
449+ } else if (moduleIndex == 3) {
450+ xPos += m_bigDelegateSize.width() + columnSpacing();
451+ } else if (moduleIndex == 4) {
452+ xPos += (m_smallDelegateSize.width() + columnSpacing()) * 2;
453+ } else if (moduleIndex == 5) {
454+ xPos += m_bigDelegateSize.width() + m_smallDelegateSize.width() + columnSpacing() * 2;
455+ }
456+
457+ return QPointF(xPos, yPos);
458+}
459+
460+void OrganicGrid::findBottomModelIndexToAdd(int *modelIndex, qreal *yPos)
461+{
462+ if (m_visibleItems.isEmpty()) {
463+ *modelIndex = 0;
464+ *yPos = 0;
465+ } else {
466+ *modelIndex = m_firstVisibleIndex + m_visibleItems.count();
467+ *yPos = positionForIndex(*modelIndex).y();
468+ }
469+}
470+
471+void OrganicGrid::findTopModelIndexToAdd(int *modelIndex, qreal *yPos)
472+{
473+ if (m_visibleItems.isEmpty()) {
474+ *modelIndex = 0;
475+ *yPos = 0;
476+ } else {
477+ *modelIndex = m_firstVisibleIndex - 1;
478+ *yPos = positionForIndex(*modelIndex).y();
479+ }
480+}
481+
482+void OrganicGrid::positionItem(int modelIndex, QQuickItem *item)
483+{
484+ // modelIndex has to be either m_firstVisibleIndex - 1 or m_firstVisibleIndex + m_visibleItems.count() or the first
485+ if (modelIndex == m_firstVisibleIndex + m_visibleItems.count()) {
486+ m_visibleItems << item;
487+ } else if (modelIndex == m_firstVisibleIndex - 1) {
488+ m_firstVisibleIndex = modelIndex;
489+ m_visibleItems.prepend(item);
490+ } else if (modelIndex == 0) {
491+ m_firstVisibleIndex = 0;
492+ m_visibleItems << item;
493+ } else {
494+ qWarning() << "OrganicGrid::positionItem - Got unexpected modelIndex"
495+ << modelIndex << m_firstVisibleIndex << m_visibleItems.count();
496+ return;
497+ }
498+
499+ const QPointF pos = positionForIndex(modelIndex);
500+ item->setPosition(pos);
501+
502+ // TODO Do we want warnings here in case the sizes are not the one we want like we have in the journals?
503+ const int moduleIndex = modelIndex % 6;
504+ if (moduleIndex == 0 || moduleIndex == 1 || moduleIndex == 3 || moduleIndex == 5) {
505+ item->setSize(m_smallDelegateSize);
506+ } else {
507+ item->setSize(m_bigDelegateSize);
508+ }
509+}
510+
511+bool OrganicGrid::removeNonVisibleItems(qreal /*bufferFromY*/, qreal /*bufferToY*/)
512+{
513+ // TODO
514+ return false;
515+}
516+
517+void OrganicGrid::cleanupExistingItems()
518+{
519+ Q_FOREACH(QQuickItem *item, m_visibleItems)
520+ releaseItem(item);
521+ m_visibleItems.clear();
522+ m_firstVisibleIndex = -1;
523+}
524+
525+void OrganicGrid::doRelayout()
526+{
527+ const qreal moduleWidth = m_smallDelegateSize.width() * 2 + columnSpacing() * 2 + m_bigDelegateSize.width();
528+ m_numberOfModulesPerRow = floor((width() + columnSpacing()) / (moduleWidth + columnSpacing()));
529+ m_numberOfModulesPerRow = qMax(1, m_numberOfModulesPerRow);
530+
531+ int i = m_firstVisibleIndex;
532+ const QList<QQuickItem*> allItems = m_visibleItems;
533+ m_visibleItems.clear();
534+ Q_FOREACH(QQuickItem *item, allItems) {
535+ positionItem(i, item);
536+ ++i;
537+ }
538+}
539+
540+void OrganicGrid::updateItemCulling(qreal visibleFromY, qreal visibleToY)
541+{
542+ int i = m_firstVisibleIndex;
543+ Q_FOREACH(QQuickItem *item, m_visibleItems) {
544+ QQuickItemPrivate::get(item)->setCulled(item->y() + item->height() <= visibleFromY || item->y() >= visibleToY);
545+ ++i;
546+ }
547+}
548+
549+void OrganicGrid::calculateImplicitHeight()
550+{
551+ const qreal moduleHeight = m_smallDelegateSize.height() + rowSpacing() + m_bigDelegateSize.height();
552+ const int itemCount = !m_delegateModel ? 0 : m_delegateModel->count();
553+ const int fullRows = floor(itemCount / (m_numberOfModulesPerRow * 6));
554+ const qreal fullRowsHeight = fullRows == 0 ? 0 : fullRows * moduleHeight + rowSpacing() * (fullRows - 1);
555+
556+ const int remainingItems = itemCount - fullRows * m_numberOfModulesPerRow * 6;
557+ if (remainingItems == 0) {
558+ setImplicitHeight(fullRowsHeight);
559+ } else if (remainingItems <= 2) {
560+ setImplicitHeight(m_smallDelegateSize.height() + rowSpacing());
561+ } else {
562+ setImplicitHeight(m_bigDelegateSize.height() + rowSpacing());
563+ }
564+}
565+
566+#if (QT_VERSION < QT_VERSION_CHECK(5, 1, 0))
567+void OrganicGrid::processModelRemoves(const QVector<QQuickChangeSet::Remove> & /*removes*/)
568+#else
569+void OrganicGrid::processModelRemoves(const QVector<QQmlChangeSet::Remove> & /*removes*/)
570+#endif
571+{
572+ // TODO
573+}
574+