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

0001 /*
0002  * SPDX-FileCopyrightText: 2020 Arjen Hiemstra <ahiemstra@heimr.nl>
0003  *
0004  * SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
0005  */
0006 
0007 #include "PagesModel.h"
0008 
0009 #include <QDebug>
0010 #include <QDir>
0011 #include <QStandardPaths>
0012 
0013 #include <KConfig>
0014 #include <KConfigGroup>
0015 #include <KNSCore/EntryWrapper>
0016 
0017 #include "PageDataObject.h"
0018 
0019 PagesModel::PagesModel(QObject *parent)
0020     : QAbstractListModel(parent)
0021 {
0022 }
0023 
0024 QHash<int, QByteArray> PagesModel::roleNames() const
0025 {
0026     static QHash<int, QByteArray> roles{{TitleRole, "title"},
0027                                         {DataRole, "data"},
0028                                         {IconRole, "icon"},
0029                                         {FileNameRole, "fileName"},
0030                                         {HiddenRole, "hidden"},
0031                                         {FilesWriteableRole, "filesWriteable"}};
0032     return roles;
0033 }
0034 
0035 int PagesModel::rowCount(const QModelIndex &parent) const
0036 {
0037     if (parent.isValid()) {
0038         return 0;
0039     }
0040     return m_pages.count();
0041 }
0042 
0043 QVariant PagesModel::data(const QModelIndex &index, int role) const
0044 {
0045     if (!checkIndex(index, CheckIndexOption::IndexIsValid | CheckIndexOption::DoNotUseParent)) {
0046         return QVariant{};
0047     }
0048 
0049     auto data = m_pages.at(index.row());
0050     switch (role) {
0051     case TitleRole:
0052         return data->value(QStringLiteral("title"));
0053     case DataRole:
0054         return QVariant::fromValue(data);
0055     case IconRole:
0056         return data->value(QStringLiteral("icon"));
0057     case FileNameRole:
0058         return data->fileName();
0059     case HiddenRole:
0060         return m_hiddenPages.contains(data->fileName());
0061     case FilesWriteableRole:
0062         return m_writeableCache[data->fileName()];
0063     default:
0064         return QVariant{};
0065     }
0066 }
0067 
0068 void PagesModel::classBegin()
0069 {
0070 }
0071 
0072 void PagesModel::componentComplete()
0073 {
0074     QHash<QString, QString> files;
0075 
0076     const auto directories = QStandardPaths::standardLocations(QStandardPaths::AppDataLocation);
0077     for (const auto &directory : directories) {
0078         QDir dir{directory};
0079         const auto entries = dir.entryInfoList({QStringLiteral("*.page")}, QDir::NoDotAndDotDot | QDir::Files);
0080         for (const auto &entry : entries) {
0081             const FilesWriteableStates entryState = entry.isWritable() ? AllWriteable : NotWriteable;
0082             if (!files.contains(entry.fileName())) {
0083                 files.insert(entry.fileName(), dir.relativeFilePath(entry.fileName()));
0084                 m_writeableCache.insert(entry.fileName(), entryState);
0085             } else {
0086                 FilesWriteableStates &state = m_writeableCache[entry.fileName()];
0087                 if ((state == NotWriteable && entryState == AllWriteable) || (state == AllWriteable && entryState == NotWriteable)) {
0088                     state = LocalChanges;
0089                 }
0090             }
0091         }
0092     }
0093 
0094     beginResetModel();
0095     for (auto itr = files.begin(); itr != files.end(); ++itr) {
0096         auto config = KSharedConfig::openConfig(itr.value(), KConfig::CascadeConfig, QStandardPaths::AppDataLocation);
0097 
0098         auto page = new PageDataObject{config, this};
0099         page->load(*config, QStringLiteral("page"));
0100 
0101         connect(page, &PageDataObject::saved, this, [this, page]() {
0102             if (m_writeableCache[page->fileName()] == NotWriteable) {
0103                 m_writeableCache[page->fileName()] = LocalChanges;
0104                 auto i = m_pages.indexOf(page);
0105                 Q_EMIT dataChanged(index(i), index(i), {FilesWriteableRole});
0106             }
0107         });
0108         connect(page, &PageDataObject::valueChanged, this, [this, page] {
0109             auto i = m_pages.indexOf(page);
0110             Q_EMIT dataChanged(index(i), index(i), {TitleRole, IconRole});
0111         });
0112         connect(page, &PageDataObject::loaded, this, [this, page] {
0113             auto i = m_pages.indexOf(page);
0114             Q_EMIT dataChanged(index(i), index(i), {TitleRole, IconRole});
0115         });
0116         m_pages.append(page);
0117     }
0118     sort();
0119     endResetModel();
0120 }
0121 
0122 void PagesModel::sort(int column, Qt::SortOrder order)
0123 {
0124     Q_UNUSED(column)
0125     Q_UNUSED(order)
0126 
0127     if (m_pageOrder.isEmpty()) {
0128         return;
0129     }
0130 
0131     Q_EMIT layoutAboutToBeChanged({QPersistentModelIndex()}, QAbstractItemModel::VerticalSortHint);
0132     auto last = std::stable_partition(m_pages.begin(), m_pages.end(), [this](PageDataObject *page) {
0133         return m_pageOrder.contains(page->fileName());
0134     });
0135     std::sort(m_pages.begin(), last, [this](PageDataObject *left, PageDataObject *right) {
0136         return m_pageOrder.indexOf(left->fileName()) < m_pageOrder.indexOf(right->fileName());
0137     });
0138     std::transform(last, m_pages.end(), std::back_inserter(m_pageOrder), [](PageDataObject *page) {
0139         return page->fileName();
0140     });
0141     if (last != m_pages.end()) {
0142         Q_EMIT pageOrderChanged();
0143     }
0144     changePersistentIndex(QModelIndex(), QModelIndex());
0145     Q_EMIT layoutChanged();
0146 }
0147 
0148 PageDataObject *PagesModel::addPage(const QString &baseName, const QVariantMap &properties)
0149 {
0150     int counter = 0;
0151     const QString extension = QStringLiteral(".page");
0152     QString fileName = baseName + extension;
0153     while (m_writeableCache.contains(fileName)) {
0154         fileName = baseName + QString::number(++counter) + extension;
0155     }
0156 
0157     KSharedConfig::Ptr config = KSharedConfig::openConfig(fileName, KConfig::CascadeConfig, QStandardPaths::AppDataLocation);
0158     auto page = new PageDataObject(config, this);
0159     page->load(*config, QStringLiteral("page"));
0160 
0161     for (auto itr = properties.begin(); itr != properties.end(); ++itr) {
0162         page->insert(itr.key(), itr.value());
0163     }
0164     m_writeableCache[fileName] = AllWriteable;
0165 
0166     connect(page, &PageDataObject::valueChanged, this, [this, page]() {
0167         auto i = m_pages.indexOf(page);
0168         Q_EMIT dataChanged(index(i), index(i), {TitleRole, IconRole, FilesWriteableRole});
0169     });
0170 
0171     beginInsertRows(QModelIndex{}, m_pages.size(), m_pages.size());
0172     m_pages.append(page);
0173     m_pageOrder.append(fileName);
0174     Q_EMIT pageOrderChanged();
0175     endInsertRows();
0176 
0177     connect(page, &PageDataObject::loaded, this, [this, page] {
0178         auto i = m_pages.indexOf(page);
0179         Q_EMIT dataChanged(index(i), index(i), {TitleRole, IconRole});
0180     });
0181 
0182     return page;
0183 }
0184 
0185 QStringList PagesModel::pageOrder() const
0186 {
0187     return m_pageOrder;
0188 }
0189 
0190 void PagesModel::setPageOrder(const QStringList &pageOrder)
0191 {
0192     if (pageOrder != m_pageOrder) {
0193         m_pageOrder = pageOrder;
0194         Q_EMIT pageOrderChanged();
0195         sort();
0196     }
0197 }
0198 
0199 QStringList PagesModel::hiddenPages() const
0200 {
0201     return m_hiddenPages;
0202 }
0203 
0204 void PagesModel::setHiddenPages(const QStringList &hiddenPages)
0205 {
0206     if (hiddenPages != m_hiddenPages) {
0207         m_hiddenPages = hiddenPages;
0208         Q_EMIT hiddenPagesChanged();
0209         Q_EMIT dataChanged(index(0, 0), index(m_pages.count() - 1, 0), {HiddenRole});
0210     }
0211 }
0212 
0213 PageDataObject *PagesModel::importPage(const QUrl &file)
0214 {
0215     const QString fileName = file.fileName();
0216     if (!fileName.endsWith(QLatin1String(".page"))) {
0217         return nullptr;
0218     }
0219     // Let addPage first figure out the file name to avoid duplicates and then reload the page after we placed the file
0220     auto page = addPage(fileName);
0221     const auto destination = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + QChar('/') + page->fileName();
0222     QFile::copy(file.toLocalFile(), destination);
0223     page->resetPage();
0224     return page;
0225 }
0226 
0227 void PagesModel::removeLocalPageFiles(const QString &fileName)
0228 {
0229     auto it = std::find_if(m_pages.begin(), m_pages.end(), [&](PageDataObject *page) {
0230         return page->fileName() == fileName;
0231     });
0232     if (it == m_pages.end()) {
0233         return;
0234     }
0235     if (m_writeableCache[fileName] == NotWriteable) {
0236         return;
0237     }
0238     PageDataObject *const page = *it;
0239     const QStringList files = QStandardPaths::locateAll(QStandardPaths::AppDataLocation, page->fileName(), QStandardPaths::LocateFile);
0240     for (const auto &file : files) {
0241         if (QFileInfo(file).isWritable()) {
0242             QFile::remove(file);
0243         }
0244     }
0245     int row = it - m_pages.begin();
0246     if (m_writeableCache[fileName] == AllWriteable) {
0247         beginRemoveRows(QModelIndex(), row, row);
0248         m_pages.erase(it);
0249         m_hiddenPages.removeAll(fileName);
0250         m_writeableCache.remove(fileName);
0251         endRemoveRows();
0252     } else {
0253         page->resetPage();
0254         m_writeableCache[fileName] = NotWriteable;
0255         Q_EMIT dataChanged(index(row, 0), index(row, 0), {TitleRole, IconRole, DataRole, FilesWriteableRole});
0256     }
0257 }
0258 
0259 void PagesModel::ghnsEntriesChanged(const QQmlListReference &changedEntries)
0260 {
0261     for (int i = 0; i < changedEntries.count(); ++i) {
0262         ghnsEntryStatusChanged(changedEntries.at(i));
0263     }
0264 }
0265 
0266 void PagesModel::ghnsEntryStatusChanged(QObject *entry)
0267 {
0268     const auto wrapper = qobject_cast<KNSCore::EntryWrapper *>(entry);
0269     if (!wrapper) {
0270         return;
0271     }
0272 
0273     if (wrapper->entry().status() == KNS3::Entry::Installed) {
0274         const auto files = wrapper->entry().installedFiles();
0275         for (const auto &file : files) {
0276             const QString fileName = QUrl::fromLocalFile(file).fileName();
0277             if (fileName.endsWith(QLatin1String(".page"))) {
0278                 auto it = std::find_if(m_pages.begin(), m_pages.end(), [&](PageDataObject *page) {
0279                     return page->fileName() == fileName;
0280                 });
0281                 if (it != m_pages.end()) {
0282                     // User selected to overwrite the existing file in the kns dialog
0283                     (*it)->resetPage();
0284                     if (m_writeableCache[fileName] == NotWriteable) {
0285                         m_writeableCache[fileName] = LocalChanges;
0286                     }
0287                 } else {
0288                     addPage(fileName.chopped(strlen(".page")));
0289                 }
0290             }
0291         }
0292     } else if (wrapper->entry().status() == KNS3::Entry::Deleted) {
0293         const auto files = wrapper->entry().uninstalledFiles();
0294         for (const auto &file : files) {
0295             const QString fileName = QUrl::fromLocalFile(file).fileName();
0296             if (fileName.endsWith(QLatin1String(".page"))) {
0297                 removeLocalPageFiles(fileName);
0298             }
0299         }
0300     }
0301 }