File indexing completed on 2024-05-19 05:42:15
0001 // ct_lvtprj_project_file.h -*-C++-*- 0002 0003 /* 0004 // Copyright 2023 Codethink Ltd <codethink@codethink.co.uk> 0005 // SPDX-License-Identifier: Apache-2.0 0006 // 0007 // Licensed under the Apache License, Version 2.0 (the "License"); 0008 // you may not use this file except in compliance with the License. 0009 // You may obtain a copy of the License at 0010 // 0011 // http://www.apache.org/licenses/LICENSE-2.0 0012 // 0013 // Unless required by applicable law or agreed to in writing, software 0014 // distributed under the License is distributed on an "AS IS" BASIS, 0015 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 0016 // See the License for the specific language governing permissions and 0017 // limitations under the License. 0018 */ 0019 0020 #include <ct_lvtmdb_soci_writer.h> 0021 #include <ct_lvtprj_projectfile.h> 0022 #include <ct_lvtshr_stringhelpers.h> 0023 0024 #include <KZip> 0025 0026 // Std 0027 #include <algorithm> 0028 #include <iostream> 0029 #include <random> 0030 #include <string_view> 0031 0032 // Qt, Json 0033 #include <QCoreApplication> 0034 #include <QDir> 0035 #include <QFile> 0036 #include <QJsonDocument> 0037 #include <QJsonObject> 0038 #include <QStandardPaths> 0039 #include <QTimer> 0040 0041 using namespace Codethink::lvtprj; 0042 namespace fs = std::filesystem; 0043 0044 namespace { 0045 constexpr std::string_view CODE_DB = "code_database.db"; 0046 constexpr std::string_view CAD_DB = "cad_database.db"; 0047 constexpr std::string_view METADATA = "metadata.json"; 0048 constexpr std::string_view LEFT_PANEL_HISTORY = "left_panel"; 0049 constexpr std::string_view RIGHT_PANEL_HISTORY = "right_panel"; 0050 constexpr std::string_view BOOKMARKS_FOLDER = "bookmarks"; 0051 0052 bool compressDir(QFileInfo const& saveTo, QDir const& dirToCompress) 0053 { 0054 if (!QDir{}.exists(saveTo.absolutePath()) && !QDir{}.mkdir(saveTo.absolutePath())) { 0055 qDebug() << "[compressDir] Could not prepare path to save."; 0056 return false; 0057 } 0058 0059 auto zipFile = KZip(saveTo.absoluteFilePath()); 0060 if (!zipFile.open(QIODevice::WriteOnly)) { 0061 qDebug() << "[compressDir] Could not open file to compress:" << saveTo; 0062 qDebug() << zipFile.errorString(); 0063 return false; 0064 } 0065 0066 auto r = zipFile.addLocalDirectory(dirToCompress.path(), ""); 0067 if (!r) { 0068 qDebug() << "[compressDir] Could not add files to project:" << dirToCompress; 0069 qDebug() << zipFile.errorString(); 0070 return false; 0071 } 0072 0073 return true; 0074 } 0075 0076 bool extractDir(QFileInfo const& projectFile, QDir const& openLocation) 0077 { 0078 auto zipFile = KZip(projectFile.absoluteFilePath()); 0079 if (!zipFile.open(QIODevice::ReadOnly)) { 0080 qDebug() << "[compressDir] Could not open file to read contents:" << projectFile; 0081 qDebug() << zipFile.errorString(); 0082 return false; 0083 } 0084 const KArchiveDirectory *dir = zipFile.directory(); 0085 return dir->copyTo(openLocation.path()); 0086 } 0087 0088 // return a random string, for the unique project folder in $temp. 0089 [[nodiscard]] std::string random_string(size_t length) 0090 { 0091 constexpr std::string_view charset = 0092 "0123456789" 0093 "ABCDEFGHIJKLMNOPQRSTUVWXYZ" 0094 "abcdefghijklmnopqrstuvwxyz"; 0095 0096 std::random_device rd; 0097 std::mt19937 mt(rd()); 0098 0099 // doc says that the range is [lower, upper], and not [lower, upper) as I'd expect. 0100 std::uniform_int_distribution<size_t> dist(0, charset.size() - 1); 0101 0102 std::string str(length, 0); 0103 std::generate_n(str.begin(), length, [&dist, &mt, &charset]() -> char { 0104 return charset[dist(mt)]; 0105 }); 0106 return str; 0107 } 0108 0109 // creates a non-exisitng 16 alphanumeric folder for the project. 0110 [[nodiscard]] std::pair<std::error_code, fs::path> createNonExistingFolder(const std::filesystem::path& tmp) 0111 { 0112 fs::path openLocation; 0113 for (;;) { 0114 openLocation = tmp / random_string(16); 0115 bool openLocationExists = fs::exists(fs::status(openLocation)); 0116 if (openLocationExists) { 0117 continue; 0118 } 0119 0120 std::error_code ec; 0121 fs::create_directories(openLocation, ec); 0122 return {ec, openLocation}; 0123 } 0124 0125 assert(false && "unreachable"); 0126 return {std::error_code{}, fs::path{}}; 0127 } 0128 0129 [[nodiscard]] auto createUniqueTempFolder() -> cpp::result<std::filesystem::path, ProjectFileError> 0130 { 0131 std::error_code ec; 0132 const fs::path tmp = fs::temp_directory_path(ec); 0133 if (ec) { 0134 return cpp::fail(ProjectFileError{ec.message()}); 0135 } 0136 0137 auto [ec2, folderName] = createNonExistingFolder(tmp); 0138 if (ec2) { 0139 return cpp::fail(ProjectFileError{ec2.message()}); 0140 } 0141 0142 return folderName; 0143 } 0144 0145 } // namespace 0146 0147 struct ProjectFile::Private { 0148 bool isOpen = false; 0149 // is the project open? 0150 0151 bool isDirty = false; 0152 // are there changes in the project that are not saved? 0153 0154 bool hasCodeDatabase = false; 0155 // do we have the datbase for the code db yet? 0156 0157 bool hasGraphDatabase = false; 0158 // do we have the graph database? (the db that stores the positions and metadata) 0159 0160 bool allowedAutoSave = true; 0161 // Will we autosave? 0162 0163 std::filesystem::path backupLocation; 0164 // location in the disk where to open and save the backup files. 0165 0166 std::filesystem::path location; 0167 // location in disk where to open and save the contents of the project. 0168 0169 std::filesystem::path openLocation; 0170 // where this project is open on disk. this is a unzipped folder in $TEMP 0171 0172 std::filesystem::path sourceCodePath; 0173 // Location to the actual source code for parsed projects 0174 0175 std::string projectName; 0176 // name of the project 0177 0178 std::string projectInformation; 0179 // longer explanation of the project 0180 0181 QTimer projectSaveBackupTimer; 0182 // when the timer is over, we save the backup file. 0183 }; 0184 0185 ProjectFile::ProjectFile(): d(std::make_unique<ProjectFile::Private>()) 0186 { 0187 std::filesystem::path backupF = backupFolder(); 0188 std::filesystem::directory_entry backupDir(backupF); 0189 if (!backupDir.exists()) { 0190 if (!std::filesystem::create_directories(backupDir)) { 0191 std::cerr << "Could not create directories for the project backup."; 0192 std::cerr << "Disabling autosave features"; 0193 d->allowedAutoSave = false; 0194 } 0195 } 0196 } 0197 0198 ProjectFile::~ProjectFile() 0199 { 0200 (void) close(); 0201 } 0202 0203 std::filesystem::path ProjectFile::backupFolder() 0204 { 0205 const QString appName = qApp ? qApp->applicationName() : QStringLiteral("CodeVis"); 0206 const QString docPath = QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation) + QDir::separator() 0207 + appName + QDir::separator() + "Backup"; 0208 return docPath.toStdString(); 0209 } 0210 0211 void ProjectFile::requestAutosave(int msec) 0212 { 0213 if (!d->allowedAutoSave) { 0214 return; 0215 } 0216 0217 // initialize the autosave timer only once. 0218 static bool unused = [this]() -> bool { 0219 d->projectSaveBackupTimer.setSingleShot(true); 0220 connect(&d->projectSaveBackupTimer, &QTimer::timeout, this, [this] { 0221 auto res = saveBackup(); 0222 if (res.has_error()) { 0223 Q_EMIT communicateError(res.error()); 0224 } 0225 }); 0226 return true; 0227 }(); 0228 (void) unused; 0229 0230 if (msec == 0) { 0231 auto res = saveBackup(); 0232 if (res.has_error()) { 0233 Q_EMIT communicateError(res.error()); 0234 } 0235 } else { 0236 d->projectSaveBackupTimer.start(std::chrono::milliseconds{msec}); 0237 } 0238 } 0239 0240 std::filesystem::path ProjectFile::backupPath() const 0241 { 0242 return d->backupLocation; 0243 } 0244 0245 auto ProjectFile::close() -> cpp::result<void, ProjectFileError> 0246 { 0247 if (!isOpen()) { 0248 return {}; // closing a closed project is a noop. 0249 } 0250 0251 try { 0252 if (!fs::remove_all(d->openLocation)) { 0253 return cpp::fail(ProjectFileError{"Error removing temporary folder"}); 0254 } 0255 } catch (std::filesystem::filesystem_error& error) { 0256 return cpp::fail(ProjectFileError{error.what()}); 0257 } 0258 0259 d = std::make_unique<ProjectFile::Private>(); 0260 return {}; 0261 } 0262 0263 bool ProjectFile::isOpen() const 0264 { 0265 return d->isOpen; 0266 } 0267 0268 auto ProjectFile::createEmpty() -> cpp::result<void, ProjectFileError> 0269 { 0270 cpp::result<fs::path, ProjectFileError> tmpFolder = createUniqueTempFolder(); 0271 if (tmpFolder.has_error()) { 0272 return cpp::fail(tmpFolder.error()); 0273 } 0274 0275 d->openLocation = tmpFolder.value(); 0276 d->isOpen = true; 0277 0278 { 0279 lvtmdb::SociWriter writer; 0280 if (!writer.createOrOpen(codeDatabasePath().string(), "codebase_db.sql")) { 0281 return cpp::fail(ProjectFileError{ 0282 "Couldnt create database. Tip: Make sure the database spec folder is installed properly."}); 0283 } 0284 } 0285 { 0286 lvtmdb::SociWriter writer; 0287 if (!writer.createOrOpen(cadDatabasePath().string(), "cad_db.sql")) { 0288 return cpp::fail(ProjectFileError{ 0289 "Couldnt create database. Tip: Make sure the database spec folder is installed properly."}); 0290 } 0291 } 0292 0293 return {}; 0294 } 0295 0296 auto ProjectFile::open(const std::filesystem::path& path) -> cpp::result<void, ProjectFileError> 0297 { 0298 // std::string lacks .endsWith - cpp20 fixes this but we are on 17. 0299 const QString tmp = QString::fromStdString(path.string()); 0300 const std::string filePath = tmp.endsWith(".lks") ? tmp.toStdString() : tmp.toStdString() + ".lks"; 0301 0302 const bool exists = fs::exists(fs::status(filePath)); 0303 if (!exists) { 0304 return cpp::fail(ProjectFileError{"Project file does not exist on disk"}); 0305 } 0306 0307 cpp::result<fs::path, ProjectFileError> tmpFolder = createUniqueTempFolder(); 0308 if (tmpFolder.has_error()) { 0309 return cpp::fail(ProjectFileError{tmpFolder.error()}); 0310 } 0311 0312 d->openLocation = tmpFolder.value(); 0313 const auto projectFile = QFileInfo{QString::fromStdString(filePath)}; 0314 const auto openLocation = QDir{QString::fromStdString(d->openLocation.string())}; 0315 if (!extractDir(projectFile, openLocation)) { 0316 return cpp::fail(ProjectFileError{"Failed to extract project contents"}); 0317 } 0318 0319 d->location = filePath; 0320 d->isOpen = true; 0321 loadProjectMetadata(); 0322 0323 return {}; 0324 } 0325 0326 auto ProjectFile::saveAs(const fs::path& path, BackupFileBehavior behavior) -> cpp::result<void, ProjectFileError> 0327 { 0328 fs::path projectPath = path; 0329 if (!projectPath.string().ends_with(".lks")) { 0330 projectPath += ".lks"; 0331 } 0332 0333 // create a backup file the project already exists. 0334 // if any error happens, the user can revert to the backup. 0335 try { 0336 if (fs::exists(fs::status(projectPath))) { 0337 // fs path has no operator `+`, so I can't initialize correctly. 0338 // but it has operator +=. :| 0339 fs::path bkpPath = projectPath; 0340 bkpPath += ".bpk"; 0341 fs::rename(projectPath, bkpPath); 0342 } 0343 } catch (fs::filesystem_error& error) { 0344 return cpp::fail(ProjectFileError{error.what()}); 0345 } 0346 0347 d->location = projectPath; 0348 0349 auto dumped = dumpProjectMetadata(); 0350 if (dumped.has_error()) { 0351 return cpp::fail(dumped.error()); 0352 } 0353 0354 // save new file. 0355 const auto saveWhat = QDir{QString::fromStdString(d->openLocation.string())}; 0356 const auto saveTo = QFileInfo{QString::fromStdString(projectPath.string())}; 0357 if (!compressDir(saveTo, saveWhat)) { 0358 qDebug() << "Failed to generate project file"; 0359 return cpp::fail(ProjectFileError{"Failed to generate project file"}); 0360 } 0361 0362 // We correctly saved the file, we can delete the auto backup file. 0363 if (behavior == BackupFileBehavior::Discard) { 0364 if (!std::filesystem::remove(d->backupLocation)) { 0365 // error deleting the backup file, but the project file was correctly saved. 0366 // this is not a hard error, just complain on the terminal. 0367 qDebug() << "Error removing backup file"; 0368 } 0369 } 0370 0371 return {}; 0372 } 0373 0374 cpp::result<void, ProjectFileError> ProjectFile::saveBackup() 0375 { 0376 return saveAs(d->backupLocation, BackupFileBehavior::Keep); 0377 } 0378 0379 auto ProjectFile::save() -> cpp::result<void, ProjectFileError> 0380 { 0381 auto ret = saveAs(d->location, BackupFileBehavior::Discard); 0382 if (ret.has_error()) { 0383 return ret; 0384 } 0385 0386 d->isDirty = false; 0387 return ret; 0388 } 0389 0390 void ProjectFile::setProjectName(std::string name) 0391 { 0392 d->projectName = std::move(name); 0393 d->backupLocation = backupFolder().string() + "/" + d->projectName + ".lks"; 0394 } 0395 0396 void ProjectFile::setProjectInformation(std::string projectInfo) 0397 { 0398 d->projectInformation = std::move(projectInfo); 0399 } 0400 0401 fs::path ProjectFile::openLocation() const 0402 { 0403 assert(d->isOpen); 0404 return d->openLocation; 0405 } 0406 0407 fs::path ProjectFile::codeDatabasePath() const 0408 { 0409 assert(d->isOpen); 0410 return d->openLocation / CODE_DB; 0411 } 0412 0413 bool ProjectFile::hasCodeDatabase() const 0414 { 0415 const fs::path codeDb = codeDatabasePath(); 0416 return fs::exists(fs::status(codeDb)); 0417 } 0418 0419 fs::path ProjectFile::cadDatabasePath() const 0420 { 0421 return d->openLocation / CAD_DB; 0422 } 0423 0424 bool ProjectFile::hasCadDatabase() const 0425 { 0426 const fs::path cadDb = cadDatabasePath(); 0427 return fs::exists(fs::status(cadDb)); 0428 } 0429 0430 std::string ProjectFile::projectInformation() const 0431 { 0432 return d->projectInformation; 0433 } 0434 0435 std::string ProjectFile::projectName() const 0436 { 0437 return d->projectName; 0438 } 0439 0440 auto ProjectFile::dumpProjectMetadata() const -> cpp::result<void, ProjectFileError> 0441 { 0442 QJsonObject obj{{"name", QString::fromStdString(d->projectName)}, 0443 {"description", QString::fromStdString(d->projectInformation)}, 0444 {"sourcePath", QString::fromStdString(d->sourceCodePath.string())}}; 0445 QJsonDocument doc(obj); 0446 0447 const fs::path metadata_file = d->openLocation / METADATA; 0448 auto saveFileName = QString::fromStdString(metadata_file.string()); 0449 QFile saveFile(saveFileName); 0450 if (!saveFile.open(QIODevice::WriteOnly)) { 0451 return cpp::fail(ProjectFileError{saveFile.errorString().toStdString()}); 0452 } 0453 0454 qint64 bytesSaved = saveFile.write(doc.toJson()); 0455 if (bytesSaved == 0) { 0456 return cpp::fail(ProjectFileError{"Error saving metadata file, zero bytes size."}); 0457 } 0458 return {}; 0459 } 0460 0461 void ProjectFile::loadProjectMetadata() 0462 { 0463 const fs::path metadata_file = d->openLocation / METADATA; 0464 const auto loadFileName = QString::fromStdString(metadata_file.string()); 0465 0466 QFile loadFile(loadFileName); 0467 loadFile.open(QIODevice::ReadOnly); 0468 0469 const QByteArray fileData = loadFile.readAll(); 0470 QJsonObject obj = QJsonDocument::fromJson(fileData).object(); 0471 0472 if (obj.contains("name")) { 0473 d->projectName = obj.value("name").toString().toStdString(); 0474 } 0475 if (obj.contains("description")) { 0476 d->projectInformation = obj.value("description").toString().toStdString(); 0477 } 0478 if (obj.contains("sourcePath")) { 0479 d->sourceCodePath = obj.value("sourcePath").toString().toStdString(); 0480 } 0481 } 0482 0483 fs::path ProjectFile::location() const 0484 { 0485 return d->location; 0486 } 0487 0488 std::string_view ProjectFile::codebaseDbFilename() 0489 { 0490 return CODE_DB; 0491 } 0492 0493 void ProjectFile::setSourceCodePath(std::filesystem::path sourceCodePath) 0494 { 0495 d->sourceCodePath = std::move(sourceCodePath); 0496 } 0497 0498 std::filesystem::path ProjectFile::sourceCodePath() const 0499 { 0500 return d->sourceCodePath; 0501 } 0502 0503 void ProjectFile::setDirty() 0504 { 0505 d->isDirty = true; 0506 } 0507 0508 bool ProjectFile::isDirty() const 0509 { 0510 return d->isDirty; 0511 } 0512 0513 std::vector<QJsonDocument> jsonDocInFolder(const QString& folder) 0514 { 0515 // get the list of files on the left panel tab 0516 QDir dir(folder); 0517 const auto dirEntries = dir.entryList(QDir::Filter::NoDotAndDotDot | QDir::Filter::Files); 0518 0519 std::vector<QJsonDocument> ret; 0520 ret.reserve(dirEntries.size()); 0521 0522 for (const QString& filePath : dirEntries) { 0523 QFile file(folder + QDir::separator() + filePath); 0524 const bool opened = file.open(QIODevice::ReadOnly); 0525 if (!opened) { 0526 continue; 0527 } 0528 ret.push_back(QJsonDocument::fromJson(file.readAll())); 0529 } 0530 0531 return ret; 0532 } 0533 0534 std::vector<QJsonDocument> ProjectFile::leftPanelTab() 0535 { 0536 const auto folder = QString::fromStdString((d->openLocation / LEFT_PANEL_HISTORY).string()); 0537 return jsonDocInFolder(folder); 0538 } 0539 0540 std::vector<QJsonDocument> ProjectFile::rightPanelTab() 0541 { 0542 const auto folder = QString::fromStdString((d->openLocation / RIGHT_PANEL_HISTORY).string()); 0543 return jsonDocInFolder(folder); 0544 } 0545 0546 cpp::result<void, ProjectFileError> ProjectFile::resetCadDatabaseFromCodeDatabase() 0547 { 0548 auto cadDbPath = cadDatabasePath(); 0549 try { 0550 std::filesystem::remove(cadDbPath); 0551 } catch (std::filesystem::filesystem_error& err) { 0552 const auto errorMsg = std::string{"Error removing cad database: "} + err.what(); 0553 return cpp::fail(ProjectFileError{errorMsg}); 0554 } 0555 try { 0556 std::filesystem::copy(codeDatabasePath(), cadDbPath); 0557 } catch (std::filesystem::filesystem_error& err) { 0558 const auto errorMsg = std::string{"Error removing cad database: "} + err.what(); 0559 return cpp::fail(ProjectFileError{errorMsg}); 0560 } 0561 0562 lvtmdb::SociWriter writer; 0563 if (!writer.updateDbSchema(cadDbPath.string(), "cad_db.sql")) { 0564 const auto errorMsg = std::string{"Error removing adding cad tables to the database."}; 0565 return cpp::fail(ProjectFileError{errorMsg}); 0566 } 0567 0568 return {}; 0569 } 0570 0571 void ProjectFile::prepareSave() 0572 { 0573 for (const auto folder : {LEFT_PANEL_HISTORY, RIGHT_PANEL_HISTORY}) { 0574 QDir full_folder(QString::fromStdString((d->openLocation / folder).string())); 0575 const auto dirEntries = full_folder.entryList(QDir::Filter::NoDotAndDotDot | QDir::Filter::Files); 0576 for (const QString& filePath : dirEntries) { 0577 QFile file(full_folder.absolutePath() + QDir::separator() + filePath); 0578 file.remove(); 0579 } 0580 } 0581 } 0582 0583 QList<QString> ProjectFile::bookmarks() const 0584 { 0585 const auto folderPath = QString::fromStdString((d->openLocation / BOOKMARKS_FOLDER).string()); 0586 QDir dir(folderPath); 0587 0588 auto dirEntries = dir.entryList(QDir::Filter::NoDotAndDotDot | QDir::Filter::Files); 0589 for (auto& dirEntry : dirEntries) { 0590 dirEntry.replace(".json", ""); 0591 } 0592 0593 qDebug() << dirEntries; 0594 return dirEntries; 0595 } 0596 0597 QJsonDocument ProjectFile::getBookmark(const QString& name) const 0598 { 0599 const auto folderPath = QString::fromStdString((d->openLocation / BOOKMARKS_FOLDER).string()); 0600 QFile bookmark(folderPath + QDir::separator() + name + ".json"); 0601 if (!bookmark.exists()) { 0602 qDebug() << "Json document doesn't exists"; 0603 } 0604 if (!bookmark.open(QIODevice::ReadOnly)) { 0605 qDebug() << "Impossible to load json file"; 0606 } 0607 0608 return QJsonDocument::fromJson(bookmark.readAll()); 0609 } 0610 0611 void ProjectFile::removeBookmark(const QString& name) 0612 { 0613 const auto folderPath = QString::fromStdString((d->openLocation / BOOKMARKS_FOLDER).string()); 0614 QFile bookmark(folderPath + QDir::separator() + name + ".json"); 0615 if (!bookmark.exists()) { 0616 qDebug() << "Json document doesn't exists" << bookmark.fileName(); 0617 return; 0618 } 0619 0620 if (!bookmark.remove()) { 0621 qDebug() << "Couldn't remove bookmark"; 0622 return; 0623 } 0624 setDirty(); 0625 0626 Q_EMIT bookmarksChanged(); 0627 } 0628 0629 [[nodiscard]] cpp::result<void, ProjectFileError> ProjectFile::saveBookmark(const QJsonDocument& doc, 0630 ProjectFile::BookmarkType bookmarkType) 0631 { 0632 const auto file_id = doc.object()["id"].toString(); 0633 const auto folder = [bookmarkType]() -> std::string_view { 0634 switch (bookmarkType) { 0635 case LeftPane: 0636 return LEFT_PANEL_HISTORY; 0637 case RightPane: 0638 return RIGHT_PANEL_HISTORY; 0639 case Bookmark: 0640 return BOOKMARKS_FOLDER; 0641 } 0642 0643 // there's a bug on compilers (at least g++) and it does not 0644 // realizes I'm actually returning something always from the 0645 // switch case, requiring me to also add a return afterwards. 0646 assert(false && "should never hit here"); 0647 return BOOKMARKS_FOLDER; 0648 }(); 0649 0650 QDir dir; // just to call mkpath, this is not a static method. 0651 const auto folderPath = QString::fromStdString((d->openLocation / folder).string()); 0652 dir.mkpath(folderPath); 0653 0654 QFile saveFile(folderPath + QDir::separator() + (file_id + ".json")); 0655 if (!saveFile.open(QIODevice::WriteOnly)) { 0656 return cpp::fail(ProjectFileError{saveFile.errorString().toStdString()}); 0657 } 0658 0659 qint64 bytesSaved = saveFile.write(doc.toJson()); 0660 if (bytesSaved == 0) { 0661 return cpp::fail(ProjectFileError{"Error saving bookmark, zero bytes saved."}); 0662 } 0663 0664 if (folder == BOOKMARKS_FOLDER) { 0665 Q_EMIT bookmarksChanged(); 0666 } 0667 0668 setDirty(); 0669 return {}; 0670 }