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 }