File indexing completed on 2024-05-12 05:37:13
0001 /* 0002 SPDX-FileCopyrightText: 2019 Marco Martin <mart@kde.org> 0003 0004 SPDX-License-Identifier: LGPL-2.0-or-later 0005 */ 0006 0007 #include "gridlayoutmanager.h" 0008 #include "appletslayout.h" 0009 #include "containmentlayoutmanager_debug.h" 0010 0011 #include <cmath> 0012 0013 GridLayoutManager::GridLayoutManager(AppletsLayout *layout) 0014 : AbstractLayoutManager(layout) 0015 { 0016 } 0017 0018 GridLayoutManager::~GridLayoutManager() 0019 { 0020 } 0021 0022 QString GridLayoutManager::serializeLayout() const 0023 { 0024 QString result; 0025 0026 for (auto *item : layout()->childItems()) { 0027 ItemContainer *itemCont = qobject_cast<ItemContainer *>(item); 0028 if (itemCont && itemCont != layout()->placeHolder()) { 0029 result += itemCont->key() + QLatin1Char(':') + QString::number(itemCont->x()) + QLatin1Char(',') + QString::number(itemCont->y()) + QLatin1Char(',') 0030 + QString::number(itemCont->width()) + QLatin1Char(',') + QString::number(itemCont->height()) + QLatin1Char(',') 0031 + QString::number(itemCont->rotation()) + QLatin1Char(';'); 0032 } 0033 } 0034 0035 return result; 0036 } 0037 0038 void GridLayoutManager::parseLayout(const QString &savedLayout) 0039 { 0040 m_parsedConfig.clear(); 0041 const QStringList itemsConfigs = savedLayout.split(QLatin1Char(';')); 0042 0043 for (const auto &itemString : itemsConfigs) { 0044 QStringList itemConfig = itemString.split(QLatin1Char(':')); 0045 if (itemConfig.count() != 2) { 0046 continue; 0047 } 0048 0049 QString id = itemConfig[0]; 0050 QStringList itemGeom = itemConfig[1].split(QLatin1Char(',')); 0051 if (itemGeom.count() != 5) { 0052 continue; 0053 } 0054 0055 m_parsedConfig[id] = {itemGeom[0].toDouble(), itemGeom[1].toDouble(), itemGeom[2].toDouble(), itemGeom[3].toDouble(), itemGeom[4].toDouble()}; 0056 } 0057 } 0058 0059 bool GridLayoutManager::itemIsManaged(ItemContainer *item) 0060 { 0061 return m_pointsForItem.contains(item); 0062 } 0063 0064 inline void maintainItemEdgeAlignment(ItemContainer *item, const QRectF &newRect, const QRectF &oldRect) 0065 { 0066 const qreal leftDist = item->x() - oldRect.x(); 0067 const qreal hCenterDist = item->x() + item->width() / 2 - oldRect.center().x(); 0068 const qreal rightDist = oldRect.right() - item->x() - item->width(); 0069 0070 qreal hMin = qMin(qMin(qAbs(leftDist), qAbs(hCenterDist)), qAbs(rightDist)); 0071 if (qFuzzyCompare(hMin, qAbs(leftDist))) { 0072 // Right alignment, do nothing 0073 } else if (qFuzzyCompare(hMin, qAbs(hCenterDist))) { 0074 item->setX(newRect.center().x() - item->width() / 2 + hCenterDist); 0075 } else if (qFuzzyCompare(hMin, qAbs(rightDist))) { 0076 item->setX(newRect.right() - item->width() - rightDist); 0077 } 0078 0079 const qreal topDist = item->y() - oldRect.y(); 0080 const qreal vCenterDist = item->y() + item->height() / 2 - oldRect.center().y(); 0081 const qreal bottomDist = oldRect.bottom() - item->y() - item->height(); 0082 0083 qreal vMin = qMin(qMin(qAbs(topDist), qAbs(vCenterDist)), qAbs(bottomDist)); 0084 0085 if (qFuzzyCompare(vMin, qAbs(topDist))) { 0086 // Top alignment, do nothing 0087 } else if (qFuzzyCompare(vMin, qAbs(vCenterDist))) { 0088 item->setY(newRect.center().y() - item->height() / 2 + vCenterDist); 0089 } else if (qFuzzyCompare(vMin, qAbs(bottomDist))) { 0090 item->setY(newRect.bottom() - item->height() - bottomDist); 0091 } 0092 } 0093 0094 void GridLayoutManager::layoutGeometryChanged(const QRectF &newGeometry, const QRectF &oldGeometry) 0095 { 0096 Q_UNUSED(newGeometry); 0097 Q_UNUSED(oldGeometry); 0098 0099 m_grid.clear(); 0100 m_pointsForItem.clear(); 0101 for (auto *item : layout()->childItems()) { 0102 // Stash the old config 0103 // m_parsedConfig[item->key()] = {item->x(), item->y(), item->width(), item->height(), item->rotation()}; 0104 // Move the item to maintain the distance with the anchors point 0105 auto *itemCont = qobject_cast<ItemContainer *>(item); 0106 if (itemCont && itemCont != layout()->placeHolder()) { 0107 // NOTE: do not use positionItemAndAssign here, because we do not want to Q_EMIT layoutNeedsSaving, to not save after resize 0108 positionItem(itemCont); 0109 assignSpaceImpl(itemCont); 0110 } 0111 } 0112 } 0113 0114 void GridLayoutManager::resetLayout() 0115 { 0116 m_grid.clear(); 0117 m_pointsForItem.clear(); 0118 for (auto *item : layout()->childItems()) { 0119 ItemContainer *itemCont = qobject_cast<ItemContainer *>(item); 0120 if (itemCont && itemCont != layout()->placeHolder()) { 0121 // NOTE: do not use positionItemAndAssign here, because we do not want to Q_EMIT layoutNeedsSaving, to not save after resize 0122 positionItem(itemCont); 0123 assignSpaceImpl(itemCont); 0124 } 0125 } 0126 } 0127 0128 void GridLayoutManager::resetLayoutFromConfig(const QRectF &newGeom, const QRectF &oldGeom) 0129 { 0130 m_grid.clear(); 0131 m_pointsForItem.clear(); 0132 QList<ItemContainer *> missingItems; 0133 0134 for (auto *item : layout()->childItems()) { 0135 ItemContainer *itemCont = qobject_cast<ItemContainer *>(item); 0136 if (itemCont && itemCont != layout()->placeHolder()) { 0137 if (!restoreItem(itemCont)) { 0138 missingItems << itemCont; 0139 } 0140 } 0141 } 0142 0143 for (auto *item : std::as_const(missingItems)) { 0144 // NOTE: do not use positionItemAndAssign here, because we do not want to Q_EMIT layoutNeedsSaving, to not save after resize 0145 maintainItemEdgeAlignment(item, newGeom, oldGeom); 0146 positionItem(item); 0147 assignSpaceImpl(item); 0148 } 0149 0150 if (!missingItems.isEmpty()) { 0151 layout()->save(); 0152 } 0153 } 0154 0155 bool GridLayoutManager::restoreItem(ItemContainer *item) 0156 { 0157 auto it = m_parsedConfig.find(item->key()); 0158 0159 if (it != m_parsedConfig.end()) { 0160 // Actual restore 0161 item->setPosition(QPointF(it.value().x, it.value().y)); 0162 item->setSize(QSizeF(it.value().width, it.value().height)); 0163 item->setRotation(it.value().rotation); 0164 0165 // NOTE: do not use positionItemAndAssign here, because we do not want to Q_EMIT layoutNeedsSaving, to not save after resize 0166 // If size is empty the layout is not in a valid state and probably startup is not completed yet 0167 if (!layout()->size().isEmpty()) { 0168 releaseSpaceImpl(item); 0169 positionItem(item); 0170 assignSpaceImpl(item); 0171 } 0172 0173 return true; 0174 } 0175 0176 return false; 0177 } 0178 0179 bool GridLayoutManager::isRectAvailable(const QRectF &rect) 0180 { 0181 // TODO: define directions in which it can grow 0182 if (rect.x() < 0 || rect.y() < 0 || rect.x() + rect.width() > layout()->width() || rect.y() + rect.height() > layout()->height()) { 0183 return false; 0184 } 0185 0186 const QRect cellItemGeom = cellBasedGeometry(rect); 0187 0188 for (int row = cellItemGeom.top(); row <= cellItemGeom.bottom(); ++row) { 0189 for (int column = cellItemGeom.left(); column <= cellItemGeom.right(); ++column) { 0190 if (!isCellAvailable(QPair<int, int>(row, column))) { 0191 return false; 0192 } 0193 } 0194 } 0195 return true; 0196 } 0197 0198 bool GridLayoutManager::assignSpaceImpl(ItemContainer *item) 0199 { 0200 // Don't Q_EMIT extra layoutneedssaving signals 0201 releaseSpaceImpl(item); 0202 if (!isRectAvailable(itemGeometry(item))) { 0203 qCWarning(CONTAINMENTLAYOUTMANAGER_DEBUG) << "Trying to take space not available" << item; 0204 return false; 0205 } 0206 0207 const QRect cellItemGeom = cellBasedGeometry(itemGeometry(item)); 0208 0209 for (int row = cellItemGeom.top(); row <= cellItemGeom.bottom(); ++row) { 0210 for (int column = cellItemGeom.left(); column <= cellItemGeom.right(); ++column) { 0211 QPair<int, int> cell(row, column); 0212 m_grid.insert(cell, item); 0213 m_pointsForItem[item].insert(cell); 0214 } 0215 } 0216 0217 // Reorder items tab order 0218 for (auto *i2 : layout()->childItems()) { 0219 ItemContainer *item2 = qobject_cast<ItemContainer *>(i2); 0220 if (item2 && item2->parentItem() == item->parentItem() && item != item2 && item2 != layout()->placeHolder() && item->y() < item2->y() + item2->height() 0221 && item->x() <= item2->x()) { 0222 item->stackBefore(item2); 0223 break; 0224 } 0225 } 0226 0227 if (item->layoutAttached()) { 0228 connect(item, &ItemContainer::sizeHintsChanged, this, [this, item]() { 0229 adjustToItemSizeHints(item); 0230 }); 0231 } 0232 0233 return true; 0234 } 0235 0236 void GridLayoutManager::releaseSpaceImpl(ItemContainer *item) 0237 { 0238 auto it = m_pointsForItem.find(item); 0239 0240 if (it == m_pointsForItem.end()) { 0241 return; 0242 } 0243 0244 for (const auto &point : it.value()) { 0245 m_grid.remove(point); 0246 } 0247 0248 m_pointsForItem.erase(it); 0249 0250 disconnect(item, &ItemContainer::sizeHintsChanged, this, nullptr); 0251 } 0252 0253 int GridLayoutManager::rows() const 0254 { 0255 return layout()->height() / cellSize().height(); 0256 } 0257 0258 int GridLayoutManager::columns() const 0259 { 0260 return layout()->width() / cellSize().width(); 0261 } 0262 0263 void GridLayoutManager::adjustToItemSizeHints(ItemContainer *item) 0264 { 0265 if (!item->layoutAttached() || item->editMode()) { 0266 return; 0267 } 0268 0269 bool changed = false; 0270 0271 // Minimum 0272 const qreal newMinimumHeight = item->layoutAttached()->property("minimumHeight").toReal(); 0273 const qreal newMinimumWidth = item->layoutAttached()->property("minimumWidth").toReal(); 0274 0275 if (newMinimumHeight > item->height()) { 0276 item->setHeight(newMinimumHeight); 0277 changed = true; 0278 } 0279 if (newMinimumWidth > item->width()) { 0280 item->setWidth(newMinimumWidth); 0281 changed = true; 0282 } 0283 0284 // Preferred 0285 const qreal newPreferredHeight = item->layoutAttached()->property("preferredHeight").toReal(); 0286 const qreal newPreferredWidth = item->layoutAttached()->property("preferredWidth").toReal(); 0287 0288 if (newPreferredHeight > item->height()) { 0289 item->setHeight(layout()->cellHeight() * ceil(newPreferredHeight / layout()->cellHeight())); 0290 changed = true; 0291 } 0292 if (newPreferredWidth > item->width()) { 0293 item->setWidth(layout()->cellWidth() * ceil(newPreferredWidth / layout()->cellWidth())); 0294 changed = true; 0295 } 0296 0297 /*// Maximum : IGNORE? 0298 const qreal newMaximumHeight = item->layoutAttached()->property("preferredHeight").toReal(); 0299 const qreal newMaximumWidth = item->layoutAttached()->property("preferredWidth").toReal(); 0300 0301 if (newMaximumHeight > 0 && newMaximumHeight < height()) { 0302 item->setHeight(newMaximumHeight); 0303 changed = true; 0304 } 0305 if (newMaximumHeight > 0 && newMaximumWidth < width()) { 0306 item->setWidth(newMaximumWidth); 0307 changed = true; 0308 }*/ 0309 0310 // Relayout if anything changed 0311 if (changed && itemIsManaged(item)) { 0312 releaseSpace(item); 0313 positionItem(item); 0314 assignSpace(item); 0315 } 0316 } 0317 0318 QRect GridLayoutManager::cellBasedGeometry(const QRectF &geom) const 0319 { 0320 return QRect(round(qBound(0.0, geom.x(), qMax(0.0, layout()->width() - geom.width())) / cellSize().width()), 0321 round(qBound(0.0, geom.y(), qMax(0.0, layout()->height() - geom.height())) / cellSize().height()), 0322 round((qreal)geom.width() / cellSize().width()), 0323 round((qreal)geom.height() / cellSize().height())); 0324 } 0325 0326 QRect GridLayoutManager::cellBasedBoundingGeometry(const QRectF &geom) const 0327 { 0328 return QRect(floor(qBound(0.0, geom.x(), layout()->width() - geom.width()) / cellSize().width()), 0329 floor(qBound(0.0, geom.y(), layout()->height() - geom.height()) / cellSize().height()), 0330 ceil((qreal)geom.width() / cellSize().width()), 0331 ceil((qreal)geom.height() / cellSize().height())); 0332 } 0333 0334 bool GridLayoutManager::isOutOfBounds(const QPair<int, int> &cell) const 0335 { 0336 return cell.first < 0 || cell.second < 0 || cell.first >= rows() || cell.second >= columns(); 0337 } 0338 0339 bool GridLayoutManager::isCellAvailable(const QPair<int, int> &cell) const 0340 { 0341 return !isOutOfBounds(cell) && !m_grid.contains(cell); 0342 } 0343 0344 QRectF GridLayoutManager::itemGeometry(QQuickItem *item) const 0345 { 0346 return QRectF(item->x(), item->y(), item->width(), item->height()); 0347 } 0348 0349 QPair<int, int> GridLayoutManager::nextCell(const QPair<int, int> &cell, AppletsLayout::PreferredLayoutDirection direction) const 0350 { 0351 QPair<int, int> nCell = cell; 0352 0353 switch (direction) { 0354 case AppletsLayout::AppletsLayout::BottomToTop: 0355 --nCell.first; 0356 break; 0357 case AppletsLayout::AppletsLayout::TopToBottom: 0358 ++nCell.first; 0359 break; 0360 case AppletsLayout::AppletsLayout::RightToLeft: 0361 --nCell.second; 0362 break; 0363 case AppletsLayout::AppletsLayout::LeftToRight: 0364 default: 0365 ++nCell.second; 0366 break; 0367 } 0368 0369 return nCell; 0370 } 0371 0372 QPair<int, int> GridLayoutManager::nextAvailableCell(const QPair<int, int> &cell, AppletsLayout::PreferredLayoutDirection direction) const 0373 { 0374 QPair<int, int> nCell = cell; 0375 while (!isOutOfBounds(nCell)) { 0376 nCell = nextCell(nCell, direction); 0377 0378 if (isOutOfBounds(nCell)) { 0379 switch (direction) { 0380 case AppletsLayout::AppletsLayout::BottomToTop: 0381 nCell.first = rows() - 1; 0382 --nCell.second; 0383 break; 0384 case AppletsLayout::AppletsLayout::TopToBottom: 0385 nCell.first = 0; 0386 ++nCell.second; 0387 break; 0388 case AppletsLayout::AppletsLayout::RightToLeft: 0389 --nCell.first; 0390 nCell.second = columns() - 1; 0391 break; 0392 case AppletsLayout::AppletsLayout::LeftToRight: 0393 default: 0394 ++nCell.first; 0395 nCell.second = 0; 0396 break; 0397 } 0398 } 0399 0400 if (isCellAvailable(nCell)) { 0401 return nCell; 0402 } 0403 } 0404 0405 return QPair<int, int>(-1, -1); 0406 } 0407 0408 QPair<int, int> GridLayoutManager::nextTakenCell(const QPair<int, int> &cell, AppletsLayout::PreferredLayoutDirection direction) const 0409 { 0410 QPair<int, int> nCell = cell; 0411 while (!isOutOfBounds(nCell)) { 0412 nCell = nextCell(nCell, direction); 0413 0414 if (isOutOfBounds(nCell)) { 0415 switch (direction) { 0416 case AppletsLayout::AppletsLayout::BottomToTop: 0417 nCell.first = rows() - 1; 0418 --nCell.second; 0419 break; 0420 case AppletsLayout::AppletsLayout::TopToBottom: 0421 nCell.first = 0; 0422 ++nCell.second; 0423 break; 0424 case AppletsLayout::AppletsLayout::RightToLeft: 0425 --nCell.first; 0426 nCell.second = columns() - 1; 0427 break; 0428 case AppletsLayout::AppletsLayout::LeftToRight: 0429 default: 0430 ++nCell.first; 0431 nCell.second = 0; 0432 break; 0433 } 0434 } 0435 0436 if (!isCellAvailable(nCell)) { 0437 return nCell; 0438 } 0439 } 0440 0441 return QPair<int, int>(-1, -1); 0442 } 0443 0444 int GridLayoutManager::freeSpaceInDirection(const QPair<int, int> &cell, AppletsLayout::PreferredLayoutDirection direction) const 0445 { 0446 QPair<int, int> nCell = cell; 0447 0448 int avail = 0; 0449 0450 while (isCellAvailable(nCell)) { 0451 ++avail; 0452 nCell = nextCell(nCell, direction); 0453 } 0454 0455 return avail; 0456 } 0457 0458 QRectF GridLayoutManager::nextAvailableSpace(ItemContainer *item, const QSizeF &minimumSize, AppletsLayout::PreferredLayoutDirection direction) const 0459 { 0460 // The mionimum size in grid units 0461 const QSize minimumGridSize(ceil((qreal)minimumSize.width() / cellSize().width()), ceil((qreal)minimumSize.height() / cellSize().height())); 0462 0463 QRect itemCellGeom = cellBasedGeometry(itemGeometry(item)); 0464 itemCellGeom.setWidth(qMax(itemCellGeom.width(), minimumGridSize.width())); 0465 itemCellGeom.setHeight(qMax(itemCellGeom.height(), minimumGridSize.height())); 0466 0467 QSize partialSize; 0468 0469 QPair<int, int> cell(itemCellGeom.y(), itemCellGeom.x()); 0470 if (direction == AppletsLayout::AppletsLayout::RightToLeft) { 0471 cell.second += itemCellGeom.width(); 0472 } else if (direction == AppletsLayout::AppletsLayout::BottomToTop) { 0473 cell.first += itemCellGeom.height(); 0474 } 0475 0476 if (!isCellAvailable(cell)) { 0477 cell = nextAvailableCell(cell, direction); 0478 } 0479 0480 while (!isOutOfBounds(cell)) { 0481 if (direction == AppletsLayout::LeftToRight || direction == AppletsLayout::RightToLeft) { 0482 partialSize = QSize(INT_MAX, 0); 0483 0484 int currentRow = cell.first; 0485 for (; currentRow < cell.first + itemCellGeom.height(); ++currentRow) { 0486 const int freeRow = freeSpaceInDirection(QPair<int, int>(currentRow, cell.second), direction); 0487 0488 partialSize.setWidth(qMin(partialSize.width(), freeRow)); 0489 0490 if (freeRow > 0) { 0491 partialSize.setHeight(partialSize.height() + 1); 0492 } else if (partialSize.height() < minimumGridSize.height()) { 0493 break; 0494 } 0495 0496 if (partialSize.width() >= itemCellGeom.width() && partialSize.height() >= itemCellGeom.height()) { 0497 break; 0498 } else if (partialSize.width() < minimumGridSize.width()) { 0499 break; 0500 } 0501 } 0502 0503 if (partialSize.width() >= minimumGridSize.width() && partialSize.height() >= minimumGridSize.height()) { 0504 const int width = qMin(itemCellGeom.width(), partialSize.width()) * cellSize().width(); 0505 const int height = qMin(itemCellGeom.height(), partialSize.height()) * cellSize().height(); 0506 0507 if (direction == AppletsLayout::RightToLeft) { 0508 return QRectF((cell.second + 1) * cellSize().width() - width, cell.first * cellSize().height(), width, height); 0509 // AppletsLayout::LeftToRight 0510 } else { 0511 return QRectF(cell.second * cellSize().width(), cell.first * cellSize().height(), width, height); 0512 } 0513 } else { 0514 cell = nextAvailableCell(nextTakenCell(cell, direction), direction); 0515 } 0516 0517 } else if (direction == AppletsLayout::TopToBottom || direction == AppletsLayout::BottomToTop) { 0518 partialSize = QSize(0, INT_MAX); 0519 0520 int currentColumn = cell.second; 0521 for (; currentColumn < cell.second + itemCellGeom.width(); ++currentColumn) { 0522 const int freeColumn = freeSpaceInDirection(QPair<int, int>(cell.first, currentColumn), direction); 0523 0524 partialSize.setHeight(qMin(partialSize.height(), freeColumn)); 0525 0526 if (freeColumn > 0) { 0527 partialSize.setWidth(partialSize.width() + 1); 0528 } else if (partialSize.width() < minimumGridSize.width()) { 0529 break; 0530 } 0531 0532 if (partialSize.width() >= itemCellGeom.width() && partialSize.height() >= itemCellGeom.height()) { 0533 break; 0534 } else if (partialSize.height() < minimumGridSize.height()) { 0535 break; 0536 } 0537 } 0538 0539 if (partialSize.width() >= minimumGridSize.width() && partialSize.height() >= minimumGridSize.height()) { 0540 const int width = qMin(itemCellGeom.width(), partialSize.width()) * cellSize().width(); 0541 const int height = qMin(itemCellGeom.height(), partialSize.height()) * cellSize().height(); 0542 0543 if (direction == AppletsLayout::BottomToTop) { 0544 return QRectF(cell.second * cellSize().width(), (cell.first + 1) * cellSize().height() - height, width, height); 0545 // AppletsLayout::TopToBottom: 0546 } else { 0547 return QRectF(cell.second * cellSize().width(), cell.first * cellSize().height(), width, height); 0548 } 0549 } else { 0550 cell = nextAvailableCell(nextTakenCell(cell, direction), direction); 0551 } 0552 } 0553 } 0554 0555 // We didn't manage to find layout space, return invalid geometry 0556 return QRectF(); 0557 } 0558 0559 #include "moc_gridlayoutmanager.cpp"