File indexing completed on 2024-05-12 17:08:55

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 : qAsConst(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(), layout()->width() - geom.width()) / cellSize().width()),
0321                  round(qBound(0.0, geom.y(), 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"