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 : }
|