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