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 ¬ePath) 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 ¬eName) 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 }