File indexing completed on 2024-12-29 05:06:03
0001 // SPDX-FileCopyrightText: 2023 Devin Lin <devin@kde.org> 0002 // SPDX-License-Identifier: GPL-2.0-or-later 0003 0004 #include "favouritesmodel.h" 0005 #include "homescreenstate.h" 0006 0007 #include <QByteArray> 0008 #include <QDebug> 0009 #include <QJsonArray> 0010 #include <QJsonDocument> 0011 #include <QModelIndex> 0012 #include <QProcess> 0013 #include <QQuickWindow> 0014 0015 #include <KApplicationTrader> 0016 #include <KConfigGroup> 0017 #include <KIO/ApplicationLauncherJob> 0018 #include <KNotificationJobUiDelegate> 0019 #include <KService> 0020 #include <KSharedConfig> 0021 #include <KSycoca> 0022 0023 FavouritesModel *FavouritesModel::self() 0024 { 0025 static FavouritesModel *inst = new FavouritesModel(); 0026 return inst; 0027 } 0028 0029 FavouritesModel::FavouritesModel(QObject *parent) 0030 : QAbstractListModel{parent} 0031 { 0032 connect(HomeScreenState::self(), &HomeScreenState::pageWidthChanged, this, [this]() { 0033 evaluateDelegatePositions(true); 0034 }); 0035 connect(HomeScreenState::self(), &HomeScreenState::pageHeightChanged, this, [this]() { 0036 evaluateDelegatePositions(true); 0037 }); 0038 connect(HomeScreenState::self(), &HomeScreenState::pageCellWidthChanged, this, [this]() { 0039 evaluateDelegatePositions(true); 0040 }); 0041 connect(HomeScreenState::self(), &HomeScreenState::pageCellHeightChanged, this, [this]() { 0042 evaluateDelegatePositions(true); 0043 }); 0044 connect(HomeScreenState::self(), &HomeScreenState::favouritesBarLocationChanged, this, [this]() { 0045 evaluateDelegatePositions(true); 0046 }); 0047 connect(HomeScreenState::self(), &HomeScreenState::pageOrientationChanged, this, [this]() { 0048 evaluateDelegatePositions(true); 0049 }); 0050 } 0051 0052 int FavouritesModel::rowCount(const QModelIndex &parent) const 0053 { 0054 Q_UNUSED(parent) 0055 return m_delegates.count(); 0056 } 0057 0058 QVariant FavouritesModel::data(const QModelIndex &index, int role) const 0059 { 0060 if (!index.isValid() || index.row() < 0 || index.row() >= m_delegates.size()) { 0061 return QVariant(); 0062 } 0063 0064 switch (role) { 0065 case DelegateRole: 0066 return QVariant::fromValue(m_delegates.at(index.row()).delegate); 0067 case XPositionRole: 0068 return QVariant::fromValue(m_delegates.at(index.row()).xPosition); 0069 } 0070 0071 return QVariant(); 0072 } 0073 0074 QHash<int, QByteArray> FavouritesModel::roleNames() const 0075 { 0076 return {{DelegateRole, "delegate"}, {XPositionRole, "xPosition"}}; 0077 } 0078 0079 void FavouritesModel::removeEntry(int row) 0080 { 0081 if (row < 0 || row >= m_delegates.size()) { 0082 return; 0083 } 0084 0085 beginRemoveRows(QModelIndex(), row, row); 0086 // HACK: do not deleteLater(), because the delegate might still be used somewhere else 0087 // m_delegates[row].delegate->deleteLater(); 0088 m_delegates.removeAt(row); 0089 endRemoveRows(); 0090 0091 evaluateDelegatePositions(); 0092 0093 save(); 0094 } 0095 0096 void FavouritesModel::moveEntry(int fromRow, int toRow) 0097 { 0098 if (fromRow < 0 || toRow < 0 || fromRow >= m_delegates.size() || toRow >= m_delegates.size() || fromRow == toRow) { 0099 return; 0100 } 0101 if (toRow > fromRow) { 0102 ++toRow; 0103 } 0104 0105 beginMoveRows(QModelIndex(), fromRow, fromRow, QModelIndex(), toRow); 0106 if (toRow > fromRow) { 0107 auto delegate = m_delegates.at(fromRow); 0108 m_delegates.insert(toRow, delegate); 0109 m_delegates.takeAt(fromRow); 0110 0111 } else { 0112 auto delegate = m_delegates.takeAt(fromRow); 0113 m_delegates.insert(toRow, delegate); 0114 } 0115 endMoveRows(); 0116 0117 evaluateDelegatePositions(); 0118 0119 save(); 0120 } 0121 0122 bool FavouritesModel::canAddEntry(int row, FolioDelegate *delegate) 0123 { 0124 if (!delegate) { 0125 return false; 0126 } 0127 0128 if (row < 0 || row > m_delegates.size()) { 0129 return false; 0130 } 0131 0132 return true; 0133 } 0134 0135 bool FavouritesModel::addEntry(int row, FolioDelegate *delegate) 0136 { 0137 if (!canAddEntry(row, delegate)) { 0138 return false; 0139 } 0140 0141 if (row == m_delegates.size()) { 0142 beginInsertRows(QModelIndex(), row, row); 0143 m_delegates.append({delegate, 0}); 0144 evaluateDelegatePositions(false); 0145 endInsertRows(); 0146 } else if (m_delegates[row].delegate->type() == FolioDelegate::None) { 0147 replaceGhostEntry(delegate); 0148 } else { 0149 beginInsertRows(QModelIndex(), row, row); 0150 m_delegates.insert(row, {delegate, 0}); 0151 evaluateDelegatePositions(false); 0152 endInsertRows(); 0153 } 0154 0155 // ensure saves are connected when requested by the delegate 0156 connectSaveRequests(delegate); 0157 0158 evaluateDelegatePositions(); 0159 0160 save(); 0161 0162 return true; 0163 } 0164 0165 FolioDelegate *FavouritesModel::getEntryAt(int row) 0166 { 0167 if (row < 0 || row >= m_delegates.size()) { 0168 return nullptr; 0169 } 0170 0171 return m_delegates[row].delegate; 0172 } 0173 0174 int FavouritesModel::getGhostEntryPosition() 0175 { 0176 for (int i = 0; i < m_delegates.size(); i++) { 0177 if (m_delegates[i].delegate->type() == FolioDelegate::None) { 0178 return i; 0179 } 0180 } 0181 return -1; 0182 } 0183 0184 void FavouritesModel::setGhostEntry(int row) 0185 { 0186 bool found = false; 0187 0188 // check if a ghost entry already exists, then swap them 0189 for (int i = 0; i < m_delegates.size(); i++) { 0190 if (m_delegates[i].delegate->type() == FolioDelegate::None) { 0191 found = true; 0192 0193 if (row != i) { 0194 moveEntry(i, row); 0195 } 0196 } 0197 } 0198 0199 // if it doesn't, add a new empty delegate 0200 if (!found) { 0201 FolioDelegate *ghost = new FolioDelegate{this}; 0202 addEntry(row, ghost); 0203 } 0204 } 0205 0206 void FavouritesModel::replaceGhostEntry(FolioDelegate *delegate) 0207 { 0208 for (int i = 0; i < m_delegates.size(); i++) { 0209 if (m_delegates[i].delegate->type() == FolioDelegate::None) { 0210 m_delegates[i].delegate = delegate; 0211 0212 Q_EMIT dataChanged(createIndex(i, 0), createIndex(i, 0), {DelegateRole}); 0213 break; 0214 } 0215 } 0216 } 0217 0218 void FavouritesModel::deleteGhostEntry() 0219 { 0220 for (int i = 0; i < m_delegates.size(); i++) { 0221 if (m_delegates[i].delegate->type() == FolioDelegate::None) { 0222 removeEntry(i); 0223 } 0224 } 0225 } 0226 0227 QJsonArray FavouritesModel::exportToJson() 0228 { 0229 QJsonArray arr; 0230 for (int i = 0; i < m_delegates.size(); i++) { 0231 FolioDelegate *delegate = m_delegates[i].delegate; 0232 0233 // if this delegate is empty, ignore it 0234 if (!delegate || delegate->type() == FolioDelegate::None) { 0235 continue; 0236 } 0237 0238 arr.append(delegate->toJson()); 0239 } 0240 return arr; 0241 } 0242 0243 void FavouritesModel::save() 0244 { 0245 if (!m_containment) { 0246 return; 0247 } 0248 0249 QJsonArray arr = exportToJson(); 0250 QByteArray data = QJsonDocument(arr).toJson(QJsonDocument::Compact); 0251 0252 m_containment->config().writeEntry("Favourites", QString::fromStdString(data.toStdString())); 0253 Q_EMIT m_containment->configNeedsSaving(); 0254 } 0255 0256 void FavouritesModel::load() 0257 { 0258 if (!m_containment) { 0259 return; 0260 } 0261 0262 QJsonDocument doc = QJsonDocument::fromJson(m_containment->config().readEntry("Favourites", "{}").toUtf8()); 0263 loadFromJson(doc.array()); 0264 } 0265 0266 void FavouritesModel::loadFromJson(QJsonArray arr) 0267 { 0268 beginResetModel(); 0269 0270 m_delegates.clear(); 0271 0272 for (QJsonValueRef r : arr) { 0273 QJsonObject obj = r.toObject(); 0274 FolioDelegate *delegate = FolioDelegate::fromJson(obj, this); 0275 0276 if (delegate) { 0277 connectSaveRequests(delegate); 0278 m_delegates.append({delegate, 0}); 0279 } 0280 } 0281 0282 evaluateDelegatePositions(false); 0283 endResetModel(); 0284 } 0285 0286 void FavouritesModel::connectSaveRequests(FolioDelegate *delegate) 0287 { 0288 if (delegate->type() == FolioDelegate::Folder && delegate->folder()) { 0289 connect(delegate->folder(), &FolioApplicationFolder::saveRequested, this, &FavouritesModel::save); 0290 } 0291 } 0292 0293 void FavouritesModel::setContainment(Plasma::Containment *containment) 0294 { 0295 m_containment = containment; 0296 } 0297 0298 bool FavouritesModel::dropPositionIsEdge(qreal x, qreal y) const 0299 { 0300 qreal startPosition = getDelegateRowStartPos(); 0301 bool isLocationBottom = HomeScreenState::self()->favouritesBarLocation() == HomeScreenState::Bottom; 0302 qreal cellLength = isLocationBottom ? HomeScreenState::self()->pageCellWidth() : HomeScreenState::self()->pageCellHeight(); 0303 0304 qreal pos = isLocationBottom ? x : y; 0305 0306 if (pos < startPosition) { 0307 return true; 0308 } 0309 0310 qreal currentPos = startPosition; 0311 0312 for (int i = 0; i < m_delegates.size(); i++) { 0313 // if it is within the centre 70% of a delegate, it is not at an edge 0314 if (pos >= (currentPos + cellLength * 0.15) && pos <= (currentPos + cellLength * 0.85)) { 0315 return false; 0316 } 0317 0318 currentPos += cellLength; 0319 } 0320 0321 return true; 0322 } 0323 0324 int FavouritesModel::dropInsertPosition(qreal x, qreal y) const 0325 { 0326 qreal startPosition = getDelegateRowStartPos(); 0327 bool isLocationBottom = HomeScreenState::self()->favouritesBarLocation() == HomeScreenState::Bottom; 0328 qreal cellLength = isLocationBottom ? HomeScreenState::self()->pageCellWidth() : HomeScreenState::self()->pageCellHeight(); 0329 0330 qreal pos = isLocationBottom ? x : y; 0331 0332 if (pos < startPosition) { 0333 return adjustIndex(0); 0334 } 0335 0336 qreal currentPos = startPosition; 0337 for (int i = 0; i < m_delegates.size(); i++) { 0338 if (pos < currentPos + cellLength * 0.85) { 0339 return adjustIndex(i); 0340 } else if (pos < currentPos + cellLength) { 0341 return adjustIndex(i + 1); 0342 } 0343 0344 currentPos += cellLength; 0345 } 0346 return adjustIndex(m_delegates.size()); 0347 } 0348 0349 QPointF FavouritesModel::getDelegateScreenPosition(int position) const 0350 { 0351 position = adjustIndex(position); 0352 0353 qreal screenHeight = HomeScreenState::self()->viewHeight(); 0354 qreal screenWidth = HomeScreenState::self()->viewWidth(); 0355 qreal pageHeight = HomeScreenState::self()->pageHeight(); 0356 qreal pageWidth = HomeScreenState::self()->pageWidth(); 0357 qreal screenTopPadding = HomeScreenState::self()->viewTopPadding(); 0358 qreal screenBottomPadding = HomeScreenState::self()->viewBottomPadding(); 0359 qreal screenLeftPadding = HomeScreenState::self()->viewLeftPadding(); 0360 qreal screenRightPadding = HomeScreenState::self()->viewRightPadding(); 0361 qreal cellHeight = HomeScreenState::self()->pageCellHeight(); 0362 qreal cellWidth = HomeScreenState::self()->pageCellWidth(); 0363 0364 qreal startPosition = getDelegateRowStartPos(); 0365 0366 switch (HomeScreenState::self()->favouritesBarLocation()) { 0367 case HomeScreenState::Bottom: { 0368 qreal favouritesHeight = screenHeight - pageHeight - screenBottomPadding - screenTopPadding; 0369 qreal x = screenLeftPadding + startPosition + cellWidth * position; 0370 qreal y = screenTopPadding + pageHeight + (favouritesHeight / 2) - (cellHeight / 2); 0371 return {x, y}; 0372 } 0373 case HomeScreenState::Left: { 0374 qreal favouritesWidth = screenWidth - screenLeftPadding - pageWidth - screenRightPadding; 0375 qreal x = screenLeftPadding + (favouritesWidth / 2) - (cellWidth / 2); 0376 qreal y = startPosition + cellHeight * position; 0377 return {x, y}; 0378 } 0379 case HomeScreenState::Right: { 0380 qreal favouritesWidth = screenWidth - screenLeftPadding - pageWidth - screenRightPadding; 0381 qreal x = screenLeftPadding + pageWidth + (favouritesWidth / 2) - (cellWidth / 2); 0382 qreal y = startPosition + cellHeight * position; 0383 return {x, y}; 0384 } 0385 } 0386 return {0, 0}; 0387 } 0388 0389 void FavouritesModel::evaluateDelegatePositions(bool emitSignal) 0390 { 0391 bool isLocationBottom = HomeScreenState::self()->favouritesBarLocation() == HomeScreenState::Bottom; 0392 qreal cellLength = isLocationBottom ? HomeScreenState::self()->pageCellWidth() : HomeScreenState::self()->pageCellHeight(); 0393 qreal startPosition = getDelegateRowStartPos(); 0394 qreal currentPos = startPosition; 0395 0396 for (int i = 0; i < m_delegates.size(); ++i) { 0397 m_delegates[adjustIndex(i)].xPosition = qRound(currentPos); 0398 currentPos += cellLength; 0399 } 0400 0401 if (emitSignal) { 0402 Q_EMIT dataChanged(createIndex(0, 0), createIndex(m_delegates.size() - 1, 0), {XPositionRole}); 0403 } 0404 } 0405 0406 qreal FavouritesModel::getDelegateRowStartPos() const 0407 { 0408 const int length = m_delegates.size(); 0409 const bool isLocationBottom = HomeScreenState::self()->favouritesBarLocation() == HomeScreenState::Bottom; 0410 const qreal cellLength = isLocationBottom ? HomeScreenState::self()->pageCellWidth() : HomeScreenState::self()->pageCellHeight(); 0411 const qreal pageLength = isLocationBottom ? HomeScreenState::self()->pageWidth() : HomeScreenState::self()->pageHeight(); 0412 0413 const qreal topMargin = HomeScreenState::self()->viewTopPadding(); 0414 const qreal leftMargin = HomeScreenState::self()->viewLeftPadding(); 0415 const qreal panelOffset = isLocationBottom ? leftMargin : topMargin; 0416 0417 return (pageLength / 2) - (((qreal)length) / 2) * cellLength + panelOffset; 0418 } 0419 0420 int FavouritesModel::adjustIndex(int index) const 0421 { 0422 if (HomeScreenState::self()->favouritesBarLocation() == HomeScreenState::Bottom 0423 || HomeScreenState::self()->favouritesBarLocation() == HomeScreenState::Left) { 0424 return index; 0425 } else { 0426 // if it's on the right side of the screen, we flip the order of the delegates 0427 return qMax(0, m_delegates.size() - index - 1); 0428 } 0429 }