LCOV - code coverage report
Current view: top level - src/project/internal - recentfilescontroller.cpp Coverage Total Hit
Test: filtered_coverage.info Lines: 85.2 % 189 161
Test Date: 2026-11-05 04:07:02

            Line data    Source code
       1              : /*
       2              :  * SPDX-License-Identifier: GPL-3.0-only
       3              :  * MuseScore-Studio-CLA-applies
       4              :  *
       5              :  * MuseScore Studio
       6              :  * Music Composition & Notation
       7              :  *
       8              :  * Copyright (C) 2021 MuseScore Limited and others
       9              :  *
      10              :  * This program is free software: you can redistribute it and/or modify
      11              :  * it under the terms of the GNU General Public License version 3 as
      12              :  * published by the Free Software Foundation.
      13              :  *
      14              :  * This program is distributed in the hope that it will be useful,
      15              :  * but WITHOUT ANY WARRANTY; without even the implied warranty of
      16              :  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
      17              :  * GNU General Public License for more details.
      18              :  *
      19              :  * You should have received a copy of the GNU General Public License
      20              :  * along with this program.  If not, see <https://www.gnu.org/licenses/>.
      21              :  */
      22              : #include "recentfilescontroller.h"
      23              : 
      24              : #include "global/async/async.h"
      25              : #include "global/defer.h"
      26              : #include "global/serialization/json.h"
      27              : 
      28              : #include "multiwindows/resourcelockguard.h"
      29              : 
      30              : #include "app_config.h"
      31              : 
      32              : #ifdef QT_CONCURRENT_SUPPORTED
      33              : #include "global/concurrency/concurrent.h"
      34              : #endif
      35              : 
      36              : #include "log.h"
      37              : 
      38              : using namespace mu::project;
      39              : using namespace muse;
      40              : using namespace muse::async;
      41              : 
      42              : static const std::string RECENT_FILES_RESOURCE_NAME("RECENT_FILES");
      43              : 
      44            3 : void RecentFilesController::init()
      45              : {
      46            4 :     TRACEFUNC;
      47              : 
      48            3 :     m_dirty = true;
      49              : 
      50            3 :     multiwindowsProvider()->resourceChanged().onReceive(this, [this](const std::string& resourceName) {
      51            3 :         if (resourceName == RECENT_FILES_RESOURCE_NAME) {
      52            2 :             if (!m_isSaving) {
      53            1 :                 m_dirty = true;
      54              : 
      55            1 :                 m_recentFilesListChanged.send(RecentFilesReset {});
      56              :             }
      57              :         }
      58            3 :     });
      59            3 : }
      60              : 
      61           26 : const RecentFilesList& RecentFilesController::recentFilesList() const
      62              : {
      63           27 :     TRACEFUNC;
      64              : 
      65           26 :     if (m_dirty) {
      66           26 :         const_cast<RecentFilesController*>(this)->loadRecentFilesList();
      67              :     }
      68              : 
      69           26 :     const_cast<RecentFilesController*>(this)->removeNonexistentFiles();
      70              : 
      71           26 :     return m_recentFilesList;
      72           26 : }
      73              : 
      74           13 : Channel<RecentFilesChange> RecentFilesController::recentFilesListChanged() const
      75              : {
      76           13 :     return m_recentFilesListChanged;
      77              : }
      78              : 
      79            5 : void RecentFilesController::prependRecentFile(const RecentFile& newFile)
      80              : {
      81            5 :     if (!newFile.isValid()) {
      82            1 :         return;
      83              :     }
      84              : 
      85            5 :     TRACEFUNC;
      86              : 
      87            4 :     RecentFilesList newList;
      88            4 :     newList.reserve(m_recentFilesList.size() + 1);
      89            4 :     newList.push_back(newFile);
      90              : 
      91            4 :     bool nonexistentRemoved = false;
      92              : 
      93           12 :     for (const RecentFile& file : m_recentFilesList) {
      94            8 :         if (file.path == newFile.path) {
      95            1 :             continue;
      96              :         }
      97              : 
      98            7 :         if (fileSystem()->exists(file.path)) {
      99            6 :             newList.push_back(file);
     100              :         } else {
     101            1 :             nonexistentRemoved = true;
     102              :         }
     103              :     }
     104              : 
     105            4 :     if (nonexistentRemoved) {
     106            1 :         setRecentFilesListAndNotify(newList, RecentFilesReset {});
     107              :     } else {
     108            3 :         setRecentFilesListAndNotify(newList, RecentFilePrepended { newFile });
     109              :     }
     110              : 
     111            4 :     prependPlatformRecentFile(newFile.path);
     112            4 : }
     113              : 
     114            2 : void RecentFilesController::moveRecentFile(const muse::io::path_t& before, const RecentFile& after)
     115              : {
     116            3 :     TRACEFUNC;
     117              : 
     118            2 :     bool moved = false;
     119            2 :     RecentFilesList newList = m_recentFilesList;
     120              : 
     121            4 :     for (RecentFile& file : newList) {
     122            3 :         if (file.path == before) {
     123            1 :             file = after;
     124            1 :             moved = true;
     125            1 :             break;
     126              :         }
     127              :     }
     128              : 
     129            2 :     if (moved) {
     130            1 :         setRecentFilesListAndNotify(newList, RecentFilePathChanged { before, after });
     131              :     }
     132            2 : }
     133              : 
     134            4 : void RecentFilesController::removeRecentFile(const muse::io::path_t& path)
     135              : {
     136            4 :     if (path.empty()) {
     137            1 :         return;
     138              :     }
     139              : 
     140            4 :     TRACEFUNC;
     141              : 
     142            3 :     RecentFilesList newList;
     143            3 :     newList.reserve(m_recentFilesList.size());
     144              : 
     145            3 :     bool removed = false;
     146              : 
     147            9 :     for (const RecentFile& file : m_recentFilesList) {
     148            6 :         if (file.path == path) {
     149            2 :             removed = true;
     150            2 :             continue;
     151              :         }
     152              : 
     153            4 :         newList.push_back(file);
     154              :     }
     155              : 
     156            3 :     if (removed) {
     157            2 :         setRecentFilesListAndNotify(newList, RecentFileRemoved { path });
     158              :     }
     159            3 : }
     160              : 
     161            1 : void RecentFilesController::clearRecentFiles()
     162              : {
     163            1 :     setRecentFilesListAndNotify({}, RecentFilesReset {});
     164              : 
     165            1 :     clearPlatformRecentFiles();
     166            1 : }
     167              : 
     168            0 : void RecentFilesController::prependPlatformRecentFile(const muse::io::path_t&) {}
     169              : 
     170            0 : void RecentFilesController::clearPlatformRecentFiles() {}
     171              : 
     172           26 : void RecentFilesController::loadRecentFilesList()
     173              : {
     174           26 :     RecentFilesList newList;
     175              : 
     176           26 :     DEFER {
     177           26 :         setRecentFilesList(newList);
     178           26 :     };
     179              : 
     180           26 :     RetVal<ByteArray> data;
     181              :     {
     182           26 :         mi::ReadResourceLockGuard lock_guard(multiwindowsProvider(), RECENT_FILES_RESOURCE_NAME);
     183           26 :         data = fileSystem()->readFile(configuration()->recentFilesJsonPath());
     184           26 :     }
     185              : 
     186           26 :     if (!data.ret || data.val.empty()) {
     187            1 :         data.val = configuration()->compatRecentFilesData();
     188              :     }
     189              : 
     190           26 :     if (data.val.empty()) {
     191            0 :         return;
     192              :     }
     193              : 
     194           26 :     std::string err;
     195           26 :     const JsonDocument json = JsonDocument::fromJson(data.val, &err);
     196           26 :     if (!err.empty()) {
     197            1 :         LOGE() << "Loading JSON failed: " << err;
     198            1 :         return;
     199              :     }
     200              : 
     201           25 :     if (!json.isArray()) {
     202            0 :         return;
     203              :     }
     204              : 
     205           25 :     const JsonArray array = json.rootArray();
     206           70 :     for (size_t i = 0; i < array.size(); ++i) {
     207           45 :         const JsonValue val = array.at(i);
     208              : 
     209           45 :         if (val.isString()) {
     210           44 :             newList.emplace_back(muse::io::path_t(val.toStdString()));
     211            1 :         } else if (val.isObject()) {
     212            1 :             const JsonObject obj = val.toObject();
     213            1 :             RecentFile file;
     214            2 :             file.path = obj["path"].toStdString();
     215            1 :             file.displayNameOverride = QString::fromStdString(obj["displayName"].toStdString());
     216            1 :             newList.emplace_back(std::move(file));
     217            1 :         } else {
     218            0 :             continue;
     219              :         }
     220           45 :     }
     221           30 : }
     222              : 
     223           26 : void RecentFilesController::removeNonexistentFiles()
     224              : {
     225           26 :     bool removed = false;
     226              : 
     227           26 :     RecentFilesList newList;
     228           26 :     newList.reserve(m_recentFilesList.size());
     229              : 
     230           71 :     for (const RecentFile& file : m_recentFilesList) {
     231           45 :         if (fileSystem()->exists(file.path)) {
     232           44 :             newList.push_back(file);
     233              :         } else {
     234            1 :             removed = true;
     235              :         }
     236              :     }
     237              : 
     238           26 :     if (removed) {
     239            1 :         setRecentFilesList(newList);
     240              : 
     241            1 :         async::Async::call(nullptr, [this, newList]() {
     242            1 :             saveRecentFilesList();
     243              : 
     244            1 :             m_recentFilesListChanged.send(RecentFilesReset {});
     245            1 :         });
     246              :     }
     247           26 : }
     248              : 
     249           35 : bool RecentFilesController::setRecentFilesList(const RecentFilesList& list)
     250              : {
     251           35 :     if (m_recentFilesList == list) {
     252           10 :         return false;
     253              :     }
     254              : 
     255           25 :     m_recentFilesList = list;
     256              : 
     257           25 :     cleanUpThumbnailCache(list);
     258              : 
     259           25 :     return true;
     260              : }
     261              : 
     262            8 : void RecentFilesController::setRecentFilesListAndNotify(const RecentFilesList& list, const RecentFilesChange& change)
     263              : {
     264            8 :     if (!setRecentFilesList(list)) {
     265            0 :         return;
     266              :     }
     267              : 
     268            8 :     saveRecentFilesList();
     269              : 
     270            8 :     m_recentFilesListChanged.send(change);
     271              : }
     272              : 
     273            9 : void RecentFilesController::saveRecentFilesList()
     274              : {
     275           10 :     TRACEFUNC;
     276              : 
     277            9 :     m_isSaving = true;
     278              : 
     279            9 :     DEFER {
     280            9 :         m_isSaving = false;
     281            9 :     };
     282              : 
     283            9 :     JsonArray jsonArray;
     284           26 :     for (const RecentFile& file : m_recentFilesList) {
     285           17 :         if (!file.displayNameOverride.isEmpty()) {
     286            1 :             JsonObject obj;
     287            2 :             obj["path"] = file.path.toStdString();
     288            3 :             obj["displayName"] = file.displayNameOverride.toStdString();
     289            1 :             jsonArray << obj;
     290            1 :         } else {
     291           16 :             jsonArray << file.path.toStdString();
     292              :         }
     293              :     }
     294              : 
     295            9 :     JsonDocument json(jsonArray);
     296              : 
     297              :     {
     298            9 :         mi::WriteResourceLockGuard resource_guard(multiwindowsProvider(), RECENT_FILES_RESOURCE_NAME);
     299            9 :         Ret ret = fileSystem()->writeFile(configuration()->recentFilesJsonPath(), json.toJson());
     300            9 :         if (!ret) {
     301            1 :             LOGE() << "Failed to save recent files list: " << ret.toString();
     302              :         }
     303            9 :     }
     304            9 : }
     305              : 
     306            0 : Promise<QPixmap> RecentFilesController::thumbnail(const muse::io::path_t& filePath) const
     307              : {
     308            0 :     return Promise<QPixmap>([this, filePath](auto resolve, auto reject) {
     309            0 :         if (filePath.empty()) {
     310            0 :             return reject(int(Ret::Code::UnknownError), "Invalid file specified");
     311              :         }
     312              : #ifdef QT_CONCURRENT_SUPPORTED
     313            0 :         Concurrent::run([this, filePath, resolve, reject]() {
     314            0 :             std::lock_guard lock(m_thumbnailCacheMutex);
     315              : 
     316            0 :             DateTime lastModified = fileSystem()->lastModified(filePath);
     317              : 
     318            0 :             auto it = m_thumbnailCache.find(filePath);
     319            0 :             if (it != m_thumbnailCache.cend()) {
     320            0 :                 if (lastModified == it->second.lastModified) {
     321            0 :                     (void)resolve(it->second.thumbnail);
     322            0 :                     return;
     323              :                 }
     324              :             }
     325              : 
     326            0 :             RetVal<QPixmap> thumbnail = mscMetaReader()->readThumbnail(filePath);
     327            0 :             if (!thumbnail.ret) {
     328            0 :                 m_thumbnailCache[filePath] = CachedThumbnail();
     329            0 :                 (void)reject(thumbnail.ret.code(), thumbnail.ret.toString());
     330              :             } else {
     331            0 :                 m_thumbnailCache[filePath] = CachedThumbnail { thumbnail.val, lastModified };
     332            0 :                 (void)resolve(thumbnail.val);
     333              :             }
     334            0 :         });
     335              : #else
     336              :         UNUSED(resolve);
     337              :         UNUSED(this);
     338              :         return reject(int(Ret::Code::NotSupported), "NotSupported");
     339              : #endif
     340              : 
     341            0 :         return Promise<QPixmap>::Result::unchecked();
     342            0 :     }, PromiseType::AsyncByBody);
     343              : }
     344              : 
     345           25 : void RecentFilesController::cleanUpThumbnailCache(const RecentFilesList& files)
     346              : {
     347              : #ifdef QT_CONCURRENT_SUPPORTED
     348           25 :     Concurrent::run([this, files] {
     349           25 :         std::lock_guard lock(m_thumbnailCacheMutex);
     350              : 
     351           25 :         if (files.empty()) {
     352            1 :             m_thumbnailCache.clear();
     353              :         } else {
     354           24 :             std::map<muse::io::path_t, CachedThumbnail> cleanedCache;
     355              : 
     356           70 :             for (const RecentFile& file : files) {
     357           46 :                 auto it = m_thumbnailCache.find(file.path);
     358           46 :                 if (it != m_thumbnailCache.cend()) {
     359            0 :                     cleanedCache[file.path] = it->second;
     360              :                 }
     361              :             }
     362              : 
     363           24 :             m_thumbnailCache = cleanedCache;
     364           24 :         }
     365           25 :     });
     366              : #else
     367              :     UNUSED(files);
     368              : #endif
     369           25 : }
        

Generated by: LCOV version 2.0-1