Unity 8
verticaljournal.cpp
1 /*
2  * Copyright (C) 2013, 2016 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 /*
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"
29 
30 #include <math.h>
31 
32 #include <private/qquickitem_p.h>
33 
34 VerticalJournal::VerticalJournal()
35  : m_columnWidth(0)
36 {
37 }
38 
39 qreal VerticalJournal::columnWidth() const
40 {
41  return m_columnWidth;
42 }
43 
44 void VerticalJournal::setColumnWidth(qreal columnWidth)
45 {
46  if (columnWidth != m_columnWidth) {
47  m_columnWidth = columnWidth;
48  Q_EMIT columnWidthChanged();
49 
50  if (isComponentComplete()) {
51  Q_FOREACH(const auto &column, m_columnVisibleItems) {
52  Q_FOREACH(const ViewItem item, column) {
53  item.m_item->setWidth(columnWidth);
54  }
55  }
56  relayout();
57  }
58  }
59 }
60 
61 void VerticalJournal::findBottomModelIndexToAdd(int *modelIndex, qreal *yPos)
62 {
63  *modelIndex = 0;
64  *yPos = std::numeric_limits<qreal>::max();
65 
66  Q_FOREACH(const auto &column, m_columnVisibleItems) {
67  if (!column.isEmpty()) {
68  const ViewItem &item = column.last();
69  *yPos = qMin(*yPos, item.y() + item.height() + rowSpacing());
70  *modelIndex = qMax(*modelIndex, item.m_modelIndex + 1);
71  } else {
72  *yPos = 0;
73  }
74  }
75 }
76 
77 void VerticalJournal::findTopModelIndexToAdd(int *modelIndex, qreal *yPos)
78 {
79  *modelIndex = 0;
80  *yPos = std::numeric_limits<qreal>::lowest();
81  int columnToAddTo = -1;
82 
83  // Find the topmost free column
84  for (int i = 0; i < m_columnVisibleItems.count(); ++i) {
85  const auto &column = m_columnVisibleItems[i];
86  if (!column.isEmpty()) {
87  const ViewItem &item = column.first();
88  const auto itemTopPos = item.y() - rowSpacing();
89  if (itemTopPos > *yPos) {
90  *yPos = itemTopPos;
91  *modelIndex = item.m_modelIndex - 1;
92  columnToAddTo = i;
93  }
94  }
95  }
96 
97  if (*modelIndex > 0) {
98  Q_ASSERT(m_indexColumnMap.contains(*modelIndex));
99  while (*modelIndex > 0 && m_indexColumnMap[*modelIndex] != columnToAddTo) {
100  // We found out that we have to add to columnToAddTo
101  // and thought that we had to add *modelIndex, but history tells
102  // it is not correct, so find up from *modelIndex until we found the index
103  // that has to end up in columnToAddTo
104  *modelIndex = *modelIndex - 1;
105  Q_ASSERT(m_indexColumnMap.contains(*modelIndex));
106  }
107  }
108 }
109 
110 bool VerticalJournal::removeNonVisibleItems(qreal bufferFromY, qreal bufferToY)
111 {
112  bool changed = false;
113 
114  for (int i = 0; i < m_columnVisibleItems.count(); ++i) {
115  QList<ViewItem> &column = m_columnVisibleItems[i];
116  while (!column.isEmpty() && column.first().y() + column.first().height() < bufferFromY) {
117  releaseItem(column.takeFirst().m_item);
118  changed = true;
119  }
120 
121  while (!column.isEmpty() && column.last().y() > bufferToY) {
122  releaseItem(column.takeLast().m_item);
123  changed = true;
124  }
125  }
126 
127  return changed;
128 }
129 
130 void VerticalJournal::addItemToView(int modelIndex, QQuickItem *item)
131 {
132  if (item->width() != m_columnWidth) {
133  qWarning() << "Item" << modelIndex << "width is not the one that the columnWidth mandates, resetting it";
134  item->setWidth(m_columnWidth);
135  }
136 
137  // Check if we add it to the bottom of existing column items
138  const QList<ViewItem> &firstColumn = m_columnVisibleItems[0];
139  qreal columnToAddY = !firstColumn.isEmpty() ? firstColumn.last().y() + firstColumn.last().height() : -rowSpacing();
140  int columnToAddTo = 0;
141  for (int i = 1; i < m_columnVisibleItems.count(); ++i) {
142  const QList<ViewItem> &column = m_columnVisibleItems[i];
143  const qreal iY = !column.isEmpty() ? column.last().y() + column.last().height() : -rowSpacing();
144  if (iY < columnToAddY) {
145  columnToAddTo = i;
146  columnToAddY = iY;
147  }
148  }
149 
150  const QList<ViewItem> &columnToAdd = m_columnVisibleItems[columnToAddTo];
151  if (columnToAdd.isEmpty() || columnToAdd.last().m_modelIndex < modelIndex) {
152  item->setX(columnToAddTo * (m_columnWidth + columnSpacing()));
153  item->setY(columnToAddY + rowSpacing());
154 
155  m_columnVisibleItems[columnToAddTo] << ViewItem(item, modelIndex);
156  m_indexColumnMap[modelIndex] = columnToAddTo;
157  } else {
158  Q_ASSERT(m_indexColumnMap.contains(modelIndex));
159  columnToAddTo = m_indexColumnMap[modelIndex];
160  columnToAddY = m_columnVisibleItems[columnToAddTo].first().y();
161 
162  item->setX(columnToAddTo * (m_columnWidth + columnSpacing()));
163  item->setY(columnToAddY - rowSpacing() - item->height());
164 
165  m_columnVisibleItems[columnToAddTo].prepend(ViewItem(item, modelIndex));
166  }
167 }
168 
169 void VerticalJournal::cleanupExistingItems()
170 {
171  // Cleanup the existing items
172  for (int i = 0; i < m_columnVisibleItems.count(); ++i) {
173  QList<ViewItem> &column = m_columnVisibleItems[i];
174  Q_FOREACH(const ViewItem item, column)
175  releaseItem(item.m_item);
176  column.clear();
177  }
178  m_indexColumnMap.clear();
179  setImplicitHeightDirty();
180 }
181 
182 void VerticalJournal::calculateImplicitHeight()
183 {
184  int lastModelIndex = -1;
185  qreal bottomMostY = 0;
186  Q_FOREACH(const auto &column, m_columnVisibleItems) {
187  if (!column.isEmpty()) {
188  const ViewItem &item = column.last();
189  lastModelIndex = qMax(lastModelIndex, item.m_modelIndex);
190  bottomMostY = qMax(bottomMostY, item.y() + item.height());
191  }
192  }
193  if (lastModelIndex >= 0) {
194  const double averageHeight = bottomMostY / (lastModelIndex + 1);
195  setImplicitHeight(bottomMostY + averageHeight * (model()->rowCount() - lastModelIndex - 1));
196  } else {
197  setImplicitHeight(0);
198  }
199 }
200 
201 void VerticalJournal::doRelayout()
202 {
203  QList<ViewItem> allItems;
204  Q_FOREACH(const auto &column, m_columnVisibleItems)
205  allItems << column;
206 
207  qSort(allItems);
208 
209  const int nColumns = qMax(1., floor((double)(width() + columnSpacing()) / (m_columnWidth + columnSpacing())));
210  m_columnVisibleItems.resize(nColumns);
211  m_indexColumnMap.clear();
212  for (int i = 0; i < nColumns; ++i)
213  m_columnVisibleItems[i].clear();
214 
215  // If the first of allItems doesn't contain index 0 we need to drop them
216  // all since we can't consistently relayout without the first item being there
217 
218  if (!allItems.isEmpty()) {
219  if (allItems.first().m_modelIndex == 0) {
220  Q_FOREACH(const ViewItem item, allItems)
221  addItemToView(item.m_modelIndex, item.m_item);
222  } else {
223  Q_FOREACH(const ViewItem item, allItems)
224  releaseItem(item.m_item);
225  }
226  }
227 }
228 
229 void VerticalJournal::updateItemCulling(qreal visibleFromY, qreal visibleToY)
230 {
231  Q_FOREACH(const auto &column, m_columnVisibleItems) {
232  Q_FOREACH(const ViewItem item, column) {
233  const bool cull = item.y() + item.height() <= visibleFromY || item.y() >= visibleToY;
234  QQuickItemPrivate::get(item.m_item)->setCulled(cull);
235  }
236  }
237 }
238 
239 void VerticalJournal::processModelRemoves(const QVector<QQmlChangeSet::Change> &removes)
240 {
241  Q_FOREACH(const QQmlChangeSet::Change remove, removes) {
242  for (int i = remove.count - 1; i >= 0; --i) {
243  const int indexToRemove = remove.index + i;
244  // Since we only support removing from the end, indexToRemove
245  // must refer to the last item of one of the columns or
246  // be bigger than them (because it's not in the viewport and
247  // thus we have not created a delegate for it)
248  bool found = false;
249  int lastCreatedIndex = INT_MIN;
250  for (int i = 0; !found && i < m_columnVisibleItems.count(); ++i) {
251  QList<ViewItem> &column = m_columnVisibleItems[i];
252  if (!column.isEmpty()) {
253  const int lastColumnIndex = column.last().m_modelIndex;
254  if (lastColumnIndex == indexToRemove) {
255  releaseItem(column.takeLast().m_item);
256  found = true;
257  }
258  lastCreatedIndex = qMax(lastCreatedIndex, lastColumnIndex);
259  }
260  }
261  if (!found) {
262  if (indexToRemove < lastCreatedIndex) {
263  qDebug() << "VerticalJournal only supports removal from the end of the model, resetting instead";
264  cleanupExistingItems();
265  break;
266  } else {
267  setImplicitHeightDirty();
268  }
269  }
270  }
271  }
272 }
273 
274 void VerticalJournal::itemGeometryChanged(QQuickItem * /*item*/, const QRectF &newGeometry, const QRectF &oldGeometry)
275 {
276  const qreal heightDiff = newGeometry.height() - oldGeometry.height();
277  if (heightDiff != 0) {
278  relayout();
279  }
280 }