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