File indexing completed on 2024-12-22 04:48:19

0001 // SPDX-FileCopyrightText: 2023 Carl Schwan <carl@carlschwan.eu>
0002 // SPDX-License-Identifier: LGPL-2.0-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
0003 
0004 #include "noteTreeModel.h"
0005 #include "../kleverUtility.h"
0006 #include <KIO/CopyJob>
0007 #include <QDir>
0008 #include <QIcon>
0009 #include <klocalizedstring.h>
0010 
0011 TreeItem::TreeItem(const QString &path, const int depth_level, NoteTreeModel *model, TreeItem *parentItem)
0012     : m_parentItem(parentItem)
0013     , m_model(model)
0014     , m_path(path)
0015     , m_depth_level(depth_level)
0016 {
0017     const QFileInfo fileInfo(path);
0018     Q_ASSERT(fileInfo.exists());
0019 
0020     const QString fileName = fileInfo.fileName();
0021     m_displayName = fileName == QStringLiteral(".BaseCategory") ? KleverConfig::categoryDisplayName() : fileName;
0022 
0023     if (depth_level < 3) {
0024         const QFileInfoList fileList = QDir(path).entryInfoList(QDir::Filter::NoDotAndDotDot | QDir::Filter::AllEntries | QDir::Filter::AccessMask);
0025 
0026         for (const QFileInfo &file : fileList) {
0027             const QString name = file.fileName();
0028 
0029             const bool isNotBaseFolder = (name != QStringLiteral(".BaseCategory") && name != QStringLiteral(".BaseGroup"));
0030 
0031             if (!file.isDir() || (name.startsWith(QStringLiteral(".")) && isNotBaseFolder)) {
0032                 continue;
0033             }
0034 
0035             auto subTree = std::make_unique<TreeItem>(file.absoluteFilePath(), m_depth_level + 1, m_model, this);
0036 
0037             if (name == QStringLiteral(".BaseGroup")) {
0038                 // Notes inside ".BaseGroup" folder should be shown as being held by the category directly, not by a group
0039                 // Move all the subTree child to the parent of the subTree
0040                 for (int i = subTree->childCount() - 1; i >= 0; i--) {
0041                     auto categoryNote = subTree->uniqueChildAt(i);
0042 
0043                     categoryNote->m_parentItem = this;
0044 
0045                     appendChild(std::move(categoryNote));
0046                 }
0047                 // Delete the now useless subtree
0048                 subTree.reset(nullptr);
0049             } else {
0050                 appendChild(std::move(subTree));
0051             }
0052         }
0053     }
0054 }
0055 
0056 void TreeItem::appendChild(std::unique_ptr<TreeItem> &&item)
0057 {
0058     if (item->m_depth_level == 3 && m_model->noteMapEnabled()) {
0059         // very important to make a copy here !
0060         const QString path = QString(item->m_path).remove(KleverConfig::storagePath());
0061         if (m_model->isInit()) {
0062             Q_EMIT m_model->newGlobalPathFound(path);
0063         } else {
0064             m_model->addInitialGlobalPath(path);
0065         }
0066     }
0067     m_childItems.push_back(std::move(item));
0068 }
0069 
0070 TreeItem *TreeItem::child(int row) const
0071 {
0072     if (row < 0 || row >= static_cast<int>(m_childItems.size())) {
0073         return nullptr;
0074     }
0075     return m_childItems.at(row).get();
0076 }
0077 
0078 std::unique_ptr<TreeItem> TreeItem::uniqueChildAt(int row)
0079 {
0080     if (row < 0 || row >= static_cast<int>(m_childItems.size())) {
0081         return nullptr;
0082     }
0083 
0084     return std::move(m_childItems.at(row));
0085 }
0086 
0087 int TreeItem::childCount() const
0088 {
0089     return m_childItems.size();
0090 }
0091 
0092 int TreeItem::row() const
0093 {
0094     if (m_parentItem) {
0095         const auto it = std::find_if(m_parentItem->m_childItems.cbegin(), m_parentItem->m_childItems.cend(), [this](const std::unique_ptr<TreeItem> &treeItem) {
0096             return treeItem.get() == const_cast<TreeItem *>(this);
0097         });
0098 
0099         Q_ASSERT(it != m_parentItem->m_childItems.cend());
0100 
0101         return std::distance(m_parentItem->m_childItems.cbegin(), it);
0102     }
0103 
0104     return 0;
0105 }
0106 
0107 QVariant TreeItem::data(int role) const
0108 {
0109     switch (role) {
0110     case NoteTreeModel::PathRole:
0111         return m_path;
0112 
0113     case Qt::DisplayRole:
0114     case NoteTreeModel::DisplayNameRole:
0115         return m_displayName;
0116 
0117     case Qt::DecorationRole:
0118         return QIcon::fromTheme(QStringLiteral("document-edit-sign"));
0119 
0120     case NoteTreeModel::IconNameRole:
0121         switch (m_depth_level) {
0122         case 1:
0123             return QStringLiteral("documentation");
0124         case 2:
0125             return QStringLiteral("document-open-folder");
0126         case 3:
0127             return QStringLiteral("document-edit-sign");
0128         default:
0129             Q_UNREACHABLE();
0130         }
0131 
0132     case NoteTreeModel::UseCaseRole:
0133         switch (m_depth_level) {
0134         case 1:
0135             return QStringLiteral("Category");
0136         case 2:
0137             return QStringLiteral("Group");
0138         case 3:
0139             return QStringLiteral("Note");
0140         default:
0141             Q_UNREACHABLE();
0142         }
0143 
0144     case NoteTreeModel::NoteNameRole:
0145         if (m_depth_level != 3)
0146             return QLatin1String();
0147         return data(NoteTreeModel::DisplayNameRole);
0148 
0149     case NoteTreeModel::BranchNameRole:
0150         if (m_depth_level != 3)
0151             return QLatin1String();
0152         else { //The switch statement is not happy without this else...
0153             const QString parentName = m_parentItem->data(NoteTreeModel::DisplayNameRole).toString();
0154             if (m_parentItem->data(NoteTreeModel::UseCaseRole).toString() != QStringLiteral("Group")) {
0155                 return parentName;
0156             }
0157             const QString grandParentName = m_parentItem->parentItem()->data(NoteTreeModel::DisplayNameRole).toString();
0158             const QString finalValue = grandParentName + QStringLiteral("→") + parentName;
0159             return finalValue;
0160         }
0161 
0162     case NoteTreeModel::FullNameRole: {
0163         const QString returnValue = data(NoteTreeModel::BranchNameRole).toString() + QStringLiteral(": ") + data(NoteTreeModel::NoteNameRole).toString();
0164         return returnValue;
0165     }
0166 
0167     case NoteTreeModel::WantFocusRole:
0168         return m_wantFocus;
0169 
0170     case NoteTreeModel::WantExpandRole:
0171         return m_wantExpand;
0172 
0173     default:
0174         Q_UNREACHABLE();
0175     }
0176 };
0177 
0178 TreeItem *TreeItem::parentItem() const
0179 {
0180     return m_parentItem;
0181 }
0182 
0183 void TreeItem::remove()
0184 {
0185     Q_ASSERT(m_parentItem);
0186 
0187     if (m_model->noteMapEnabled())
0188         Q_EMIT m_model->globalPathRemoved(m_path);
0189 
0190     const auto it = std::find_if(m_parentItem->m_childItems.cbegin(), m_parentItem->m_childItems.cend(), [this](const std::unique_ptr<TreeItem> &treeItem) {
0191         return treeItem.get() == const_cast<TreeItem *>(this);
0192     });
0193     m_parentItem->m_childItems.erase(it);
0194 }
0195 
0196 void TreeItem::changePath(const QString &newPart, const QModelIndex &parentModelIndex, int newPartIdx)
0197 {
0198     QStringList currentPathParts = m_path.split(QStringLiteral("/"));
0199     if (currentPathParts.last() == QStringLiteral(".BaseCategory"))
0200         return;
0201 
0202     if (newPartIdx == -1)
0203         newPartIdx = currentPathParts.size() - 1;
0204     currentPathParts[newPartIdx] = newPart;
0205 
0206     QString newPath = currentPathParts.join(QStringLiteral("/"));
0207     if (m_depth_level == 3 && m_model->noteMapEnabled()) {
0208         Q_EMIT m_model->globalPathUpdated(m_path, newPath);
0209     }
0210     m_path = newPath;
0211 
0212     // By default we assume that we are in the first call
0213     // So the parentModelIndex is actually the model index of this object
0214     QModelIndex thisModelIndex = parentModelIndex;
0215 
0216     // We have the parent model index and not the model index of this object
0217     if (static_cast<TreeItem *>(parentModelIndex.internalPointer()) != this) {
0218         thisModelIndex = m_model->index(this->row(), 0, thisModelIndex);
0219     }
0220     Q_EMIT m_model->dataChanged(thisModelIndex, thisModelIndex);
0221 
0222     for (const std::unique_ptr<TreeItem> &child : m_childItems) {
0223         child->changePath(newPart, thisModelIndex, newPartIdx);
0224     }
0225 }
0226 
0227 void TreeItem::changeDisplayName(const QString &name)
0228 {
0229     m_displayName = name;
0230 }
0231 
0232 void TreeItem::askForFocus(const QModelIndex &itemIndex)
0233 {
0234     // We just want to send a signal to QML
0235     m_wantFocus = true;
0236     Q_EMIT m_model->dataChanged(itemIndex, itemIndex);
0237     m_wantFocus = false;
0238     Q_EMIT m_model->dataChanged(itemIndex, itemIndex);
0239 }
0240 
0241 void TreeItem::askForExpand(const QModelIndex &itemIndex)
0242 {
0243     // We just want to send a signal to QML
0244     m_wantExpand = true;
0245     Q_EMIT m_model->dataChanged(itemIndex, itemIndex);
0246     m_wantExpand = false;
0247     Q_EMIT m_model->dataChanged(itemIndex, itemIndex);
0248 }
0249 
0250 NoteTreeModel::NoteTreeModel(QObject *parent)
0251     : QAbstractItemModel(parent)
0252 {
0253 }
0254 
0255 void NoteTreeModel::initModel()
0256 {
0257     if (KleverConfig::storagePath().isEmpty() || !KleverConfig::storagePath().toLower().endsWith(QStringLiteral("klevernotes"))) {
0258         return;
0259     }
0260 
0261     if (!KleverUtility::exists(KleverConfig::storagePath())) {
0262         const bool storageCreated = makeStorage(KleverConfig::storagePath());
0263         if (!storageCreated) {
0264             m_rootItem = nullptr;
0265             return;
0266         }
0267 
0268         // This normally won't happen, but who knows
0269         const QString basePath = KleverConfig::storagePath().append(QStringLiteral("/.BaseCategory/.BaseGroup"));
0270         bool groupCreated = KleverUtility::exists(basePath);
0271         if (!groupCreated) {
0272             const QString categoryPath = KleverConfig::storagePath().append(QStringLiteral("/.BaseCategory"));
0273             groupCreated = makeGroup(categoryPath, QStringLiteral(".BaseGroup"));
0274             if (!groupCreated) {
0275                 m_rootItem = nullptr;
0276                 return;
0277             }
0278         }
0279         const bool initDemoNote = makeNote(basePath, QStringLiteral("Demo"));
0280         if (!initDemoNote) {
0281             Q_EMIT errorOccurred(i18n("An error occurred while trying to create the demo note."));
0282         } else {
0283             const QString notePath = basePath + QStringLiteral("/Demo/");
0284 
0285             const QString mdPath = notePath + QStringLiteral("note.md");
0286             QFile::remove(mdPath);
0287             QFile::copy(QStringLiteral(":/demo_note.md"), mdPath);
0288 
0289             QFile demoNote(mdPath);
0290             demoNote.setPermissions(QFile::ReadOwner|QFile::WriteOwner|
0291                                     QFile::ReadUser|QFile::WriteUser|
0292                                     QFile::ReadGroup|QFile::WriteGroup|
0293                                     QFile::ReadOther|QFile::WriteOther);
0294 
0295             const QString imagePath = notePath + QStringLiteral("Images/");
0296             QDir().mkpath(imagePath);
0297             QFile::copy(QStringLiteral(":/Images/logo.png"), imagePath + QStringLiteral("logo.png"));
0298         }
0299     }
0300 
0301     beginResetModel();
0302     m_rootItem = std::make_unique<TreeItem>(KleverConfig::storagePath(), 0, this);
0303     endResetModel();
0304 
0305     if (m_noteMapEnabled) {
0306         m_isInit = true;
0307         Q_EMIT initialGlobalPathsSent(m_initialGlobalPaths);
0308     }
0309 }
0310 
0311 
0312 QModelIndex NoteTreeModel::index(int row, int column, const QModelIndex &parent) const
0313 {
0314     if (!hasIndex(row, column, parent)) {
0315         return {};
0316     }
0317 
0318     TreeItem *parentItem;
0319 
0320     if (!parent.isValid()) {
0321         parentItem = m_rootItem.get();
0322     } else {
0323         parentItem = static_cast<TreeItem *>(parent.internalPointer());
0324     }
0325 
0326     TreeItem *childItem = parentItem->child(row);
0327     if (childItem) {
0328         return createIndex(row, column, childItem);
0329     }
0330     return {};
0331 }
0332 
0333 QHash<int, QByteArray> NoteTreeModel::roleNames() const
0334 {
0335     return {
0336         {DisplayNameRole, "displayName"},
0337         {PathRole, "path"},
0338         {IconNameRole, "iconName"},
0339         {UseCaseRole, "useCase"},
0340         {NoteNameRole, "noteName"},
0341         {BranchNameRole, "branchName"},
0342         {FullNameRole, "fullName"},
0343         {WantFocusRole, "wantFocus"},
0344         {WantExpandRole, "wantExpand"},
0345     };
0346 }
0347 
0348 QModelIndex NoteTreeModel::parent(const QModelIndex &index) const
0349 {
0350     if (!index.isValid()) {
0351         return {};
0352     }
0353 
0354     const auto childItem = static_cast<TreeItem *>(index.internalPointer());
0355     const auto parentItem = childItem->parentItem();
0356 
0357     if (parentItem == m_rootItem.get()) {
0358         return {};
0359     }
0360 
0361     return createIndex(parentItem->row(), 0, parentItem);
0362 }
0363 
0364 int NoteTreeModel::rowCount(const QModelIndex &parent) const
0365 {
0366     TreeItem *parentItem;
0367     if (parent.column() > 0) {
0368         return 0;
0369     }
0370 
0371     if (!parent.isValid()) {
0372         parentItem = m_rootItem.get();
0373     } else {
0374         parentItem = static_cast<TreeItem *>(parent.internalPointer());
0375     }
0376 
0377     if (m_rootItem == nullptr) {
0378         return 0;
0379     }
0380 
0381     return parentItem->childCount();
0382 }
0383 
0384 int NoteTreeModel::columnCount(const QModelIndex &parent) const
0385 {
0386     Q_UNUSED(parent)
0387     return 1;
0388 }
0389 
0390 QVariant NoteTreeModel::data(const QModelIndex &index, int role) const
0391 {
0392     if (!index.isValid()) {
0393         return {};
0394     }
0395 
0396     const auto item = static_cast<TreeItem *>(index.internalPointer());
0397 
0398     return item->data(role);
0399 }
0400 
0401 void NoteTreeModel::addRow(const QString &rowName, const QString &parentPath, const int rowLevel, const QModelIndex &parentModelIndex)
0402 {
0403     const auto parentRow = !parentModelIndex.isValid() ? m_rootItem.get() : static_cast<TreeItem *>(parentModelIndex.internalPointer());
0404 
0405     bool rowCreated;
0406     switch (rowLevel) {
0407         case 1:
0408             rowCreated = makeCategory(parentPath, rowName);
0409             break;
0410         case 2:
0411             rowCreated = makeGroup(parentPath, rowName);
0412             break;
0413         case 3:
0414             rowCreated = makeNote(parentPath, rowName);
0415             break;
0416         default:
0417             Q_EMIT errorOccurred(i18n("An error occurred while trying to create this item."));
0418             rowCreated = false;
0419             break;
0420     }
0421     if (!rowCreated) return;
0422 
0423     const QString rowPath = parentPath + QLatin1Char('/') + rowName;
0424     auto newRow = std::make_unique<TreeItem>(rowPath, rowLevel, this, parentRow);
0425 
0426     beginInsertRows(parentModelIndex, parentRow->childCount(), parentRow->childCount());
0427     parentRow->appendChild(std::move(newRow));
0428     endInsertRows();
0429 }
0430 
0431 void NoteTreeModel::removeFromTree(const QModelIndex &index)
0432 {
0433     auto row = static_cast<TreeItem *>(index.internalPointer());
0434     const QString rowPath = row->data(PathRole).toString();
0435 
0436     if (row->childCount() > 0) { // Prevent KDescendantsProxyModel from crashing
0437             row->askForExpand(index);
0438     }
0439 
0440     auto *job = KIO::trash(QUrl::fromLocalFile(rowPath));
0441     job->start();
0442 
0443     connect(job, &KJob::result, this, [job, row, index, this] {
0444         if (!job->error()) {
0445             beginRemoveRows(parent(index), index.row(), index.row());
0446             row->remove();
0447             endRemoveRows();
0448             return;
0449         }
0450         Q_EMIT errorOccurred(i18n("An error occurred while trying to remove this item."));
0451         qWarning() << job->errorString();
0452     });
0453 }
0454 
0455 void NoteTreeModel::rename(const QModelIndex &rowModelIndex, const QString &newName)
0456 {
0457     const auto row = static_cast<TreeItem *>(rowModelIndex.internalPointer());
0458 
0459     const QString rowPath = row->data(PathRole).toString();
0460 
0461     if (rowPath.endsWith(QStringLiteral(".BaseCategory"))) {
0462         KleverConfig::setCategoryDisplayName(newName);
0463     } else {
0464         QDir dir(rowPath);
0465         dir.cdUp();
0466 
0467         const QString newPath = dir.absolutePath() + QLatin1Char('/') + newName;
0468 
0469         const bool renamed = QDir().rename(rowPath, newPath);
0470 
0471         if (!renamed) {
0472             Q_EMIT errorOccurred(i18n("An error occurred while trying to rename this item."));
0473             return;
0474         }
0475     }
0476 
0477     row->changeDisplayName(newName);
0478     row->changePath(newName, rowModelIndex);
0479 }
0480 
0481 void NoteTreeModel::askForFocus(const QModelIndex& rowModelIndex)
0482 {
0483     const auto row = static_cast<TreeItem *>(rowModelIndex.internalPointer());
0484     row->askForFocus(rowModelIndex);
0485 }
0486 
0487 void NoteTreeModel::askForExpand(const QModelIndex& rowModelIndex)
0488 {
0489     const auto row = static_cast<TreeItem *>(rowModelIndex.internalPointer());
0490     row->askForExpand(rowModelIndex);
0491 }
0492 
0493 QModelIndex NoteTreeModel::getNoteModelIndex(const QString &notePath)
0494 {
0495     QStringList currentPathParts = notePath.split(QStringLiteral("/"));
0496     currentPathParts.pop_front(); // remove the first empty string
0497     QString currentPathPart = currentPathParts.takeAt(0);
0498 
0499     auto currentParentItem = m_rootItem.get();
0500     QModelIndex currentModelIndex;
0501 
0502     bool hasBreak = false;
0503     for (int i = 0; i < currentParentItem->childCount();) {
0504         const auto currentItem = currentParentItem->child(i);
0505         const QString currentItemPath = currentItem->data(PathRole).toString();
0506         if (currentItemPath.endsWith(currentPathPart)) {
0507             currentModelIndex = createIndex(i, 0, currentItem);
0508             if (currentPathParts.isEmpty()) {
0509                 hasBreak = true;
0510                 break;
0511             } else {
0512                 currentPathPart = currentPathParts.takeAt(0);
0513 
0514                 if (currentPathPart == QStringLiteral(".BaseGroup"))
0515                     currentPathPart = currentPathParts.takeAt(0);
0516             }
0517             currentParentItem = currentItem;
0518             i = 0;
0519             continue;
0520         }
0521         i++;
0522     }
0523 
0524     return hasBreak ? currentModelIndex : QModelIndex(); // Easier to handle in qml
0525 }
0526 
0527 // NoteMapper
0528 void NoteTreeModel::setNoteMapEnabled(const bool noteMapEnabled)
0529 {
0530     m_noteMapEnabled = noteMapEnabled;
0531 }
0532 
0533 bool NoteTreeModel::noteMapEnabled()
0534 {
0535     return m_noteMapEnabled;
0536 }
0537 
0538 bool NoteTreeModel::isInit()
0539 {
0540     return m_isInit;
0541 }
0542 
0543 void NoteTreeModel::addInitialGlobalPath(const QString &path)
0544 {
0545     m_initialGlobalPaths.append(path);
0546 }
0547 
0548 // Storage Handler
0549 bool NoteTreeModel::makeNote(const QString &groupPath, const QString &noteName)
0550 {
0551     const QString notePath = groupPath + QLatin1Char('/') + noteName;
0552 
0553     QFile note(notePath + QStringLiteral("/note.md"));
0554     QFile todo(notePath + QStringLiteral("/todo.json"));
0555 
0556     const bool noteFolderCreated = KleverUtility::create(notePath);
0557     bool creationSucces;
0558     if (noteFolderCreated) {
0559         creationSucces = note.open(QIODevice::ReadWrite);
0560         note.close();
0561         if (creationSucces) {
0562             creationSucces = todo.open(QIODevice::ReadWrite);
0563             todo.close();
0564         }
0565     }
0566     if (!noteFolderCreated || !creationSucces)
0567         Q_EMIT errorOccurred(i18n("An error occurred while trying to create the note."));
0568 
0569     return (noteFolderCreated && creationSucces);
0570 }
0571 
0572 bool NoteTreeModel::makeGroup(const QString &categoryPath, const QString &groupName)
0573 {
0574     const QString groupPath = categoryPath + QLatin1Char('/') + groupName;
0575 
0576     const bool groupCreated = KleverUtility::create(groupPath);
0577     if (!groupCreated)
0578         Q_EMIT errorOccurred(i18n("An error occurred while trying to create the group."));
0579     return groupCreated;
0580 }
0581 
0582 bool NoteTreeModel::makeCategory(const QString &storagePath, const QString &categoryName)
0583 {
0584     const QString categoryPath = storagePath + QLatin1Char('/') + categoryName;
0585 
0586     const bool groupCreated = makeGroup(categoryPath, QStringLiteral(".BaseGroup"));
0587     if (!groupCreated)
0588         Q_EMIT errorOccurred(i18n("An error occurred while trying to create the category."));
0589     return groupCreated;
0590 }
0591 
0592 bool NoteTreeModel::makeStorage(const QString &storagePath)
0593 {
0594     const bool categoryCreated = makeCategory(storagePath, QStringLiteral("/.BaseCategory"));
0595     if (!categoryCreated)
0596         Q_EMIT errorOccurred(i18n("An error occurred while trying to create the storage."));
0597     return categoryCreated;
0598 }