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 }