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 :
23 : #include "notationproject.h"
24 :
25 : #include <memory>
26 :
27 : #include <QBuffer>
28 : #include <QDir>
29 : #include <QFile>
30 :
31 : #include "global/concurrency/concurrent.h"
32 : #include "global/io/buffer.h"
33 : #include "global/io/file.h"
34 : #include "global/io/ioretcodes.h"
35 : #include "global/io/devtools/allzerosbuffercorruptor.h"
36 :
37 : #include "engraving/compat/engravingcompat.h"
38 : #include "engraving/dom/excerpt.h"
39 : #include "engraving/dom/masterscore.h"
40 : #include "engraving/dom/measure.h"
41 : #include "engraving/dom/measurebase.h"
42 : #include "engraving/dom/page.h"
43 : #include "engraving/dom/repeatlist.h"
44 : #include "engraving/dom/system.h"
45 : #include "engraving/editing/editscoreproperties.h"
46 : #include "engraving/editing/editstyle.h"
47 : #include "engraving/editing/transaction/transaction.h"
48 : #include "engraving/engravingerrors.h"
49 : #include "engraving/engravingproject.h"
50 : #include "engraving/infrastructure/mscio.h"
51 : #include "engraving/rw/inoutdata.h"
52 : #include "engraving/rw/write/writecontext.h"
53 :
54 : #include "iprojectautosaver.h"
55 : #include "notation/iexcerptnotation.h" // IWYU pragma: keep
56 : #include "notation/inotationundostack.h" // IWYU pragma: keep
57 : #include "notation/inotationviewstate.h"
58 : #include "notation/internal/masternotation.h"
59 : #include "notation/notationerrors.h"
60 : #include "projectaudiosettings.h"
61 : #include "projectfileinfoprovider.h"
62 : #include "projecterrors.h"
63 : #include "types/projectcreateoptions.h"
64 : #include "types/projectmeta.h"
65 :
66 : #include "defer.h"
67 : #include "log.h"
68 :
69 : using namespace mu;
70 : using namespace muse;
71 : using namespace muse::io;
72 : using namespace mu::engraving;
73 : using namespace mu::notation;
74 : using namespace mu::project;
75 :
76 0 : static void setupScoreMetaTags(mu::engraving::MasterScore* masterScore, const ProjectCreateOptions& projectOptions)
77 : {
78 0 : if (!projectOptions.title.isEmpty()) {
79 0 : masterScore->setMetaTag(WORK_TITLE_TAG, projectOptions.title);
80 : }
81 0 : if (!projectOptions.subtitle.isEmpty()) {
82 0 : masterScore->setMetaTag(SUBTITLE_TAG, projectOptions.subtitle);
83 : }
84 0 : if (!projectOptions.composer.isEmpty()) {
85 0 : masterScore->setMetaTag(COMPOSER_TAG, projectOptions.composer);
86 : }
87 0 : if (!projectOptions.lyricist.isEmpty()) {
88 0 : masterScore->setMetaTag(LYRICIST_TAG, projectOptions.lyricist);
89 : }
90 0 : if (!projectOptions.copyright.isEmpty()) {
91 0 : masterScore->setMetaTag(COPYRIGHT_TAG, projectOptions.copyright);
92 : }
93 0 : if (!projectOptions.templatePath.empty()) {
94 0 : masterScore->setMetaTag(CREATION_DATE_TAG, QDate::currentDate().toString(Qt::ISODate));
95 : }
96 0 : }
97 :
98 0 : QString NotationProject::scoreDefaultTitle()
99 : {
100 0 : return muse::qtrc("project", "Untitled score");
101 : }
102 :
103 0 : NotationProject::~NotationProject()
104 : {
105 0 : m_projectAudioSettings = nullptr;
106 0 : m_masterNotation = nullptr;
107 0 : m_engravingProject = nullptr;
108 0 : }
109 :
110 0 : void NotationProject::setupProject()
111 : {
112 0 : TRACEFUNC;
113 :
114 0 : m_engravingProject = EngravingProject::create(iocContext());
115 0 : m_engravingProject->setFileInfoProvider(std::make_shared<ProjectFileInfoProvider>(this));
116 0 : m_masterNotation = std::shared_ptr<MasterNotation>(new MasterNotation(this, iocContext()));
117 0 : m_projectAudioSettings = std::shared_ptr<ProjectAudioSettings>(new ProjectAudioSettings(iocContext()));
118 0 : }
119 :
120 0 : Ret NotationProject::load(const muse::io::path_t& path, const OpenParams& openParams, const std::string& format_)
121 : {
122 0 : TRACEFUNC;
123 :
124 0 : std::string format = format_.empty() ? io::suffix(path) : format_;
125 :
126 0 : LOGD() << "try load: " << path << ", format: " << format;
127 :
128 0 : setupProject();
129 0 : setPath(path);
130 :
131 0 : if (!isMuseScoreFile(format)) {
132 0 : Ret ret = doImport(path, openParams);
133 0 : if (ret) {
134 0 : listenIfNeedSaveChanges();
135 : }
136 :
137 0 : return ret;
138 0 : }
139 :
140 0 : Ret ret = doLoad(path, openParams, format);
141 0 : if (!ret) {
142 0 : LOGE() << "failed load, err: " << ret.toString();
143 0 : return ret;
144 : }
145 :
146 0 : bool treatAsImported = (m_masterNotation->mscVersion() < 400 && !isCloudProject()) || format == MSCZ_BACKUP;
147 :
148 0 : listenIfNeedSaveChanges();
149 0 : setNeedSave(treatAsImported);
150 :
151 0 : m_isNewlyCreated = treatAsImported;
152 0 : m_isImported = treatAsImported;
153 :
154 0 : return ret;
155 0 : }
156 :
157 0 : Ret NotationProject::doLoad(const muse::io::path_t& path, const OpenParams& openParams, const std::string& format)
158 : {
159 0 : TRACEFUNC;
160 :
161 0 : MscReader::Params params;
162 0 : params.filePath = path.toQString();
163 0 : params.mode = mscIoModeBySuffix(format);
164 0 : IF_ASSERT_FAILED(params.mode != MscIoMode::Unknown) {
165 0 : return make_ret(Ret::Code::InternalError);
166 : }
167 :
168 0 : MscReader reader(params);
169 0 : Ret ret = reader.open();
170 0 : if (!ret) {
171 0 : return ret;
172 : }
173 :
174 : // Load engraving project
175 0 : m_engravingProject->setFileInfoProvider(std::make_shared<ProjectFileInfoProvider>(this));
176 :
177 0 : rw::ReadInOutData inOutData;
178 0 : inOutData.forcePageMode = openParams.forcePageMode;
179 0 : ret = m_engravingProject->loadMscz(reader, &inOutData, openParams.forceMode);
180 0 : if (!ret) {
181 0 : return ret;
182 : }
183 :
184 0 : MasterScore* masterScore = m_engravingProject->masterScore();
185 0 : IF_ASSERT_FAILED(masterScore) {
186 0 : return engraving::make_ret(engraving::Err::UnknownError, reader.params().filePath);
187 : }
188 :
189 0 : masterScore->lockUpdates(true);
190 0 : DEFER {
191 0 : masterScore->lockUpdates(false);
192 0 : };
193 :
194 : // Setup master score
195 0 : ret = m_engravingProject->setupMasterScore(openParams.forceMode);
196 0 : if (!ret) {
197 0 : return ret;
198 : }
199 :
200 : // Migration
201 0 : if (migrator()) {
202 0 : masterScore->lockUpdates(false); // because migration needs a second layout
203 0 : ret = migrator()->migrateEngravingProjectIfNeed(m_engravingProject);
204 0 : if (!ret) {
205 0 : return ret;
206 : }
207 0 : masterScore->lockUpdates(true);
208 : }
209 :
210 : // Load style if present
211 0 : if (!openParams.stylePath.empty()) {
212 0 : muse::io::File styleFile(openParams.stylePath);
213 0 : mu::engraving::MasterScore* ms = m_engravingProject->masterScore();
214 0 : mu::engraving::EditStyle::loadStyle(ms->transactionManager()->currentOrDummyTransaction(), ms, styleFile);
215 0 : }
216 :
217 0 : mu::engraving::compat::EngravingCompat::doPreLayoutCompatIfNeeded(m_engravingProject->masterScore());
218 :
219 0 : masterScore->updateCapo(/* ignoreNotationUpdate */ true);
220 :
221 0 : if (openParams.unrollRepeats && masterScore->repeatList().size() > 1) {
222 0 : MasterScore* original = masterScore;
223 0 : masterScore = original->unrollRepeats();
224 0 : delete original;
225 0 : m_engravingProject->setMasterScore(masterScore);
226 : } else {
227 0 : masterScore->lockUpdates(false);
228 0 : masterScore->setLayoutAll();
229 0 : masterScore->update();
230 : }
231 :
232 0 : mu::engraving::compat::EngravingCompat::doPostLayoutCompatIfNeeded(m_engravingProject->masterScore());
233 :
234 : // Load audio settings
235 0 : bool tryCompatAudio = false;
236 0 : if (!openParams.disablePlayback) {
237 0 : ret = m_projectAudioSettings->read(reader);
238 0 : if (!ret) {
239 0 : m_projectAudioSettings->makeDefault();
240 0 : tryCompatAudio = true;
241 : }
242 : }
243 :
244 : // Load cloud info
245 : {
246 0 : m_cloudInfo.sourceUrl = masterScore->metaTags()[SOURCE_TAG].toQString();
247 0 : m_cloudInfo.revisionId = masterScore->metaTags()[SOURCE_REVISION_ID_TAG].toInt();
248 :
249 0 : m_cloudAudioInfo.url = masterScore->metaTags()[AUDIO_COM_URL_TAG].toQString();
250 :
251 0 : if (configuration()->isLegacyCloudProject(path)) {
252 0 : m_cloudInfo.name = io::filename(path, false).toQString();
253 : }
254 : }
255 :
256 : // Set current if all success
257 0 : m_masterNotation->setMasterScore(masterScore, openParams.disablePlayback);
258 :
259 : // Load view settings & solo-mute states (needs to be done after notations are created)
260 0 : m_masterNotation->notation()->viewState()->read(reader);
261 0 : m_masterNotation->notation()->soloMuteState()->read(reader);
262 0 : const int mscVersion = m_masterNotation->mscVersion();
263 0 : for (const IExcerptNotationPtr& excerpt : m_masterNotation->excerpts()) {
264 0 : const INotationPtr excerptNotation = excerpt->notation();
265 0 : if (!excerpt->hasFileName() || mscVersion < 440) {
266 0 : m_masterNotation->initNotationSoloMuteState(excerptNotation);
267 0 : continue;
268 : }
269 0 : muse::io::path_t ePath = u"Excerpts/" + excerpt->fileName() + u"/";
270 0 : excerptNotation->viewState()->read(reader, ePath);
271 0 : excerptNotation->soloMuteState()->read(reader, ePath);
272 0 : }
273 :
274 : // Apply compat audio settings (needs to be done after notations are created)
275 0 : if (!openParams.disablePlayback && tryCompatAudio && !inOutData.settingsCompat.audioSettings.empty()) {
276 0 : for (const auto& audioCompat : inOutData.settingsCompat.audioSettings) {
277 0 : notation::INotationSoloMuteState::SoloMuteState state = { audioCompat.second.mute, audioCompat.second.solo };
278 0 : INotationSoloMuteStatePtr soloMuteStatePtr = m_masterNotation->notation()->soloMuteState();
279 0 : soloMuteStatePtr->setTrackSoloMuteState(audioCompat.second.instrumentId, state);
280 0 : }
281 : }
282 :
283 0 : return make_ret(Ret::Code::Ok);
284 0 : }
285 :
286 0 : Ret NotationProject::doImport(const muse::io::path_t& path, const OpenParams& openParams)
287 : {
288 0 : TRACEFUNC;
289 :
290 : // Find import reader
291 0 : std::string suffix = io::suffix(path);
292 0 : INotationReaderPtr scoreReader = readers()->reader(suffix);
293 0 : if (!scoreReader) {
294 0 : return make_ret(engraving::Err::FileUnknownType, path);
295 : }
296 :
297 : // Setup import reader
298 0 : INotationReader::Options options;
299 0 : if (openParams.forceMode) {
300 0 : options[INotationReader::OptionKey::ForceMode] = Val(openParams.forceMode);
301 : }
302 :
303 : // Setup engraving project
304 0 : mu::engraving::ScoreLoad sl;
305 0 : m_engravingProject->setFileInfoProvider(std::make_shared<ProjectFileInfoProvider>(this));
306 0 : mu::engraving::MasterScore* score = m_engravingProject->masterScore();
307 :
308 : // The order of the steps matches the order in MS3
309 : // (see https://github.com/musescore/MuseScore/blob/2513676e512d29d554cb6c4d37d3efaf53ea2c5b/mscore/file.cpp#L2260)
310 :
311 0 : io::path_t stylePath = openParams.stylePath.empty() ? notationConfiguration()->styleFileImportPath() : openParams.stylePath;
312 :
313 : // Load style if present
314 0 : if (!stylePath.empty()) {
315 0 : muse::io::File styleFile(stylePath);
316 0 : mu::engraving::EditStyle::loadStyle(score->transactionManager()->currentOrDummyTransaction(), score, styleFile);
317 0 : }
318 :
319 : // Init ChordList
320 0 : score->checkChordList();
321 :
322 : // Read (import) master score
323 0 : Ret ret = scoreReader->read(score, path, options);
324 0 : if (!ret) {
325 0 : return ret;
326 : }
327 :
328 : // Setup master score post-reading
329 0 : ret = m_engravingProject->setupMasterScore(openParams.forceMode);
330 0 : if (!ret) {
331 0 : return ret;
332 : }
333 :
334 0 : if (openParams.unrollRepeats && score->repeatList().size() > 1) {
335 0 : MasterScore* original = score;
336 0 : score = original->unrollRepeats();
337 0 : delete original;
338 0 : m_engravingProject->setMasterScore(score);
339 : }
340 :
341 : // Setup audio settings
342 0 : m_projectAudioSettings->makeDefault();
343 :
344 : // Setup view state
345 0 : m_masterNotation->notation()->viewState()->makeDefault();
346 0 : for (const IExcerptNotationPtr& excerpt : m_masterNotation->excerpts()) {
347 0 : excerpt->notation()->viewState()->makeDefault();
348 : }
349 :
350 : // Set current if all success
351 0 : m_masterNotation->setMasterScore(score, openParams.disablePlayback);
352 0 : setPath(path);
353 0 : setNeedSave(false);
354 0 : score->setMetaTag(u"originalFormat", QString::fromStdString(suffix));
355 :
356 0 : m_isNewlyCreated = true;
357 0 : m_isImported = true;
358 :
359 0 : return make_ret(Ret::Code::Ok);
360 0 : }
361 :
362 0 : Ret NotationProject::createNew(const ProjectCreateOptions& projectOptions)
363 : {
364 0 : TRACEFUNC;
365 :
366 0 : setupProject();
367 :
368 : // Load template if present
369 0 : if (!projectOptions.templatePath.empty()) {
370 0 : Ret ret = loadTemplate(projectOptions);
371 0 : return ret;
372 0 : }
373 :
374 : // Create new engraving project
375 0 : setPath(projectOptions.title.isEmpty() ? scoreDefaultTitle() : projectOptions.title);
376 0 : m_engravingProject->setFileInfoProvider(std::make_shared<ProjectFileInfoProvider>(this));
377 :
378 0 : mu::engraving::MasterScore* masterScore = m_engravingProject->masterScore();
379 0 : setupScoreMetaTags(masterScore, projectOptions);
380 :
381 : // Setup new master score
382 0 : Ret ret = m_masterNotation->setupNewScore(masterScore, projectOptions.scoreOptions);
383 0 : if (!ret) {
384 0 : return ret;
385 : }
386 :
387 : // Setup audio settings
388 0 : m_projectAudioSettings->makeDefault();
389 :
390 : // Setup view state
391 0 : m_masterNotation->notation()->viewState()->makeDefault();
392 0 : for (const IExcerptNotationPtr& excerpt : m_masterNotation->excerpts()) {
393 0 : excerpt->notation()->viewState()->makeDefault();
394 : }
395 :
396 0 : listenIfNeedSaveChanges();
397 0 : markAsUnsaved();
398 :
399 0 : m_isNewlyCreated = true;
400 :
401 0 : return make_ret(Ret::Code::Ok);
402 0 : }
403 :
404 0 : Ret NotationProject::loadTemplate(const ProjectCreateOptions& projectOptions)
405 : {
406 0 : TRACEFUNC;
407 :
408 0 : Ret ret = load(projectOptions.templatePath);
409 :
410 0 : if (ret) {
411 0 : setPath(projectOptions.title.isEmpty() ? scoreDefaultTitle() : projectOptions.title);
412 :
413 0 : mu::engraving::MasterScore* masterScore = m_masterNotation->masterScore();
414 0 : setupScoreMetaTags(masterScore, projectOptions);
415 :
416 0 : m_masterNotation->notation()->undoStack()->lock();
417 0 : m_masterNotation->applyOptions(masterScore, projectOptions.scoreOptions, true /*createdFromTemplate*/);
418 0 : m_masterNotation->notation()->undoStack()->unlock();
419 :
420 0 : setNeedSave(false);
421 0 : m_isNewlyCreated = true;
422 : }
423 :
424 0 : return ret;
425 0 : }
426 :
427 0 : muse::io::path_t NotationProject::path() const
428 : {
429 0 : return m_path;
430 : }
431 :
432 0 : void NotationProject::setPath(const muse::io::path_t& path)
433 : {
434 0 : if (m_path == path) {
435 0 : return;
436 : }
437 :
438 0 : m_path = path;
439 0 : m_pathChanged.notify();
440 0 : m_displayNameChanged.notify();
441 : }
442 :
443 0 : muse::async::Notification NotationProject::pathChanged() const
444 : {
445 0 : return m_pathChanged;
446 : }
447 :
448 0 : QString NotationProject::displayName() const
449 : {
450 0 : if (isNewlyCreated()) {
451 0 : if (m_path.empty()) {
452 0 : QString workTitle = m_masterNotation->notation()->workTitle();
453 0 : if (workTitle.isEmpty()) {
454 0 : return scoreDefaultTitle();
455 : }
456 0 : return workTitle;
457 0 : }
458 0 : return io::filename(m_path).toQString();
459 : }
460 :
461 0 : if (isCloudProject()) {
462 0 : return m_cloudInfo.name;
463 : }
464 :
465 0 : bool isSuffixInteresting = io::suffix(m_path) != engraving::MSCZ;
466 0 : return io::filename(m_path, isSuffixInteresting).toQString();
467 : }
468 :
469 0 : muse::async::Notification NotationProject::displayNameChanged() const
470 : {
471 0 : return m_displayNameChanged;
472 : }
473 :
474 0 : bool NotationProject::isCloudProject() const
475 : {
476 0 : return configuration()->isCloudProject(m_path);
477 : }
478 :
479 0 : const CloudProjectInfo& NotationProject::cloudInfo() const
480 : {
481 0 : return m_cloudInfo;
482 : }
483 :
484 0 : void NotationProject::setCloudInfo(const CloudProjectInfo& info)
485 : {
486 0 : m_cloudInfo = info;
487 0 : m_masterNotation->masterScore()->setMetaTag(SOURCE_TAG, info.sourceUrl.toString());
488 0 : m_masterNotation->masterScore()->setMetaTag(SOURCE_REVISION_ID_TAG, String::number(info.revisionId));
489 :
490 0 : m_displayNameChanged.notify();
491 0 : }
492 :
493 0 : const CloudAudioInfo& NotationProject::cloudAudioInfo() const
494 : {
495 0 : return m_cloudAudioInfo;
496 : }
497 :
498 0 : void NotationProject::setCloudAudioInfo(const CloudAudioInfo& audioInfo)
499 : {
500 0 : m_cloudAudioInfo = audioInfo;
501 0 : m_masterNotation->masterScore()->setMetaTag(AUDIO_COM_URL_TAG, audioInfo.url.toString());
502 0 : }
503 :
504 0 : Ret NotationProject::save(const muse::io::path_t& path, SaveMode saveMode, bool createBackup)
505 : {
506 0 : TRACEFUNC;
507 :
508 0 : Ret ret;
509 0 : muse::io::path_t savePath = path;
510 :
511 0 : switch (saveMode) {
512 0 : case SaveMode::SaveSelection: {
513 0 : ret = saveSelectionOnScore(savePath);
514 0 : } break;
515 0 : case SaveMode::Save:
516 : case SaveMode::SaveAs:
517 : case SaveMode::SaveCopy: {
518 0 : if (savePath.empty()) {
519 0 : IF_ASSERT_FAILED(!m_path.empty()) {
520 0 : return false;
521 : }
522 :
523 0 : savePath = m_path;
524 : }
525 :
526 0 : std::string suffix = io::suffix(savePath);
527 :
528 : // Whether a backup file will be created depends on both the caller's and user's will
529 0 : bool shouldCreateBackup = createBackup && configuration()->createBackupBeforeSaving();
530 :
531 0 : ret = saveScore(savePath, suffix, shouldCreateBackup);
532 0 : if (ret) {
533 0 : if (saveMode != SaveMode::SaveCopy) {
534 0 : markAsSaved(savePath);
535 : }
536 :
537 : // Re-compute headers and footers on save, to force timestamps update (if any)
538 0 : bool timestampsUpdated = false;
539 0 : for (Score* s : m_engravingProject->masterScore()->scoreList()) {
540 0 : if (renderer()->scoreHasTimestampHeadersFooters(s)) {
541 0 : renderer()->layoutHeadersFooters(s);
542 0 : timestampsUpdated = true;
543 : }
544 0 : }
545 0 : if (timestampsUpdated) {
546 0 : projectSession()->currentNotation()->notationChanged().send(muse::RectF());
547 : }
548 : }
549 0 : } break;
550 0 : case SaveMode::AutoSave: {
551 0 : std::string suffix = io::suffix(savePath);
552 0 : if (suffix == IProjectAutoSaver::AUTOSAVE_SUFFIX) {
553 0 : suffix = io::suffix(io::completeBasename(savePath));
554 : }
555 :
556 0 : if (suffix.empty()) {
557 : // Then it must be a MSCX folder
558 0 : suffix = engraving::MSCX;
559 : }
560 :
561 0 : ret = saveScore(savePath, suffix, false /*generateBackup*/, false /*createThumbnail*/, true /*isAutosave*/);
562 0 : } break;
563 0 : case SaveMode::SavePage:
564 0 : ASSERT_X("Use savePage(path, pageNum)");
565 : ret = muse::make_ret(Ret::Code::UnknownError);
566 : break;
567 : }
568 :
569 0 : if (ret) {
570 0 : m_saved.send(savePath, saveMode);
571 : }
572 :
573 0 : return ret;
574 0 : }
575 :
576 0 : muse::Ret NotationProject::savePage(const muse::io::path_t& path, const size_t pageNum)
577 : {
578 0 : TRACEFUNC;
579 :
580 0 : const MasterScore* score = m_masterNotation->masterScore();
581 0 : IF_ASSERT_FAILED(pageNum < score->npages()) {
582 0 : return make_ret(notation::Err::UnknownError);
583 : }
584 :
585 0 : const Page* page = score->pages().at(pageNum);
586 0 : const std::vector<System*>& systems = page->systems();
587 0 : if (systems.empty()) {
588 0 : return make_ret(notation::Err::UnknownError);
589 : }
590 :
591 0 : write::WriteRange range;
592 0 : range.startStaffIdx = 0;
593 0 : range.endStaffIdx = score->nstaves();
594 0 : range.startMeasure = systems.front()->first();
595 : // StaffWrite::writeStaff stops before reaching endMeasure, so use the measure after
596 : // the last one we want, otherwise the page's last measure/frame is dropped
597 0 : MeasureBase* lastMeasure = systems.back()->last();
598 0 : range.endMeasure = lastMeasure ? lastMeasure->next() : nullptr;
599 :
600 0 : write::WriteContext ctx(score);
601 0 : ctx.setRange(range);
602 :
603 0 : Ret ret = writeProject(path, &ctx);
604 0 : if (ret) {
605 0 : m_saved.send(path, SaveMode::SavePage);
606 : }
607 :
608 0 : return ret;
609 0 : }
610 :
611 0 : muse::async::Channel<path_t, SaveMode> NotationProject::saveComplited() const
612 : {
613 0 : return m_saved;
614 : }
615 :
616 0 : Ret NotationProject::writeToDevice(QIODevice* device)
617 : {
618 0 : TRACEFUNC;
619 :
620 0 : IF_ASSERT_FAILED(!m_path.empty()) {
621 0 : return make_ret(notation::Err::UnknownError);
622 : }
623 :
624 0 : auto buf = Buffer::opened(IODevice::WriteOnly);
625 :
626 0 : MscWriter::Params params;
627 0 : params.device = &buf;
628 0 : params.filePath = m_path.toQString();
629 0 : params.mode = MscIoMode::Zip;
630 :
631 0 : MscWriter msczWriter(params);
632 0 : msczWriter.open();
633 :
634 0 : Ret ret = writeProject(msczWriter, false);
635 0 : msczWriter.close();
636 :
637 0 : if (ret) {
638 0 : if (msczWriter.hasError()) {
639 0 : LOGE() << "MSCZ writer has error";
640 0 : return make_ret(Ret::Code::UnknownError);
641 : }
642 :
643 0 : buf.open(IODevice::OpenMode::ReadOnly);
644 0 : ByteArray ba = buf.readAll();
645 :
646 0 : if ((size_t)device->write(ba.toQByteArrayNoCopy()) != ba.size()) {
647 0 : LOGE() << "Error writing to device";
648 0 : return make_ret(Ret::Code::UnknownError);
649 : }
650 0 : }
651 :
652 0 : return ret;
653 0 : }
654 :
655 0 : Ret NotationProject::saveScore(const muse::io::path_t& path, const std::string& fileSuffix,
656 : bool generateBackup, bool createThumbnail, bool isAutosave,
657 : const write::WriteContext* ctx)
658 : {
659 0 : if (!isMuseScoreFile(fileSuffix) && !fileSuffix.empty()) {
660 0 : return exportProject(path, fileSuffix);
661 : }
662 :
663 0 : MscIoMode ioMode = mscIoModeBySuffix(fileSuffix);
664 :
665 0 : return doSave(path, ioMode, generateBackup, createThumbnail, isAutosave, ctx);
666 : }
667 :
668 0 : Ret NotationProject::doSave(const muse::io::path_t& path, engraving::MscIoMode ioMode,
669 : bool generateBackup, bool createThumbnail, bool isAutosave,
670 : const write::WriteContext* ctx)
671 : {
672 0 : TRACEFUNC;
673 :
674 0 : QString targetContainerPath = engraving::containerPath(path).toQString();
675 0 : muse::io::path_t targetMainFilePath = engraving::mainFilePath(path);
676 0 : muse::io::path_t targetMainFileName = engraving::mainFileName(path);
677 0 : QString savePath = targetContainerPath + "_saving";
678 :
679 : // Step 1: check writable
680 : {
681 0 : if ((fileSystem()->exists(savePath) && !fileSystem()->isWritable(savePath))
682 0 : || (fileSystem()->exists(targetContainerPath) && !fileSystem()->isWritable(targetContainerPath))) {
683 0 : LOGE() << "failed save, not writable path: " << targetContainerPath;
684 0 : return make_ret(io::Err::FSWriteError);
685 : }
686 :
687 0 : if (ioMode == engraving::MscIoMode::Dir) {
688 : // Dir needs to be created, otherwise we can't move to it
689 0 : if (!QDir(targetContainerPath).mkpath(".")) {
690 0 : LOGE() << "Couldn't create container directory: " << targetContainerPath;
691 0 : return make_ret(io::Err::FSMakingError);
692 : }
693 : }
694 : }
695 :
696 : // Step 2: write project
697 : {
698 0 : MscWriter::Params params;
699 0 : params.filePath = savePath;
700 0 : params.mainFileName = targetMainFileName.toQString();
701 0 : params.mode = ioMode;
702 0 : IF_ASSERT_FAILED(params.mode != MscIoMode::Unknown) {
703 0 : return make_ret(Ret::Code::InternalError);
704 : }
705 :
706 : const bool shouldCorrupt = ioMode == MscIoMode::Zip
707 0 : && !isAutosave
708 0 : && globalConfiguration()->devModeEnabled()
709 0 : && savePath.contains(" - ALL_ZEROS_CORRUPTED.mscz");
710 :
711 0 : std::unique_ptr<Buffer> maybeOutBuf;
712 0 : if (shouldCorrupt) {
713 : // Create corrupted data so devs/qa can simulate a saved corrupted file.
714 0 : maybeOutBuf = std::make_unique<AllZerosBufferCorruptor>();
715 0 : } else if (ioMode != engraving::MscIoMode::Dir) {
716 0 : maybeOutBuf = std::make_unique<Buffer>();
717 : }
718 0 : params.device = maybeOutBuf.get();
719 :
720 0 : MscWriter msczWriter(params);
721 0 : Ret ret = writeProject(msczWriter, createThumbnail, ctx);
722 0 : msczWriter.close();
723 :
724 0 : if (!ret) {
725 0 : LOGE() << "failed write project to buffer: " << ret.toString();
726 0 : return ret;
727 : }
728 :
729 0 : if (msczWriter.hasError()) {
730 0 : LOGE() << "MscWriter has error after writing project";
731 0 : return make_ret(Ret::Code::UnknownError);
732 : }
733 :
734 0 : if (maybeOutBuf) {
735 : #ifdef MUSE_THREADS_SUPPORT
736 0 : if (isAutosave) {
737 : // For autosave, write to disk on a background thread to avoid stuttering.
738 : // The buffer is fully serialized at this point so it's safe to move off the main thread.
739 0 : muse::ByteArray data = maybeOutBuf->data();
740 0 : QString savePathCopy = savePath;
741 0 : QString targetContainerPathCopy = targetContainerPath;
742 0 : muse::io::path_t targetMainFilePathCopy = targetMainFilePath;
743 0 : auto fs = fileSystem();
744 0 : Concurrent::run([fs, savePathCopy, targetContainerPathCopy, targetMainFilePathCopy, data]() {
745 0 : Ret writeRet = fs->writeFile(savePathCopy, data);
746 0 : if (!writeRet) {
747 0 : LOGE() << "Autosave: failed to write project file: " << writeRet.toString();
748 0 : return;
749 : }
750 0 : Ret copyRet = fs->copy(savePathCopy, targetContainerPathCopy, true);
751 0 : if (!copyRet) {
752 0 : LOGE() << "Autosave: failed to copy to target: " << copyRet.toString();
753 0 : return;
754 : }
755 0 : fs->remove(savePathCopy);
756 0 : QFile::setPermissions(targetMainFilePathCopy.toQString(),
757 0 : QFile::ReadOwner | QFile::WriteOwner | QFile::ReadUser | QFile::ReadGroup | QFile::ReadOther);
758 0 : LOGD() << "Autosave: background write complete: " << targetContainerPathCopy;
759 0 : });
760 0 : return make_ret(Ret::Code::Ok);
761 0 : }
762 : #endif
763 :
764 0 : ret = fileSystem()->writeFile(savePath, maybeOutBuf->data());
765 0 : if (!ret) {
766 0 : LOGE() << "Failed to write project file";
767 0 : return ret;
768 : }
769 : }
770 0 : }
771 :
772 : // Step 3: create backup if need
773 : {
774 0 : if (generateBackup) {
775 0 : makeBackup(targetContainerPath);
776 : }
777 : }
778 :
779 : // Step 4: replace to saved file
780 : {
781 0 : if (ioMode == MscIoMode::Dir) {
782 0 : RetVal<io::paths_t> filesToBeMoved = fileSystem()->scanFiles(savePath, { "*" }, io::ScanMode::FilesAndFoldersInCurrentDir);
783 0 : if (!filesToBeMoved.ret) {
784 0 : return filesToBeMoved.ret;
785 : }
786 :
787 0 : Ret ret = muse::make_ok();
788 :
789 0 : for (const muse::io::path_t& fileToBeMoved : filesToBeMoved.val) {
790 : muse::io::path_t destinationFile
791 0 : = muse::io::path_t(targetContainerPath).appendingComponent(io::filename(fileToBeMoved));
792 0 : LOGD() << fileToBeMoved << " to " << destinationFile;
793 0 : ret = fileSystem()->move(fileToBeMoved, destinationFile, true);
794 0 : if (!ret) {
795 0 : return ret;
796 : }
797 0 : }
798 :
799 : // Try to remove the temp save folder (not problematic if fails)
800 0 : ret = fileSystem()->remove(savePath, true);
801 0 : if (!ret) {
802 0 : LOGW() << ret.toString();
803 : }
804 0 : } else {
805 0 : Ret ret = muse::make_ok();
806 :
807 0 : ret = fileSystem()->copy(savePath, targetContainerPath, true);
808 0 : if (!ret) {
809 0 : return ret;
810 : }
811 :
812 0 : if (!isAutosave) {
813 0 : ret = checkSavedFileForCorruption(ioMode, targetContainerPath, targetMainFileName.toQString());
814 0 : if (!ret) {
815 0 : if (ret.code() == (int)Err::CorruptionUponSavingError) {
816 : // Validate the temporary "saving" file too.
817 0 : Ret ret2 = checkSavedFileForCorruption(ioMode, savePath, targetMainFileName.toQString());
818 0 : if (!ret2) {
819 0 : return ret2;
820 : }
821 0 : }
822 0 : return ret;
823 : }
824 : }
825 :
826 : // Remove the temp save file (not problematic if fails)
827 0 : fileSystem()->remove(savePath);
828 0 : }
829 : }
830 :
831 : // make file readable by all
832 : {
833 0 : QFile::setPermissions(targetMainFilePath.toQString(),
834 0 : QFile::ReadOwner | QFile::WriteOwner | QFile::ReadUser | QFile::ReadGroup | QFile::ReadOther);
835 : }
836 :
837 0 : LOGI() << "success save file: " << targetContainerPath;
838 0 : return make_ret(Ret::Code::Ok);
839 0 : }
840 :
841 0 : Ret NotationProject::makeBackup(muse::io::path_t filePath)
842 : {
843 0 : TRACEFUNC;
844 :
845 0 : if (io::suffix(filePath) != engraving::MSCZ) {
846 0 : LOGW() << "backup allowed only for MSCZ, currently: " << filePath;
847 0 : return make_ret(Ret::Code::Ok);
848 : }
849 :
850 0 : Ret ret = fileSystem()->exists(filePath);
851 0 : if (!ret) {
852 0 : LOGI() << "Backup won't be created, file does not exist: " << filePath;
853 0 : return make_ret(Ret::Code::Ok);
854 : }
855 :
856 0 : muse::io::path_t backupPath = configuration()->projectBackupPath(filePath);
857 0 : muse::io::path_t backupDir = io::absoluteDirpath(backupPath);
858 0 : ret = fileSystem()->makePath(backupDir);
859 0 : if (!ret) {
860 0 : LOGE() << "failed to create backup directory: " << backupDir;
861 0 : return ret;
862 : }
863 :
864 0 : fileSystem()->setAttribute(backupDir, io::IFileSystem::Attribute::Hidden);
865 :
866 0 : ret = fileSystem()->copy(filePath, backupPath, true);
867 0 : if (!ret) {
868 0 : LOGE() << "failed to copy: " << filePath << " to: " << backupPath;
869 0 : return ret;
870 : }
871 :
872 0 : fileSystem()->setAttribute(backupPath, io::IFileSystem::Attribute::Hidden);
873 :
874 0 : return ret;
875 0 : }
876 :
877 0 : muse::Ret NotationProject::writeProject(const muse::io::path_t& path, const write::WriteContext* ctx)
878 : {
879 0 : TRACEFUNC;
880 :
881 0 : IF_ASSERT_FAILED(path != m_path) {
882 0 : return make_ret(notation::Err::UnknownError);
883 : }
884 :
885 0 : std::string suffix = io::suffix(path);
886 :
887 0 : if (mscIoModeBySuffix(suffix) == MscIoMode::Unknown) {
888 0 : return make_ret(Ret::Code::InternalError);
889 : }
890 :
891 0 : return saveScore(path, suffix, false /*generateBackup*/, true /*createThumbnail*/, false /*isAutosave*/, ctx);
892 0 : }
893 :
894 0 : Ret NotationProject::writeProject(MscWriter& msczWriter, bool createThumbnail, const write::WriteContext* ctx)
895 : {
896 0 : TRACEFUNC;
897 :
898 : // Create MsczWriter
899 0 : Ret ret = msczWriter.open();
900 0 : if (!ret) {
901 0 : LOGE() << "failed open writer: " << ret.toString();
902 0 : return ret;
903 : }
904 :
905 : // Write engraving project
906 0 : ret = m_engravingProject->writeMscz(msczWriter, createThumbnail, ctx);
907 0 : if (!ret) {
908 0 : LOGE() << "failed write engraving project to mscz: " << ret.toString();
909 0 : return make_ret(notation::Err::UnknownError);
910 : }
911 :
912 : // Write master audio settings
913 0 : ret = m_projectAudioSettings->write(msczWriter, m_masterNotation->notation()->soloMuteState());
914 0 : if (!ret) {
915 0 : LOGE() << "failed write project audio settings, err: " << ret.toString();
916 0 : return ret;
917 : }
918 :
919 : // Write master view settings
920 0 : m_masterNotation->notation()->viewState()->write(msczWriter);
921 :
922 0 : if (ctx && ctx->shouldWriteRange()) {
923 0 : return make_ret(Ret::Code::Ok);
924 : }
925 :
926 : // Write excerpt view settings and solo-mute state
927 0 : for (const IExcerptNotationPtr& excerpt : m_masterNotation->excerpts()) {
928 0 : muse::io::path_t path = u"Excerpts/" + excerpt->fileName() + u"/";
929 0 : excerpt->notation()->viewState()->write(msczWriter, path);
930 :
931 0 : ByteArray soloMuteData;
932 0 : auto soloMuteBuf = Buffer::opened(IODevice::WriteOnly, &soloMuteData);
933 0 : excerpt->notation()->soloMuteState()->write(&soloMuteBuf /*out*/);
934 0 : msczWriter.writeAudioSettingsJsonFile(soloMuteData, path);
935 0 : }
936 :
937 0 : return make_ret(Ret::Code::Ok);
938 0 : }
939 :
940 0 : Ret NotationProject::saveSelectionOnScore(const muse::io::path_t& path)
941 : {
942 0 : TRACEFUNC;
943 :
944 0 : const MasterScore* score = m_masterNotation->masterScore();
945 0 : if (score->selectionEmpty()) {
946 0 : LOGE() << "failed save, empty selection";
947 0 : return make_ret(notation::Err::EmptySelection);
948 : }
949 :
950 0 : const Selection& selection = score->selection();
951 :
952 0 : write::WriteRange range;
953 0 : range.startStaffIdx = selection.staffStart();
954 0 : range.endStaffIdx = selection.staffEnd();
955 :
956 0 : range.startMeasure = selection.startSegment()->measure();
957 0 : if (range.startMeasure && range.startMeasure->isMeasure() && toMeasure(range.startMeasure)->isMMRest()) {
958 0 : range.startMeasure = toMeasure(range.startMeasure)->mmRestFirst();
959 : }
960 :
961 0 : if (const Segment* endSeg = selection.endSegment()) {
962 0 : range.endMeasure = endSeg->measure()->next();
963 : } else {
964 0 : range.endMeasure = nullptr;
965 : }
966 :
967 0 : write::WriteContext ctx(score);
968 0 : ctx.setRange(range);
969 0 : ctx.setFilter(score->selectionFilter());
970 :
971 0 : return writeProject(path, &ctx);
972 0 : }
973 :
974 0 : Ret NotationProject::checkSavedFileForCorruption(MscIoMode ioMode, const muse::io::path_t& path,
975 : const muse::io::path_t& scoreFileName)
976 : {
977 0 : TRACEFUNC;
978 :
979 0 : if (ioMode != MscIoMode::Zip) {
980 0 : return muse::make_ok();
981 : }
982 :
983 0 : MscReader::Params params;
984 0 : params.filePath = path;
985 0 : params.mainFileName = scoreFileName.toString();
986 0 : params.mode = MscIoMode::Zip;
987 :
988 0 : MscReader msczReader(params);
989 0 : Ret ret = msczReader.open();
990 0 : if (!ret) {
991 0 : return Ret(static_cast<int>(Err::CorruptionUponSavingError),
992 0 : muse::mtrc("project/save", "File “%1” could not be opened for validation. %2")
993 0 : .arg(path.toString(), String(ret.toString().c_str()))
994 0 : .toStdString());
995 : }
996 :
997 : // Try to extract the main score file.
998 0 : ByteArray scoreFile = msczReader.readScoreFile();
999 0 : msczReader.close();
1000 :
1001 0 : if (scoreFile.empty()) {
1002 0 : return Ret(static_cast<int>(Err::CorruptionUponSavingError),
1003 0 : muse::mtrc("project/save", "“%1” is corrupted or damaged.")
1004 0 : .arg(path.toString())
1005 0 : .toStdString());
1006 : }
1007 :
1008 0 : return muse::make_ok();
1009 0 : }
1010 :
1011 0 : Ret NotationProject::exportProject(const muse::io::path_t& path, const std::string& suffix)
1012 : {
1013 0 : TRACEFUNC;
1014 :
1015 0 : Buffer outBuf = Buffer::opened(IODevice::WriteOnly);
1016 :
1017 0 : auto writer = writers()->writer(suffix);
1018 0 : if (!writer) {
1019 0 : LOGE() << "Unknown export format: " << suffix;
1020 0 : return false;
1021 : }
1022 :
1023 0 : Ret ret = writer->write(m_masterNotation->notation(), outBuf);
1024 0 : if (!ret) {
1025 0 : return ret;
1026 : }
1027 0 : outBuf.close();
1028 :
1029 0 : return File::writeFile(path, outBuf.data());
1030 0 : }
1031 :
1032 0 : IMasterNotationPtr NotationProject::masterNotation() const
1033 : {
1034 0 : return m_masterNotation;
1035 : }
1036 :
1037 0 : bool NotationProject::isNewlyCreated() const
1038 : {
1039 0 : return m_isNewlyCreated;
1040 : }
1041 :
1042 0 : void NotationProject::markAsNewlyCreated()
1043 : {
1044 0 : m_isNewlyCreated = true;
1045 :
1046 0 : mu::engraving::MasterScore* masterScore = m_masterNotation->masterScore();
1047 :
1048 0 : QString title = masterScore->metaTag(WORK_TITLE_TAG);
1049 0 : setPath(!title.isEmpty() ? title : scoreDefaultTitle());
1050 :
1051 0 : markAsUnsaved();
1052 :
1053 0 : m_masterNotation->notation()->undoStack()->stackChanged().notify();
1054 0 : }
1055 :
1056 0 : bool NotationProject::isImported() const
1057 : {
1058 0 : return m_isImported;
1059 : }
1060 :
1061 0 : void NotationProject::markAsUnsaved()
1062 : {
1063 0 : setNeedSave(true);
1064 0 : }
1065 :
1066 0 : void NotationProject::listenIfNeedSaveChanges()
1067 : {
1068 0 : if (isCloudProject()) {
1069 : //! NOTE: master is always open once loaded; set before listening so it doesn't count as a change
1070 0 : m_masterNotation->notation()->setIsOpen(true);
1071 : }
1072 :
1073 0 : m_masterNotation->notation()->undoStack()->changesChannel().onReceive(this, [this](const ScoreChanges&) {
1074 0 : bool isStackClean = m_masterNotation && m_masterNotation->notation()->undoStack()->isStackClean();
1075 :
1076 0 : if (isStackClean && !m_hasNonUndoStackChanges) {
1077 0 : setNeedSave(false);
1078 : } else {
1079 0 : setNeedSave(true);
1080 : }
1081 0 : });
1082 :
1083 0 : auto listenNonUndoStackChanges = [this](const INotationPtr& notation) {
1084 0 : notation->openChanged().onNotify(this, [this]() {
1085 0 : markAsUnsaved();
1086 0 : m_hasNonUndoStackChanges = true;
1087 0 : }, Mode::SetReplace);
1088 :
1089 0 : notation->viewState()->stateChanged().onNotify(this, [this]() {
1090 0 : markAsUnsaved();
1091 0 : m_hasNonUndoStackChanges = true;
1092 0 : }, Mode::SetReplace);
1093 :
1094 0 : notation->soloMuteState()->trackSoloMuteStateChanged().onReceive(
1095 0 : this, [this](const InstrumentTrackId&, const notation::INotationSoloMuteState::SoloMuteState&) {
1096 0 : markAsUnsaved();
1097 0 : m_hasNonUndoStackChanges = true;
1098 0 : }, Mode::SetReplace);
1099 0 : };
1100 :
1101 0 : listenNonUndoStackChanges(m_masterNotation->notation());
1102 :
1103 0 : for (const IExcerptNotationPtr& excerpt : m_masterNotation->excerpts()) {
1104 0 : listenNonUndoStackChanges(excerpt->notation());
1105 : }
1106 :
1107 0 : m_masterNotation->excerptsChanged().onNotify(this, [this, listenNonUndoStackChanges]() {
1108 0 : for (const IExcerptNotationPtr& excerpt : m_masterNotation->excerpts()) {
1109 0 : listenNonUndoStackChanges(excerpt->notation());
1110 : }
1111 0 : });
1112 :
1113 0 : m_projectAudioSettings->settingsChanged().onNotify(this, [this]() {
1114 0 : markAsUnsaved();
1115 0 : m_hasNonUndoStackChanges = true;
1116 0 : });
1117 0 : }
1118 :
1119 0 : void NotationProject::markAsSaved(const muse::io::path_t& path)
1120 : {
1121 0 : TRACEFUNC;
1122 :
1123 : //! NOTE: order is important
1124 0 : m_isNewlyCreated = false;
1125 :
1126 0 : setNeedSave(false);
1127 :
1128 0 : setPath(path);
1129 :
1130 0 : m_masterNotation->notation()->undoStack()->stackChanged().notify();
1131 0 : }
1132 :
1133 0 : void NotationProject::setNeedSave(bool needSave)
1134 : {
1135 0 : mu::engraving::MasterScore* score = m_masterNotation->masterScore();
1136 0 : if (!score) {
1137 0 : return;
1138 : }
1139 :
1140 0 : setNeedAutoSave(needSave);
1141 :
1142 0 : if (!needSave) {
1143 0 : m_hasNonUndoStackChanges = false;
1144 : }
1145 :
1146 0 : if (m_isNeedSave == needSave) {
1147 0 : return;
1148 : }
1149 :
1150 0 : m_isNeedSave = needSave;
1151 0 : m_needSaveChanged.notify();
1152 : }
1153 :
1154 0 : bool NotationProject::isNeedSave() const
1155 : {
1156 0 : return m_isNeedSave;
1157 : }
1158 :
1159 0 : muse::async::Notification NotationProject::needSaveChanged() const
1160 : {
1161 0 : return m_needSaveChanged;
1162 : }
1163 :
1164 0 : Ret NotationProject::canSave() const
1165 : {
1166 0 : if (!m_masterNotation->hasParts()) {
1167 0 : return make_ret(Err::NoPartsError);
1168 : }
1169 :
1170 0 : Ret ret = m_engravingProject->checkCorrupted();
1171 0 : if (!ret) {
1172 0 : Err errorCode = m_engravingProject->isCorruptedUponLoading() ? Err::CorruptionUponOpenningError : Err::CorruptionError;
1173 0 : ret.setCode(static_cast<int>(errorCode));
1174 : }
1175 :
1176 0 : return ret;
1177 0 : }
1178 :
1179 0 : bool NotationProject::needAutoSave() const
1180 : {
1181 0 : return m_needAutoSave;
1182 : }
1183 :
1184 0 : void NotationProject::setNeedAutoSave(bool val)
1185 : {
1186 0 : m_needAutoSave = val;
1187 0 : }
1188 :
1189 0 : ProjectMeta NotationProject::metaInfo() const
1190 : {
1191 0 : TRACEFUNC;
1192 :
1193 0 : const engraving::MasterScore* score = m_masterNotation->masterScore();
1194 0 : const std::map<String, String>& allTags = score->metaTags();
1195 :
1196 0 : const auto initProjectMetaString = [&allTags](QString& string, const String& tag) {
1197 0 : const auto search = allTags.find(tag);
1198 0 : if (search == allTags.end()) {
1199 0 : LOGW() << "Project meta tag not found: " << tag;
1200 0 : return;
1201 : }
1202 0 : string = search->second;
1203 0 : };
1204 :
1205 0 : ProjectMeta meta;
1206 0 : meta.filePath = m_path;
1207 :
1208 0 : initProjectMetaString(meta.title, WORK_TITLE_TAG);
1209 0 : initProjectMetaString(meta.subtitle, SUBTITLE_TAG);
1210 0 : initProjectMetaString(meta.composer, COMPOSER_TAG);
1211 0 : initProjectMetaString(meta.arranger, ARRANGER_TAG);
1212 0 : initProjectMetaString(meta.lyricist, LYRICIST_TAG);
1213 0 : initProjectMetaString(meta.translator, TRANSLATOR_TAG);
1214 0 : initProjectMetaString(meta.copyright, COPYRIGHT_TAG);
1215 :
1216 0 : if (allTags.find(CREATION_DATE_TAG) != allTags.end()) {
1217 0 : meta.creationDate = QDate::fromString(allTags.at(CREATION_DATE_TAG), Qt::ISODate);
1218 : } else {
1219 0 : LOGW() << "Project meta tag not found: " << CREATION_DATE_TAG;
1220 : }
1221 :
1222 0 : meta.partsCount = score->excerpts().size();
1223 :
1224 0 : initProjectMetaString(meta.source, SOURCE_TAG);
1225 0 : initProjectMetaString(meta.audioComUrl, AUDIO_COM_URL_TAG);
1226 0 : initProjectMetaString(meta.platform, PLATFORM_TAG);
1227 :
1228 0 : meta.musescoreVersion = score->mscoreVersion();
1229 0 : meta.musescoreRevision = score->mscoreRevision();
1230 0 : meta.mscVersion = score->mscVersion();
1231 :
1232 0 : for (const auto& [tag, value] : allTags) {
1233 0 : if (isRepresentedInProjectMeta(tag)) {
1234 0 : continue;
1235 : }
1236 :
1237 0 : meta.additionalTags[tag] = value.toQString();
1238 : }
1239 :
1240 0 : return meta;
1241 0 : }
1242 :
1243 0 : void NotationProject::setMetaInfo(const ProjectMeta& meta, bool undoable)
1244 : {
1245 0 : if (meta == metaInfo()) {
1246 0 : return;
1247 : }
1248 :
1249 : std::map<String, String> tags {
1250 0 : { WORK_TITLE_TAG, meta.title },
1251 0 : { SUBTITLE_TAG, meta.subtitle },
1252 0 : { COMPOSER_TAG, meta.composer },
1253 0 : { ARRANGER_TAG, meta.arranger },
1254 0 : { LYRICIST_TAG, meta.lyricist },
1255 0 : { TRANSLATOR_TAG, meta.translator },
1256 0 : { COPYRIGHT_TAG, meta.copyright },
1257 0 : { CREATION_DATE_TAG, meta.creationDate.toString(Qt::ISODate) },
1258 0 : { SOURCE_TAG, meta.source },
1259 0 : { AUDIO_COM_URL_TAG, meta.audioComUrl },
1260 0 : { PLATFORM_TAG, meta.platform },
1261 0 : };
1262 :
1263 0 : for (auto it = meta.additionalTags.cbegin(); it != meta.additionalTags.cend(); ++it) {
1264 0 : tags[it.key()] = it.value().toString();
1265 : }
1266 :
1267 0 : MasterScore* score = m_masterNotation->masterScore();
1268 :
1269 0 : if (undoable) {
1270 0 : m_masterNotation->notation()->undoStack()->prepareChanges(TranslatableString("undoableAction", "Edit project properties"));
1271 0 : score->undo(new mu::engraving::ChangeMetaTags(score, tags));
1272 0 : m_masterNotation->notation()->undoStack()->commitChanges();
1273 0 : m_masterNotation->notation()->notationChanged().send(muse::RectF());
1274 : } else {
1275 0 : score->setMetaTags(tags);
1276 : }
1277 0 : }
1278 :
1279 0 : IProjectAudioSettingsPtr NotationProject::audioSettings() const
1280 : {
1281 0 : return m_projectAudioSettings;
1282 : }
|