File indexing completed on 2024-12-29 05:06:03

0001 // SPDX-FileCopyrightText: 2022-2023 Devin Lin <devin@kde.org>
0002 // SPDX-License-Identifier: GPL-2.0-or-later
0003 
0004 #include "folioapplicationfolder.h"
0005 #include "homescreenstate.h"
0006 
0007 #include <QJsonArray>
0008 #include <algorithm>
0009 
0010 FolioApplicationFolder::FolioApplicationFolder(QObject *parent, QString name)
0011     : QObject{parent}
0012     , m_name{name}
0013     , m_applicationFolderModel{new ApplicationFolderModel{this}}
0014 {
0015 }
0016 
0017 FolioApplicationFolder *FolioApplicationFolder::fromJson(QJsonObject &obj, QObject *parent)
0018 {
0019     QString name = obj[QStringLiteral("name")].toString();
0020     QList<FolioApplication *> apps;
0021     for (auto storageId : obj[QStringLiteral("apps")].toArray()) {
0022         if (KService::Ptr service = KService::serviceByStorageId(storageId.toString())) {
0023             apps.append(new FolioApplication(parent, service));
0024         }
0025     }
0026 
0027     FolioApplicationFolder *folder = new FolioApplicationFolder(parent, name);
0028     folder->setApplications(apps);
0029     return folder;
0030 }
0031 
0032 QJsonObject FolioApplicationFolder::toJson() const
0033 {
0034     QJsonObject obj;
0035     obj[QStringLiteral("type")] = "folder";
0036     obj[QStringLiteral("name")] = m_name;
0037 
0038     QJsonArray arr;
0039     for (auto delegate : m_delegates) {
0040         if (delegate.delegate->type() != FolioDelegate::Application) {
0041             continue;
0042         }
0043         arr.append(QJsonValue::fromVariant(delegate.delegate->application()->storageId()));
0044     }
0045 
0046     obj[QStringLiteral("apps")] = arr;
0047 
0048     return obj;
0049 }
0050 
0051 QString FolioApplicationFolder::name() const
0052 {
0053     return m_name;
0054 }
0055 
0056 void FolioApplicationFolder::setName(QString &name)
0057 {
0058     m_name = name;
0059     Q_EMIT nameChanged();
0060     Q_EMIT saveRequested();
0061 }
0062 
0063 QList<FolioApplication *> FolioApplicationFolder::appPreviews()
0064 {
0065     QList<FolioApplication *> previews;
0066     // we give a maximum of 4 icons
0067     for (int i = 0; i < std::min<int>(m_delegates.size(), 4); ++i) {
0068         if (!m_delegates[i].delegate->application()) {
0069             continue;
0070         }
0071         previews.push_back(m_delegates[i].delegate->application());
0072     }
0073     return previews;
0074 }
0075 
0076 ApplicationFolderModel *FolioApplicationFolder::applications()
0077 {
0078     return m_applicationFolderModel;
0079 }
0080 
0081 void FolioApplicationFolder::setApplications(QList<FolioApplication *> applications)
0082 {
0083     if (m_applicationFolderModel) {
0084         m_applicationFolderModel->deleteLater();
0085     }
0086 
0087     m_delegates.clear();
0088     for (auto *app : applications) {
0089         m_delegates.append({new FolioDelegate{app, this}, 0, 0});
0090     }
0091     m_applicationFolderModel = new ApplicationFolderModel{this};
0092     m_applicationFolderModel->evaluateDelegatePositions();
0093 
0094     Q_EMIT applicationsChanged();
0095     Q_EMIT applicationsReset();
0096     Q_EMIT saveRequested();
0097 }
0098 
0099 void FolioApplicationFolder::moveEntry(int fromRow, int toRow)
0100 {
0101     m_applicationFolderModel->moveEntry(fromRow, toRow);
0102 }
0103 
0104 bool FolioApplicationFolder::addDelegate(FolioDelegate *delegate, int row)
0105 {
0106     return m_applicationFolderModel->addDelegate(delegate, row);
0107 }
0108 
0109 void FolioApplicationFolder::removeDelegate(int row)
0110 {
0111     m_applicationFolderModel->removeDelegate(row);
0112 }
0113 
0114 int FolioApplicationFolder::dropInsertPosition(int page, qreal x, qreal y)
0115 {
0116     return m_applicationFolderModel->dropInsertPosition(page, x, y);
0117 }
0118 
0119 bool FolioApplicationFolder::isDropPositionOutside(qreal x, qreal y)
0120 {
0121     return m_applicationFolderModel->isDropPositionOutside(x, y);
0122 }
0123 
0124 ApplicationFolderModel::ApplicationFolderModel(FolioApplicationFolder *folder)
0125     : QAbstractListModel{folder}
0126     , m_folder{folder}
0127 {
0128     connect(HomeScreenState::self(), &HomeScreenState::folderPageWidthChanged, this, [this]() {
0129         evaluateDelegatePositions();
0130     });
0131     connect(HomeScreenState::self(), &HomeScreenState::folderPageHeightChanged, this, [this]() {
0132         evaluateDelegatePositions();
0133     });
0134     connect(HomeScreenState::self(), &HomeScreenState::folderPageContentWidthChanged, this, [this]() {
0135         evaluateDelegatePositions();
0136     });
0137     connect(HomeScreenState::self(), &HomeScreenState::folderPageContentHeightChanged, this, [this]() {
0138         evaluateDelegatePositions();
0139     });
0140     connect(HomeScreenState::self(), &HomeScreenState::viewWidthChanged, this, [this]() {
0141         evaluateDelegatePositions();
0142     });
0143     connect(HomeScreenState::self(), &HomeScreenState::viewHeightChanged, this, [this]() {
0144         evaluateDelegatePositions();
0145     });
0146     connect(HomeScreenState::self(), &HomeScreenState::pageCellWidthChanged, this, [this]() {
0147         evaluateDelegatePositions();
0148     });
0149     connect(HomeScreenState::self(), &HomeScreenState::pageCellHeightChanged, this, [this]() {
0150         evaluateDelegatePositions();
0151     });
0152 }
0153 
0154 int ApplicationFolderModel::rowCount(const QModelIndex &parent) const
0155 {
0156     return m_folder->m_delegates.size();
0157 }
0158 
0159 QVariant ApplicationFolderModel::data(const QModelIndex &index, int role) const
0160 {
0161     if (!index.isValid()) {
0162         return QVariant();
0163     }
0164 
0165     switch (role) {
0166     case DelegateRole:
0167         return QVariant::fromValue(m_folder->m_delegates.at(index.row()).delegate);
0168     case XPositionRole:
0169         return QVariant::fromValue(m_folder->m_delegates.at(index.row()).xPosition);
0170     case YPositionRole:
0171         return QVariant::fromValue(m_folder->m_delegates.at(index.row()).yPosition);
0172     }
0173 
0174     return QVariant();
0175 }
0176 
0177 QHash<int, QByteArray> ApplicationFolderModel::roleNames() const
0178 {
0179     return {{DelegateRole, "delegate"}, {XPositionRole, "xPosition"}, {YPositionRole, "yPosition"}};
0180 }
0181 
0182 FolioDelegate *ApplicationFolderModel::getDelegate(int index)
0183 {
0184     if (index < 0 || index >= m_folder->m_delegates.size()) {
0185         return nullptr;
0186     }
0187     return m_folder->m_delegates[index].delegate;
0188 }
0189 
0190 void ApplicationFolderModel::moveEntry(int fromRow, int toRow)
0191 {
0192     if (fromRow < 0 || toRow < 0 || fromRow >= m_folder->m_delegates.size() || toRow >= m_folder->m_delegates.size() || fromRow == toRow) {
0193         return;
0194     }
0195     if (toRow > fromRow) {
0196         ++toRow;
0197     }
0198 
0199     beginMoveRows(QModelIndex(), fromRow, fromRow, QModelIndex(), toRow);
0200     if (toRow > fromRow) {
0201         auto delegate = m_folder->m_delegates.at(fromRow);
0202         m_folder->m_delegates.insert(toRow, delegate);
0203         m_folder->m_delegates.takeAt(fromRow);
0204     } else {
0205         auto delegate = m_folder->m_delegates.takeAt(fromRow);
0206         m_folder->m_delegates.insert(toRow, delegate);
0207     }
0208     endMoveRows();
0209 
0210     evaluateDelegatePositions();
0211 
0212     Q_EMIT m_folder->applicationsChanged();
0213     Q_EMIT m_folder->saveRequested();
0214 }
0215 
0216 bool ApplicationFolderModel::canAddDelegate(FolioDelegate *delegate, int index)
0217 {
0218     if (index < 0 || index > m_folder->m_delegates.size()) {
0219         return false;
0220     }
0221 
0222     if (!delegate) {
0223         return false;
0224     }
0225 
0226     return true;
0227 }
0228 
0229 bool ApplicationFolderModel::addDelegate(FolioDelegate *delegate, int index)
0230 {
0231     if (!canAddDelegate(delegate, index)) {
0232         return false;
0233     }
0234 
0235     if (index == m_folder->m_delegates.size()) {
0236         beginInsertRows(QModelIndex(), index, index);
0237         m_folder->m_delegates.append({delegate, 0, 0});
0238         evaluateDelegatePositions(false);
0239         endInsertRows();
0240     } else if (m_folder->m_delegates[index].delegate->type() == FolioDelegate::None) {
0241         replaceGhostEntry(delegate);
0242     } else {
0243         beginInsertRows(QModelIndex(), index, index);
0244         m_folder->m_delegates.insert(index, {delegate, 0, 0});
0245         evaluateDelegatePositions(false);
0246         endInsertRows();
0247     }
0248 
0249     evaluateDelegatePositions();
0250 
0251     Q_EMIT m_folder->applicationsChanged();
0252     Q_EMIT m_folder->saveRequested();
0253 
0254     return true;
0255 }
0256 
0257 void ApplicationFolderModel::removeDelegate(int index)
0258 {
0259     if (index < 0 || index >= m_folder->m_delegates.size()) {
0260         return;
0261     }
0262 
0263     beginRemoveRows(QModelIndex(), index, index);
0264     // HACK: do not deleteLater(), because the delegate might still be used somewhere else
0265     // m_folder->m_delegates[index].app->deleteLater();
0266     m_folder->m_delegates.removeAt(index);
0267     endRemoveRows();
0268 
0269     evaluateDelegatePositions();
0270 
0271     Q_EMIT m_folder->applicationsChanged();
0272     Q_EMIT m_folder->saveRequested();
0273 }
0274 
0275 QPointF ApplicationFolderModel::getDelegatePosition(int index)
0276 {
0277     if (index < 0 || index >= m_folder->m_delegates.size()) {
0278         return {0, 0};
0279     }
0280     auto delegate = m_folder->m_delegates[index];
0281     return {delegate.xPosition, delegate.yPosition};
0282 }
0283 
0284 int ApplicationFolderModel::getGhostEntryPosition()
0285 {
0286     for (int i = 0; i < m_folder->m_delegates.size(); i++) {
0287         if (m_folder->m_delegates[i].delegate->type() == FolioDelegate::None) {
0288             return i;
0289         }
0290     }
0291     return -1;
0292 }
0293 
0294 void ApplicationFolderModel::setGhostEntry(int index)
0295 {
0296     FolioDelegate *ghost = nullptr;
0297 
0298     // check if a ghost entry already exists
0299     for (int i = 0; i < m_folder->m_delegates.size(); i++) {
0300         auto delegate = m_folder->m_delegates[i].delegate;
0301         if (delegate->type() == FolioDelegate::None) {
0302             ghost = delegate;
0303 
0304             // remove it
0305             removeDelegate(i);
0306 
0307             // correct index if necessary due to deletion
0308             if (index > i) {
0309                 index--;
0310             }
0311         }
0312     }
0313 
0314     if (!ghost) {
0315         ghost = new FolioDelegate{HomeScreenState::self()};
0316     }
0317 
0318     // add empty delegate at new position
0319     addDelegate(ghost, index);
0320 }
0321 
0322 void ApplicationFolderModel::replaceGhostEntry(FolioDelegate *delegate)
0323 {
0324     for (int i = 0; i < m_folder->m_delegates.size(); i++) {
0325         if (m_folder->m_delegates[i].delegate->type() == FolioDelegate::None) {
0326             m_folder->m_delegates[i].delegate = delegate;
0327 
0328             Q_EMIT dataChanged(createIndex(i, 0), createIndex(i, 0), {DelegateRole});
0329             break;
0330         }
0331     }
0332 }
0333 
0334 void ApplicationFolderModel::deleteGhostEntry()
0335 {
0336     for (int i = 0; i < m_folder->m_delegates.size(); i++) {
0337         if (m_folder->m_delegates[i].delegate->type() == FolioDelegate::None) {
0338             removeDelegate(i);
0339         }
0340     }
0341 }
0342 
0343 int ApplicationFolderModel::dropInsertPosition(int page, qreal x, qreal y)
0344 {
0345     qreal cellWidth = HomeScreenState::self()->pageCellWidth();
0346     qreal cellHeight = HomeScreenState::self()->pageCellHeight();
0347 
0348     int row = (y - topMarginFromScreenEdge()) / cellHeight;
0349     row = std::max(0, std::min(numRowsOnPage(), row));
0350 
0351     // the index that the position is over
0352     int leftColumn = std::max(0.0, x - leftMarginFromScreenEdge()) / cellWidth;
0353     leftColumn = std::min(numColumnsOnPage() - 1, leftColumn);
0354 
0355     qreal leftColumnPosition = leftColumn * cellWidth + leftMarginFromScreenEdge();
0356 
0357     int column = leftColumn + 1;
0358 
0359     // if it's the left half of this position or it's the last column on this row, return itself
0360     if ((x < leftColumnPosition + cellWidth * 0.5) || (leftColumn == numColumnsOnPage() - 1)) {
0361         column = leftColumn;
0362     }
0363 
0364     // calculate the position based on the page, row and column it is at
0365     int pos = (page * numRowsOnPage() * numColumnsOnPage()) + (row * numColumnsOnPage()) + column;
0366     // make sure it's in bounds
0367     return std::min((int)m_folder->m_delegates.size(), std::max(0, pos));
0368 }
0369 
0370 bool ApplicationFolderModel::isDropPositionOutside(qreal x, qreal y)
0371 {
0372     return (x < leftMarginFromScreenEdge()) || (x > (HomeScreenState::self()->viewWidth() - leftMarginFromScreenEdge())) || (y < topMarginFromScreenEdge())
0373         || (y > HomeScreenState::self()->viewHeight() - topMarginFromScreenEdge());
0374 }
0375 
0376 void ApplicationFolderModel::evaluateDelegatePositions(bool emitSignal)
0377 {
0378     qreal pageWidth = HomeScreenState::self()->folderPageWidth();
0379     qreal topMargin = verticalPageMargin();
0380     qreal leftMargin = horizontalPageMargin();
0381 
0382     qreal cellWidth = HomeScreenState::self()->pageCellWidth();
0383     qreal cellHeight = HomeScreenState::self()->pageCellHeight();
0384 
0385     int rows = numRowsOnPage();
0386     int columns = numColumnsOnPage();
0387     int numOfDelegates = m_folder->m_delegates.size();
0388 
0389     int index = 0;
0390     int page = 0;
0391 
0392     while (index < m_folder->m_delegates.size()) {
0393         int prevIndex = index;
0394 
0395         // determine positions page-by-page
0396         for (int row = 0; row < rows && index < numOfDelegates; row++) {
0397             for (int column = 0; column < columns && index < numOfDelegates; column++) {
0398                 m_folder->m_delegates[index].xPosition = qRound(page * pageWidth + leftMargin + column * cellWidth);
0399                 m_folder->m_delegates[index].yPosition = qRound(topMargin + row * cellHeight);
0400                 index++;
0401             }
0402         }
0403 
0404         // prevent infinite loop
0405         if (prevIndex == index) {
0406             break;
0407         }
0408         page++;
0409     }
0410 
0411     if (emitSignal) {
0412         Q_EMIT dataChanged(createIndex(0, 0), createIndex(m_folder->m_delegates.size() - 1, 0), {XPositionRole});
0413         Q_EMIT dataChanged(createIndex(0, 0), createIndex(m_folder->m_delegates.size() - 1, 0), {YPositionRole});
0414     }
0415 
0416     Q_EMIT numberOfPagesChanged();
0417 }
0418 
0419 QPointF ApplicationFolderModel::getDelegateStartPosition(int page)
0420 {
0421     qreal pageWidth = HomeScreenState::self()->folderPageWidth();
0422 
0423     qreal x = pageWidth * page + leftMarginFromScreenEdge();
0424     qreal y = topMarginFromScreenEdge();
0425     return QPointF{x, y};
0426 }
0427 
0428 int ApplicationFolderModel::numTotalPages()
0429 {
0430     int numOfDelegatesOnPage = numRowsOnPage() * numColumnsOnPage();
0431     return std::ceil(((qreal)m_folder->m_delegates.size()) / numOfDelegatesOnPage);
0432 }
0433 
0434 int ApplicationFolderModel::numRowsOnPage()
0435 {
0436     qreal contentHeight = HomeScreenState::self()->folderPageContentHeight();
0437     qreal cellHeight = HomeScreenState::self()->pageCellHeight();
0438 
0439     return std::max(0.0, contentHeight / cellHeight);
0440 }
0441 
0442 int ApplicationFolderModel::numColumnsOnPage()
0443 {
0444     qreal contentWidth = HomeScreenState::self()->folderPageContentWidth();
0445     qreal cellWidth = HomeScreenState::self()->pageCellWidth();
0446 
0447     return std::max(0.0, contentWidth / cellWidth);
0448 }
0449 
0450 qreal ApplicationFolderModel::leftMarginFromScreenEdge()
0451 {
0452     qreal viewWidth = HomeScreenState::self()->viewWidth();
0453     qreal folderPageWidth = HomeScreenState::self()->folderPageWidth();
0454 
0455     return (viewWidth - folderPageWidth) / 2 + horizontalPageMargin();
0456 }
0457 
0458 qreal ApplicationFolderModel::topMarginFromScreenEdge()
0459 {
0460     qreal viewHeight = HomeScreenState::self()->viewHeight();
0461     qreal folderPageHeight = HomeScreenState::self()->folderPageHeight();
0462 
0463     return (viewHeight - folderPageHeight) / 2 + verticalPageMargin();
0464 }
0465 
0466 qreal ApplicationFolderModel::horizontalPageMargin()
0467 {
0468     qreal pageWidth = HomeScreenState::self()->folderPageWidth();
0469     qreal pageContentWidth = HomeScreenState::self()->folderPageContentWidth();
0470 
0471     return (pageWidth - pageContentWidth) / 2;
0472 }
0473 
0474 qreal ApplicationFolderModel::verticalPageMargin()
0475 {
0476     qreal pageHeight = HomeScreenState::self()->folderPageHeight();
0477     qreal pageContentHeight = HomeScreenState::self()->folderPageContentHeight();
0478 
0479     return (pageHeight - pageContentHeight) / 2;
0480 }