Changeset View
Changeset View
Standalone View
Standalone View
components/containmentlayoutmanager/gridlayoutmanager.cpp
- This file was added.
1 | /* | ||||
---|---|---|---|---|---|
2 | * Copyright 2019 by Marco Martin <mart@kde.org> | ||||
3 | * | ||||
4 | * This program is free software; you can redistribute it and/or modify | ||||
5 | * it under the terms of the GNU Library General Public License as | ||||
6 | * published by the Free Software Foundation; either version 2, or | ||||
7 | * (at your option) any later version. | ||||
8 | * | ||||
9 | * This program is distributed in the hope that it will be useful, | ||||
10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||
11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||||
12 | * GNU Library General Public License for more details | ||||
13 | * | ||||
14 | * You should have received a copy of the GNU Library General Public | ||||
15 | * License along with this program; if not, write to the | ||||
16 | * Free Software Foundation, Inc., | ||||
17 | * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | ||||
18 | * | ||||
19 | */ | ||||
20 | | ||||
21 | #include "gridlayoutmanager.h" | ||||
22 | #include "appletslayout.h" | ||||
23 | #include <cmath> | ||||
24 | | ||||
25 | GridLayoutManager::GridLayoutManager(AppletsLayout *layout) | ||||
26 | : AbstractLayoutManager(layout) | ||||
27 | { | ||||
28 | } | ||||
29 | | ||||
30 | GridLayoutManager::~GridLayoutManager() | ||||
31 | { | ||||
32 | } | ||||
33 | | ||||
34 | QString GridLayoutManager::serializeLayout() const | ||||
35 | { | ||||
36 | QString result; | ||||
37 | | ||||
38 | for (auto *item : layout()->childItems()) { | ||||
39 | ItemContainer *itemCont = qobject_cast<ItemContainer*>(item); | ||||
40 | if (itemCont && itemCont != layout()->placeHolder()) { | ||||
41 | result += itemCont->key() + QLatin1Char(':') | ||||
42 | + QString::number(itemCont->x()) + QLatin1Char(',') | ||||
43 | + QString::number(itemCont->y()) + QLatin1Char(',') | ||||
44 | + QString::number(itemCont->width()) + QLatin1Char(',') | ||||
45 | + QString::number(itemCont->height()) + QLatin1Char(',') | ||||
46 | + QString::number(itemCont->rotation()) + QLatin1Char(';'); | ||||
47 | } | ||||
48 | } | ||||
49 | | ||||
50 | return result; | ||||
51 | } | ||||
52 | | ||||
53 | void GridLayoutManager::parseLayout(const QString &savedLayout) | ||||
54 | { | ||||
55 | m_parsedConfig.clear(); | ||||
56 | QStringList itemsConfigs = savedLayout.split(QLatin1Char(';')); | ||||
57 | | ||||
58 | for (const auto &itemString : itemsConfigs) { | ||||
59 | QStringList itemConfig = itemString.split(QLatin1Char(':')); | ||||
60 | if (itemConfig.count() != 2) { | ||||
61 | continue; | ||||
62 | } | ||||
63 | | ||||
64 | QString id = itemConfig[0]; | ||||
65 | QStringList itemGeom = itemConfig[1].split(QLatin1Char(',')); | ||||
66 | if (itemGeom.count() != 5) { | ||||
67 | continue; | ||||
68 | } | ||||
69 | | ||||
70 | m_parsedConfig[id] = {itemGeom[0].toInt(), itemGeom[1].toInt(), itemGeom[2].toInt(), itemGeom[3].toInt(), itemGeom[4].toInt()}; | ||||
71 | } | ||||
72 | } | ||||
73 | | ||||
74 | bool GridLayoutManager::itemIsManaged(ItemContainer *item) | ||||
75 | { | ||||
76 | return m_pointsForItem.contains(item); | ||||
77 | } | ||||
78 | | ||||
79 | inline void maintainItemEdgeAlignment(QQuickItem *item, const QRectF &newRect, const QRectF &oldRect) | ||||
80 | { | ||||
81 | const qreal leftDist = item->x() - oldRect.x(); | ||||
82 | const qreal hCenterDist = item->x() + item->width()/2 - oldRect.center().x(); | ||||
83 | const qreal rightDist = oldRect.right() - item->x() - item->width(); | ||||
84 | | ||||
85 | qreal hMin = qMin(qMin(qAbs(leftDist), qAbs(hCenterDist)), qAbs(rightDist)); | ||||
86 | if (qFuzzyCompare(hMin, qAbs(leftDist))) { | ||||
87 | // Right alignment, do nothing | ||||
88 | } else if (qFuzzyCompare(hMin, qAbs(hCenterDist))) { | ||||
89 | item->setX(newRect.center().x() - item->width()/2 + hCenterDist); | ||||
90 | } else if (qFuzzyCompare(hMin, qAbs(rightDist))) { | ||||
91 | item->setX(newRect.right() - item->width() - rightDist ); | ||||
92 | } | ||||
93 | | ||||
94 | const qreal topDist = item->y() - oldRect.y(); | ||||
95 | const qreal vCenterDist = item->y() + item->height()/2 - oldRect.center().y(); | ||||
96 | const qreal bottomDist = oldRect.bottom() - item->y() - item->height(); | ||||
97 | | ||||
98 | qreal vMin = qMin(qMin(qAbs(topDist), qAbs(vCenterDist)), qAbs(bottomDist)); | ||||
99 | | ||||
100 | if (qFuzzyCompare(vMin, qAbs(topDist))) { | ||||
101 | // Top alignment, do nothing | ||||
102 | } else if (qFuzzyCompare(vMin, qAbs(vCenterDist))) { | ||||
103 | item->setY(newRect.center().y() - item->height()/2 + vCenterDist); | ||||
104 | } else if (qFuzzyCompare(vMin, qAbs(bottomDist))) { | ||||
105 | item->setY(newRect.bottom() - item->height() - bottomDist ); | ||||
106 | } | ||||
107 | } | ||||
108 | | ||||
109 | void GridLayoutManager::layoutGeometryChanged(const QRectF &newGeometry, const QRectF &oldGeometry) | ||||
110 | { | ||||
111 | for (auto *item : layout()->childItems()) { | ||||
112 | // Stash the old config | ||||
113 | //m_parsedConfig[item->key()] = {item->x(), item->y(), item->width(), item->height(), item->rotation()}; | ||||
114 | // Move the item to maintain the distance with the anchors point | ||||
115 | maintainItemEdgeAlignment(item, newGeometry, oldGeometry); | ||||
116 | } | ||||
117 | } | ||||
118 | | ||||
119 | void GridLayoutManager::resetLayout() | ||||
120 | { | ||||
121 | m_grid.clear(); | ||||
122 | m_pointsForItem.clear(); | ||||
123 | for (auto *item : layout()->childItems()) { | ||||
124 | ItemContainer *itemCont = qobject_cast<ItemContainer*>(item); | ||||
125 | if (itemCont && itemCont != layout()->placeHolder()) { | ||||
126 | // NOTE: do not use positionItemAndAssign here, because we do not want to emit layoutNeedsSaving, to not save after resize | ||||
127 | positionItem(itemCont); | ||||
128 | assignSpaceImpl(itemCont); | ||||
129 | } | ||||
130 | } | ||||
131 | } | ||||
132 | | ||||
133 | void GridLayoutManager::resetLayoutFromConfig() | ||||
134 | { | ||||
135 | m_grid.clear(); | ||||
136 | m_pointsForItem.clear(); | ||||
137 | QList<ItemContainer *> missingItems; | ||||
138 | | ||||
139 | for (auto *item : layout()->childItems()) { | ||||
140 | ItemContainer *itemCont = qobject_cast<ItemContainer*>(item); | ||||
141 | if (itemCont && itemCont != layout()->placeHolder()) { | ||||
142 | if (!restoreItem(itemCont)) { | ||||
143 | missingItems << itemCont; | ||||
144 | } | ||||
145 | } | ||||
146 | } | ||||
147 | | ||||
148 | for (auto *item : missingItems) { | ||||
149 | positionItemAndAssign(item); | ||||
150 | } | ||||
151 | } | ||||
152 | | ||||
153 | bool GridLayoutManager::restoreItem(ItemContainer *item) | ||||
154 | { | ||||
155 | auto it = m_parsedConfig.find(item->key()); | ||||
156 | | ||||
157 | if (it != m_parsedConfig.end()) { | ||||
158 | item->setX(it.value().x); | ||||
159 | item->setY(it.value().y); | ||||
160 | item->setWidth(it.value().width); | ||||
161 | item->setHeight(it.value().height); | ||||
162 | item->setRotation(it.value().rotation); | ||||
163 | positionItemAndAssign(item); | ||||
164 | //m_parsedConfig.erase(); | ||||
165 | return true; | ||||
166 | } | ||||
167 | | ||||
168 | return false; | ||||
169 | } | ||||
170 | | ||||
171 | bool GridLayoutManager::isRectAvailable(const QRectF &rect) | ||||
172 | { | ||||
173 | //TODO: define directions in which it can grow | ||||
174 | if (rect.x() < 0 || rect.y() < 0 || rect.x() + rect.width() > layout()->width() || rect.y() + rect.height() > layout()->height()) { | ||||
175 | return false; | ||||
176 | } | ||||
177 | | ||||
178 | const QRect cellItemGeom = cellBasedGeometry(rect); | ||||
179 | | ||||
180 | for (int row = cellItemGeom.top(); row <= cellItemGeom.bottom(); ++row) { | ||||
181 | for (int column = cellItemGeom.left(); column <= cellItemGeom.right(); ++column) { | ||||
182 | if (!isCellAvailable(QPair<int, int>(row, column))) { | ||||
183 | return false; | ||||
184 | } | ||||
185 | } | ||||
186 | } | ||||
187 | return true; | ||||
188 | } | ||||
189 | | ||||
190 | bool GridLayoutManager::assignSpaceImpl(ItemContainer *item) | ||||
191 | { | ||||
192 | // Don't emit extra layoutneedssaving signals | ||||
193 | releaseSpaceImpl(item); | ||||
194 | if (!isRectAvailable(itemGeometry(item))) { | ||||
195 | qWarning()<<"Trying to take space not available"<<item; | ||||
196 | return false; | ||||
197 | } | ||||
198 | | ||||
199 | const QRect cellItemGeom = cellBasedGeometry(itemGeometry(item)); | ||||
200 | | ||||
201 | for (int row = cellItemGeom.top(); row <= cellItemGeom.bottom(); ++row) { | ||||
202 | for (int column = cellItemGeom.left(); column <= cellItemGeom.right(); ++column) { | ||||
203 | QPair<int, int> cell(row, column); | ||||
204 | m_grid.insert(cell, item); | ||||
205 | m_pointsForItem[item].insert(cell); | ||||
206 | } | ||||
207 | } | ||||
208 | | ||||
209 | // Reorder items tab order | ||||
210 | for (auto *i2 : layout()->childItems()) { | ||||
211 | ItemContainer *item2 = qobject_cast<ItemContainer*>(i2); | ||||
212 | if (item2 && item != item2 && item2 != layout()->placeHolder() | ||||
213 | && item->y() < item2->y() + item2->height() | ||||
214 | && item->x() <= item2->x()) { | ||||
215 | item->stackBefore(item2); | ||||
216 | break; | ||||
217 | } | ||||
218 | } | ||||
219 | | ||||
220 | return true; | ||||
221 | } | ||||
222 | | ||||
223 | void GridLayoutManager::releaseSpaceImpl(ItemContainer *item) | ||||
224 | { | ||||
225 | auto it = m_pointsForItem.find(item); | ||||
226 | | ||||
227 | if (it == m_pointsForItem.end()) { | ||||
228 | return; | ||||
229 | } | ||||
230 | | ||||
231 | for (const auto &point : it.value()) { | ||||
232 | m_grid.remove(point); | ||||
233 | } | ||||
234 | | ||||
235 | m_pointsForItem.erase(it); | ||||
236 | } | ||||
237 | | ||||
238 | int GridLayoutManager::rows() const | ||||
239 | { | ||||
240 | return layout()->height() / cellSize().height(); | ||||
241 | } | ||||
242 | | ||||
243 | int GridLayoutManager::columns() const | ||||
244 | { | ||||
245 | return layout()->width() / cellSize().width(); | ||||
246 | } | ||||
247 | | ||||
248 | QRect GridLayoutManager::cellBasedGeometry(const QRectF &geom) const | ||||
249 | { | ||||
250 | return QRect( | ||||
251 | round(qBound(0.0, geom.x(), layout()->width() - geom.width()) / cellSize().width()), | ||||
252 | round(qBound(0.0, geom.y(), layout()->height() - geom.height()) / cellSize().height()), | ||||
253 | round((qreal)geom.width() / cellSize().width()), | ||||
254 | round((qreal)geom.height() / cellSize().height()) | ||||
255 | ); | ||||
256 | } | ||||
257 | | ||||
258 | QRect GridLayoutManager::cellBasedBoundingGeometry(const QRectF &geom) const | ||||
259 | { | ||||
260 | return QRect( | ||||
261 | floor(qBound(0.0, geom.x(), layout()->width() - geom.width()) / cellSize().width()), | ||||
262 | floor(qBound(0.0, geom.y(), layout()->height() - geom.height()) / cellSize().height()), | ||||
263 | ceil((qreal)geom.width() / cellSize().width()), | ||||
264 | ceil((qreal)geom.height() / cellSize().height()) | ||||
265 | ); | ||||
266 | } | ||||
267 | | ||||
268 | bool GridLayoutManager::isOutOfBounds(const QPair<int, int> &cell) const | ||||
269 | { | ||||
270 | return cell.first < 0 | ||||
271 | || cell.second < 0 | ||||
272 | || cell.first >= rows() | ||||
273 | || cell.second >= columns(); | ||||
274 | } | ||||
275 | | ||||
276 | bool GridLayoutManager::isCellAvailable(const QPair<int, int> &cell) const | ||||
277 | { | ||||
278 | return !isOutOfBounds(cell) && !m_grid.contains(cell); | ||||
279 | } | ||||
280 | | ||||
281 | QRectF GridLayoutManager::itemGeometry(QQuickItem *item) const | ||||
282 | { | ||||
283 | return QRectF(item->x(), item->y(), item->width(), item->height()); | ||||
284 | } | ||||
285 | | ||||
286 | QPair<int, int> GridLayoutManager::nextCell(const QPair<int, int> &cell, AppletsLayout::PreferredLayoutDirection direction) const | ||||
287 | { | ||||
288 | QPair<int, int> nCell = cell; | ||||
289 | | ||||
290 | switch (direction) { | ||||
291 | case AppletsLayout::AppletsLayout::BottomToTop: | ||||
292 | --nCell.first; | ||||
293 | break; | ||||
294 | case AppletsLayout::AppletsLayout::TopToBottom: | ||||
295 | ++nCell.first; | ||||
296 | break; | ||||
297 | case AppletsLayout::AppletsLayout::RightToLeft: | ||||
298 | --nCell.second; | ||||
299 | break; | ||||
300 | case AppletsLayout::AppletsLayout::LeftToRight: | ||||
301 | default: | ||||
302 | ++nCell.second; | ||||
303 | break; | ||||
304 | } | ||||
305 | | ||||
306 | return nCell; | ||||
307 | } | ||||
308 | | ||||
309 | QPair<int, int> GridLayoutManager::nextAvailableCell(const QPair<int, int> &cell, AppletsLayout::PreferredLayoutDirection direction) const | ||||
310 | { | ||||
311 | QPair<int, int> nCell = cell; | ||||
312 | while (!isOutOfBounds(nCell)) { | ||||
313 | nCell = nextCell(nCell, direction); | ||||
314 | | ||||
315 | if (isOutOfBounds(nCell)) { | ||||
316 | switch (direction) { | ||||
317 | case AppletsLayout::AppletsLayout::BottomToTop: | ||||
318 | nCell.first = rows() - 1; | ||||
319 | --nCell.second; | ||||
320 | break; | ||||
321 | case AppletsLayout::AppletsLayout::TopToBottom: | ||||
322 | nCell.first = 0; | ||||
323 | ++nCell.second; | ||||
324 | break; | ||||
325 | case AppletsLayout::AppletsLayout::RightToLeft: | ||||
326 | --nCell.first; | ||||
327 | nCell.second = columns() - 1; | ||||
328 | break; | ||||
329 | case AppletsLayout::AppletsLayout::LeftToRight: | ||||
330 | default: | ||||
331 | ++nCell.first; | ||||
332 | nCell.second = 0; | ||||
333 | break; | ||||
334 | } | ||||
335 | } | ||||
336 | | ||||
337 | if (isCellAvailable(nCell)) { | ||||
338 | return nCell; | ||||
339 | } | ||||
340 | } | ||||
341 | | ||||
342 | return QPair<int, int>(-1, -1); | ||||
343 | } | ||||
344 | | ||||
345 | int GridLayoutManager::freeSpaceInDirection(const QPair<int, int> &cell, AppletsLayout::PreferredLayoutDirection direction) const | ||||
346 | { | ||||
347 | QPair<int, int> nCell = cell; | ||||
348 | | ||||
349 | int avail = 0; | ||||
350 | | ||||
351 | while (isCellAvailable(nCell)) { | ||||
352 | ++avail; | ||||
353 | nCell = nextCell(nCell, direction); | ||||
354 | } | ||||
355 | | ||||
356 | return avail; | ||||
357 | } | ||||
358 | | ||||
359 | QRectF GridLayoutManager::nextAvailableSpace(ItemContainer *item, const QSizeF &minimumSize, AppletsLayout::PreferredLayoutDirection direction) const | ||||
360 | { | ||||
361 | // The mionimum size in grid units | ||||
362 | const QSize minimumGridSize( | ||||
363 | ceil((qreal)minimumSize.width() / cellSize().width()), | ||||
364 | ceil((qreal)minimumSize.height() / cellSize().height()) | ||||
365 | ); | ||||
366 | | ||||
367 | QRect itemCellGeom = cellBasedGeometry(itemGeometry(item)); | ||||
368 | itemCellGeom.setWidth(qMax(itemCellGeom.width(), minimumGridSize.width())); | ||||
369 | itemCellGeom.setHeight(qMax(itemCellGeom.height(), minimumGridSize.height())); | ||||
370 | | ||||
371 | QSize partialSize; | ||||
372 | | ||||
373 | QPair<int, int> cell(itemCellGeom.y(), itemCellGeom.x()); | ||||
374 | | ||||
375 | while (!isOutOfBounds(cell)) { | ||||
376 | | ||||
377 | if (!isCellAvailable(cell)) { | ||||
378 | cell = nextAvailableCell(cell, direction); | ||||
379 | } | ||||
380 | | ||||
381 | if (direction == AppletsLayout::LeftToRight || direction == AppletsLayout::RightToLeft) { | ||||
382 | partialSize = QSize(INT_MAX, 0); | ||||
383 | | ||||
384 | int currentRow = cell.first; | ||||
385 | for (; currentRow < cell.first + itemCellGeom.height(); ++currentRow) { | ||||
386 | | ||||
387 | const int freeRow = freeSpaceInDirection(QPair<int, int>(currentRow, cell.second), direction); | ||||
388 | | ||||
389 | partialSize.setWidth(qMin(partialSize.width(), freeRow)); | ||||
390 | | ||||
391 | if (freeRow > 0) { | ||||
392 | partialSize.setHeight(partialSize.height() + 1); | ||||
393 | } else if (partialSize.height() < minimumGridSize.height()) { | ||||
394 | break; | ||||
395 | } | ||||
396 | | ||||
397 | if (partialSize.width() >= itemCellGeom.width() | ||||
398 | && partialSize.height() >= itemCellGeom.height()) { | ||||
399 | break; | ||||
400 | } else if (partialSize.width() < minimumGridSize.width()) { | ||||
401 | break; | ||||
402 | } | ||||
403 | } | ||||
404 | | ||||
405 | if (partialSize.width() >= minimumGridSize.width() | ||||
406 | && partialSize.height() >= minimumGridSize.height()) { | ||||
407 | | ||||
408 | const int width = qMin(itemCellGeom.width(), partialSize.width()) * cellSize().width(); | ||||
409 | const int height = qMin(itemCellGeom.height(), partialSize.height()) * cellSize().height(); | ||||
410 | | ||||
411 | if (direction == AppletsLayout::RightToLeft) { | ||||
412 | return QRectF(cell.second * cellSize().width() - width, | ||||
413 | cell.first * cellSize().height(), | ||||
414 | width, height); | ||||
415 | // AppletsLayout::LeftToRight | ||||
416 | } else { | ||||
417 | return QRectF(cell.second * cellSize().width(), | ||||
418 | cell.first * cellSize().height(), | ||||
419 | width, height); | ||||
420 | } | ||||
421 | } else { | ||||
422 | cell.first = currentRow + 1; | ||||
423 | } | ||||
424 | | ||||
425 | } else if (direction == AppletsLayout::TopToBottom || direction == AppletsLayout::BottomToTop) { | ||||
426 | partialSize = QSize(0, INT_MAX); | ||||
427 | | ||||
428 | int currentColumn = cell.second; | ||||
429 | for (; currentColumn < cell.second + itemCellGeom.width(); ++currentColumn) { | ||||
430 | | ||||
431 | const int freeColumn = freeSpaceInDirection(QPair<int, int>(cell.first, currentColumn), direction); | ||||
432 | | ||||
433 | partialSize.setHeight(qMin(partialSize.height(), freeColumn)); | ||||
434 | | ||||
435 | if (freeColumn > 0) { | ||||
436 | partialSize.setWidth(partialSize.width() + 1); | ||||
437 | } else if (partialSize.width() < minimumGridSize.width()) { | ||||
438 | break; | ||||
439 | } | ||||
440 | | ||||
441 | if (partialSize.width() >= itemCellGeom.width() | ||||
442 | && partialSize.height() >= itemCellGeom.height()) { | ||||
443 | break; | ||||
444 | } else if (partialSize.height() < minimumGridSize.height()) { | ||||
445 | break; | ||||
446 | } | ||||
447 | } | ||||
448 | | ||||
449 | if (partialSize.width() >= minimumGridSize.width() | ||||
450 | && partialSize.height() >= minimumGridSize.height()) { | ||||
451 | | ||||
452 | const int width = qMin(itemCellGeom.width(), partialSize.width()) * cellSize().width(); | ||||
453 | const int height = qMin(itemCellGeom.height(), partialSize.height()) * cellSize().height(); | ||||
454 | | ||||
455 | if (direction == AppletsLayout::BottomToTop) { | ||||
456 | return QRectF(cell.second * cellSize().width(), | ||||
457 | cell.first * cellSize().height() - height, | ||||
458 | width, height); | ||||
459 | // AppletsLayout::TopToBottom: | ||||
460 | } else { | ||||
461 | return QRectF(cell.second * cellSize().width(), | ||||
462 | cell.first * cellSize().height(), | ||||
463 | width, height); | ||||
464 | } | ||||
465 | } else { | ||||
466 | cell.second = currentColumn + 1; | ||||
467 | } | ||||
468 | } | ||||
469 | } | ||||
470 | | ||||
471 | //We didn't manage to find layout space, return invalid geometry | ||||
472 | return QRectF(); | ||||
473 | } | ||||
474 | | ||||
475 | | ||||
476 | #include "moc_gridlayoutmanager.cpp" |