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 "exportprojectscenario.h"
24 :
25 : #include "global/io/fileinfo.h"
26 : #include "global/io/filestream.h"
27 :
28 : #include "translation.h"
29 : #include "defer.h"
30 : #include "log.h"
31 :
32 : #include "engraving/dom/score.h"
33 :
34 : #include "notation/iexcerptnotation.h" // IWYU pragma: keep
35 : #include "notation/imasternotation.h"
36 : #include "notation/inotation.h"
37 : #include "notation/inotationelements.h" // IWYU pragma: keep
38 : #include "notation/inotationpainting.h" // IWYU pragma: keep
39 :
40 : #include "inotationproject.h"
41 :
42 : using namespace muse;
43 : using namespace muse::io;
44 : using namespace mu::project;
45 : using namespace mu::notation;
46 :
47 0 : std::vector<INotationWriter::UnitType> ExportProjectScenario::supportedUnitTypes(const ExportType& exportType) const
48 : {
49 0 : IF_ASSERT_FAILED(!exportType.suffixes.isEmpty()) {
50 0 : return {};
51 : }
52 :
53 0 : auto writer = writers()->writer(exportType.suffixes.front().toStdString());
54 0 : if (!writer) {
55 0 : return {};
56 : }
57 :
58 0 : return writer->supportedUnitTypes();
59 0 : }
60 :
61 0 : RetVal<muse::io::path_t> ExportProjectScenario::askExportPath(const INotationPtrList& notations, const ExportType& exportType,
62 : INotationWriter::UnitType unitType, muse::io::path_t defaultDirPath) const
63 : {
64 0 : INotationProjectPtr project = context()->project();
65 :
66 0 : std::string filenameAddition;
67 :
68 : // If only one file will be created, the filename will be exactly what the user
69 : // types in the save dialog and therefore we can put the file dialog in charge of
70 : // asking the user whether an existing file should be overridden. Otherwise, we
71 : // will take care of that ourselves.
72 0 : bool isCreatingOnlyOneFile = guessIsCreatingOnlyOneFile(notations, unitType);
73 0 : bool isExportingOnlyOneScore = notations.size() == 1;
74 :
75 0 : if (unitType == INotationWriter::UnitType::MULTI_PART && !isExportingOnlyOneScore) {
76 0 : bool containsMaster = std::find_if(notations.cbegin(), notations.cend(), [this](INotationPtr notation) {
77 0 : return isMainNotation(notation);
78 0 : }) != notations.cend();
79 :
80 0 : if (containsMaster) {
81 : //: Used in export filename suggestion. Please use only characters that are valid for filenames.
82 0 : filenameAddition = "-" + muse::trc("project/export", "Score_and_Parts");
83 : } else {
84 : //: Used in export filename suggestion. Please use only characters that are valid for filenames.
85 0 : filenameAddition = "-" + muse::trc("project/export", "Parts");
86 : }
87 0 : } else if (isExportingOnlyOneScore) {
88 0 : if (!isMainNotation(notations.front())) {
89 0 : filenameAddition = "-" + io::escapeFileName(notations.front()->name()).toStdString();
90 : }
91 :
92 0 : if (unitType == INotationWriter::UnitType::PER_PAGE && isCreatingOnlyOneFile) {
93 : // So there is only one page
94 0 : filenameAddition += "-1";
95 : }
96 : }
97 :
98 0 : muse::io::path_t defaultPath;
99 0 : if (defaultDirPath == "") {
100 0 : defaultPath = configuration()->defaultSavingFilePath(project, filenameAddition, exportType.suffixes.front().toStdString());
101 : } else {
102 0 : defaultPath = defaultDirPath.appendingComponent(io::filename(project->path(), false) + filenameAddition)
103 0 : .appendingSuffix(exportType.suffixes.front().toStdString());
104 : }
105 :
106 0 : RetVal<muse::io::path_t> exportPath;
107 0 : exportPath.val = interactive()->selectSavingFileSync(muse::trc("project/export", "Export"), defaultPath,
108 0 : exportType.filter(), isCreatingOnlyOneFile);
109 0 : exportPath.ret = !exportPath.val.empty();
110 :
111 0 : return exportPath;
112 0 : }
113 :
114 0 : bool ExportProjectScenario::exportScores(notation::INotationPtrList notations, const muse::io::path_t destinationPath,
115 : INotationWriter::UnitType unitType, bool openDestinationFolderOnExport) const
116 : {
117 0 : std::string suffix = io::suffix(destinationPath);
118 0 : INotationWriterPtr writer = writers()->writer(suffix);
119 :
120 0 : if (!writer) {
121 0 : return false;
122 : }
123 :
124 0 : IF_ASSERT_FAILED(writer->supportsUnitType(unitType)) {
125 0 : return false;
126 : }
127 :
128 : // Make sure we do this in the same as we did it in `askExportPath`, so before initializing notations
129 0 : bool isCreatingOnlyOneFile = guessIsCreatingOnlyOneFile(notations, unitType);
130 0 : bool isExportingOnlyOneScore = notations.size() == 1;
131 :
132 : // The user might have selected not-yet-inited excerpts
133 0 : ExcerptNotationList excerptsToInit;
134 0 : ExcerptNotationList potentialExcerpts = masterNotation()->potentialExcerpts();
135 :
136 0 : for (const INotationPtr& notation : notations) {
137 0 : auto it = std::find_if(potentialExcerpts.cbegin(), potentialExcerpts.cend(), [notation](const IExcerptNotationPtr& excerpt) {
138 0 : return excerpt->notation() == notation;
139 : });
140 :
141 0 : if (it != potentialExcerpts.cend()) {
142 0 : excerptsToInit.push_back(*it);
143 : }
144 : }
145 :
146 0 : masterNotation()->initExcerpts(excerptsToInit);
147 :
148 : // Scores that are closed may have never been laid out, so we lay them out now
149 0 : for (const INotationPtr& notation : notations) {
150 0 : mu::engraving::Score* score = notation->elements()->msScore();
151 0 : if (!score->autoLayoutEnabled()) {
152 0 : score->doLayout();
153 : }
154 : }
155 :
156 : // Backup view modes
157 0 : std::vector<ViewMode> viewModes = this->viewModes(notations);
158 0 : setViewModes(notations, ViewMode::PAGE);
159 :
160 0 : Progress* writerProgress = writer->progress();
161 0 : size_t fileCount = exportFileCount(notations, unitType);
162 0 : size_t currentFileNum = 0;
163 :
164 0 : if (writerProgress) {
165 0 : showExportProgress(isAudioExport(suffix));
166 0 : m_exportProgress.start();
167 :
168 0 : writerProgress->progressChanged().onReceive(this, [this, ¤tFileNum, fileCount](int64_t current, int64_t total,
169 : const std::string& status) {
170 0 : m_exportProgress.progress(currentFileNum * total + current, fileCount * total, status);
171 0 : }, async::Asyncable::Mode::SetReplace);
172 :
173 0 : m_exportProgress.canceled().onNotify(this, [writer]() {
174 0 : writer->abort();
175 0 : }, muse::async::Asyncable::Mode::SetReplace);
176 : }
177 :
178 0 : DEFER {
179 : // Restore view modes
180 0 : setViewModes(notations, viewModes);
181 :
182 0 : if (writerProgress) {
183 0 : m_exportProgress.finish(muse::make_ok());
184 0 : writerProgress->progressChanged().disconnect(this);
185 0 : m_exportProgress.finished().disconnect(this);
186 : }
187 0 : };
188 :
189 : // If isCreatingOnlyOneFile, the save dialog has already asked whether to replace
190 : // any existing files. If the user cancels, the filepath will be empty, so we would
191 : // not reach this point. But if we do, existing files should be overridden.
192 0 : m_fileConflictPolicy = isCreatingOnlyOneFile ? FileConflictPolicy::ReplaceAll : FileConflictPolicy::Undefined;
193 :
194 : INotationWriter::Options options {
195 0 : { INotationWriter::OptionKey::UNIT_TYPE, Val(unitType) },
196 0 : };
197 :
198 0 : switch (unitType) {
199 0 : case INotationWriter::UnitType::PER_PAGE: {
200 0 : for (const INotationPtr& notation : notations) {
201 0 : size_t pageCount = notation->elements()->msScore()->pages().size();
202 0 : bool isMain = isMainNotation(notation);
203 :
204 0 : for (size_t page = 0; page < pageCount; ++page) {
205 0 : options[INotationWriter::OptionKey::PAGE_NUMBER] = Val(static_cast<int>(page));
206 :
207 : muse::io::path_t definitivePath = isCreatingOnlyOneFile
208 0 : ? destinationPath
209 0 : : completeExportPath(destinationPath, notation, isMain, isExportingOnlyOneScore,
210 0 : static_cast<int>(page));
211 :
212 0 : auto exportFunction = [writer, notation, options](io::IODevice& destinationDevice) {
213 0 : return writer->write(notation, destinationDevice, options);
214 0 : };
215 :
216 0 : Ret ret = doExportLoop(definitivePath, exportFunction);
217 0 : if (ret.code() == static_cast<int>(Ret::Code::Cancel)) {
218 0 : return false;
219 : }
220 :
221 0 : ++currentFileNum;
222 0 : }
223 : }
224 0 : } break;
225 0 : case INotationWriter::UnitType::PER_PART: {
226 0 : for (const INotationPtr& notation : notations) {
227 : muse::io::path_t definitivePath = isCreatingOnlyOneFile
228 0 : ? destinationPath
229 0 : : completeExportPath(destinationPath, notation, isMainNotation(
230 0 : notation), isExportingOnlyOneScore);
231 :
232 0 : auto exportFunction = [writer, notation, options](IODevice& destinationDevice) {
233 0 : return writer->write(notation, destinationDevice, options);
234 0 : };
235 :
236 0 : Ret ret = doExportLoop(definitivePath, exportFunction);
237 0 : if (ret.code() == static_cast<int>(Ret::Code::Cancel)) {
238 0 : return false;
239 : }
240 :
241 0 : ++currentFileNum;
242 0 : }
243 0 : } break;
244 0 : case INotationWriter::UnitType::MULTI_PART: {
245 0 : auto exportFunction = [writer, notations, options](IODevice& destinationDevice) {
246 0 : return writer->writeList(notations, destinationDevice, options);
247 0 : };
248 :
249 0 : Ret ret = doExportLoop(destinationPath, exportFunction);
250 0 : if (ret.code() == static_cast<int>(Ret::Code::Cancel)) {
251 0 : return false;
252 : }
253 0 : } break;
254 : }
255 :
256 0 : if (openDestinationFolderOnExport) {
257 0 : openFolder(destinationPath);
258 : }
259 :
260 0 : return true;
261 0 : }
262 :
263 0 : const ExportInfo& ExportProjectScenario::exportInfo() const
264 : {
265 0 : return m_exportInfo;
266 : }
267 :
268 0 : void ExportProjectScenario::setExportInfo(const ExportInfo& exportInfo)
269 : {
270 0 : m_exportInfo = exportInfo;
271 0 : }
272 :
273 0 : bool ExportProjectScenario::guessIsCreatingOnlyOneFile(const notation::INotationPtrList& notations,
274 : INotationWriter::UnitType unitType) const
275 : {
276 0 : switch (unitType) {
277 0 : case INotationWriter::UnitType::PER_PAGE: {
278 0 : if (notations.size() == 1) {
279 0 : INotationPtr notation = notations.front();
280 :
281 : // Check if it is not a potential (not-yet-initialized) excerpt
282 0 : ExcerptNotationList potentialExcerpts = masterNotation()->potentialExcerpts();
283 :
284 0 : auto it = std::find_if(potentialExcerpts.cbegin(), potentialExcerpts.cend(), [notation](const IExcerptNotationPtr& excerpt) {
285 0 : return excerpt->notation() == notation;
286 : });
287 :
288 0 : if (it == potentialExcerpts.cend()) {
289 0 : ViewMode viewMode = notation->painting()->viewMode();
290 0 : notation->painting()->setViewMode(ViewMode::PAGE);
291 :
292 0 : bool onePage = notations.front()->elements()->pages().size() == 1;
293 :
294 0 : notation->painting()->setViewMode(viewMode);
295 :
296 0 : return onePage;
297 : }
298 0 : }
299 :
300 0 : return false;
301 : };
302 0 : case INotationWriter::UnitType::PER_PART:
303 0 : return notations.size() == 1;
304 0 : case INotationWriter::UnitType::MULTI_PART:
305 0 : return true;
306 : }
307 :
308 0 : return false;
309 : }
310 :
311 0 : size_t ExportProjectScenario::exportFileCount(const INotationPtrList& notations, INotationWriter::UnitType unitType) const
312 : {
313 0 : switch (unitType) {
314 0 : case INotationWriter::UnitType::PER_PAGE: {
315 0 : size_t count = 0;
316 :
317 0 : for (const INotationPtr& notation : notations) {
318 0 : count += notation->elements()->pages().size();
319 : }
320 :
321 0 : return count;
322 : };
323 0 : case INotationWriter::UnitType::PER_PART:
324 0 : return notations.size();
325 0 : case INotationWriter::UnitType::MULTI_PART:
326 0 : return 1;
327 : }
328 :
329 0 : return 0;
330 : }
331 :
332 0 : bool ExportProjectScenario::isMainNotation(INotationPtr notation) const
333 : {
334 0 : return masterNotation()->notation() == notation;
335 : }
336 :
337 0 : IMasterNotationPtr ExportProjectScenario::masterNotation() const
338 : {
339 0 : return context()->masterNotation();
340 : }
341 :
342 0 : muse::io::path_t ExportProjectScenario::completeExportPath(const muse::io::path_t& basePath, INotationPtr notation, bool isMain,
343 : bool isExportingOnlyOneScore, int pageIndex) const
344 : {
345 0 : muse::io::path_t result = io::dirpath(basePath) + "/" + io::completeBasename(basePath);
346 :
347 0 : if (!isMain && !isExportingOnlyOneScore) {
348 0 : result += "-" + io::escapeFileName(notation->name()).toStdString();
349 : }
350 :
351 0 : if (pageIndex > -1) {
352 0 : result += "-" + std::to_string(pageIndex + 1);
353 : }
354 :
355 0 : result += "." + io::suffix(basePath);
356 :
357 0 : return result;
358 0 : }
359 :
360 0 : bool ExportProjectScenario::shouldReplaceFile(const QString& filename) const
361 : {
362 0 : switch (m_fileConflictPolicy) {
363 0 : case FileConflictPolicy::ReplaceAll:
364 0 : return true;
365 0 : case FileConflictPolicy::SkipAll:
366 0 : return false;
367 0 : case FileConflictPolicy::Undefined: {
368 0 : constexpr int Replace = static_cast<int>(IInteractive::Button::CustomButton) + 1;
369 0 : constexpr int ReplaceAll = static_cast<int>(IInteractive::Button::CustomButton) + 2;
370 0 : constexpr int Skip = static_cast<int>(IInteractive::Button::CustomButton) + 3;
371 0 : constexpr int SkipAll = static_cast<int>(IInteractive::Button::CustomButton) + 4;
372 :
373 0 : IInteractive::Result result = interactive()->questionSync(
374 0 : muse::trc("project/export", "File already exists"),
375 0 : muse::qtrc("project/export", "A file already exists with the filename %1. Do you want to replace it?")
376 0 : .arg(filename).toStdString(), {
377 0 : IInteractive::ButtonData(Replace, muse::trc("project/export", "Replace")),
378 0 : IInteractive::ButtonData(ReplaceAll, muse::trc("project/export", "Replace all")),
379 0 : IInteractive::ButtonData(Skip, muse::trc("project/export", "Skip")),
380 0 : IInteractive::ButtonData(SkipAll, muse::trc("project/export", "Skip all"))
381 : });
382 :
383 0 : switch (result.button()) {
384 0 : case ReplaceAll:
385 0 : m_fileConflictPolicy = FileConflictPolicy::ReplaceAll; // fallthrough
386 0 : case Replace:
387 0 : return true;
388 0 : case SkipAll:
389 0 : m_fileConflictPolicy = FileConflictPolicy::SkipAll; // fallthrough
390 0 : case Skip:
391 : default:
392 0 : return false;
393 : }
394 : } break;
395 : }
396 :
397 0 : return false;
398 0 : }
399 :
400 0 : bool ExportProjectScenario::askForRetry(const QString& filename) const
401 : {
402 0 : IInteractive::Result result = interactive()->questionSync(
403 0 : muse::trc("project/export", "Error"),
404 0 : muse::qtrc("project/export", "An error occurred while writing the file %1. Do you want to retry?")
405 0 : .arg(filename).toStdString(), { IInteractive::Button::Retry, IInteractive::Button::Abort });
406 :
407 0 : return result.standardButton() == IInteractive::Button::Retry;
408 : }
409 :
410 0 : Ret ExportProjectScenario::doExportLoop(const muse::io::path_t& scorePath, std::function<Ret(io::IODevice&)> exportFunction) const
411 : {
412 0 : IF_ASSERT_FAILED(exportFunction) {
413 0 : return make_ret(Ret::Code::InternalError);
414 : }
415 :
416 0 : String filename = FileInfo(scorePath).fileName();
417 0 : if (fileSystem()->exists(scorePath) && !shouldReplaceFile(filename)) {
418 0 : return make_ret(Ret::Code::InternalError);
419 : }
420 :
421 : while (true) {
422 : //! NOTE Most writers stream to the destination device (file on disk).
423 : //! But there is one atypical case:
424 : //! Export score to unpacked directory - creates a directory and writes files to it
425 :
426 : // Remove previous file because we need to know whether
427 : // exportFunction writes at scorePath
428 0 : Ret ret = fileSystem()->remove(scorePath);
429 0 : if (!ret) {
430 0 : if (askForRetry(filename)) {
431 0 : continue;
432 : } else {
433 0 : return make_ret(Ret::Code::Cancel);
434 : }
435 : }
436 :
437 0 : FileStream outputFile(scorePath);
438 0 : outputFile.setMeta("file_path", scorePath.toStdString());
439 0 : if (!outputFile.open(IODevice::WriteOnly)) {
440 0 : if (askForRetry(filename)) {
441 0 : continue;
442 : } else {
443 0 : return make_ret(Ret::Code::Cancel);
444 : }
445 : }
446 :
447 0 : ret = exportFunction(outputFile);
448 0 : outputFile.close();
449 :
450 0 : if (!ret) {
451 0 : if (ret.code() == static_cast<int>(Ret::Code::Cancel)) {
452 0 : const bool isFileMode = fileSystem()->exists(scorePath);
453 0 : if (!isFileMode) {
454 0 : return ret;
455 : }
456 :
457 : // On Windows, remove() may fail immediately after close()
458 : // because the file lock has not been released yet
459 0 : for (int i = 0; i < 10; ++i) {
460 0 : if (fileSystem()->remove(scorePath)) {
461 0 : return ret;
462 : }
463 :
464 0 : std::this_thread::sleep_for(std::chrono::milliseconds(100));
465 : }
466 :
467 0 : return ret;
468 : }
469 :
470 0 : if (askForRetry(filename)) {
471 0 : continue;
472 : } else {
473 0 : return make_ret(Ret::Code::Cancel);
474 : }
475 : }
476 :
477 0 : break;
478 0 : }
479 :
480 0 : return muse::make_ok();
481 0 : }
482 :
483 0 : void ExportProjectScenario::showExportProgress(bool isAudioExport) const
484 : {
485 0 : std::string title = isAudioExport ? muse::trc("project/export", "Exporting audio…") : muse::trc("project/export", "Exporting…");
486 :
487 0 : interactive()->showProgress(title, m_exportProgress);
488 0 : }
489 :
490 0 : void ExportProjectScenario::openFolder(const muse::io::path_t& path) const
491 : {
492 0 : Ret ret = platformInteractive()->revealInFileBrowser(path.toQString());
493 :
494 0 : if (!ret) {
495 0 : LOGE() << "Could not open folder: " << path.toQString();
496 : }
497 0 : }
498 :
499 0 : std::vector<ViewMode> ExportProjectScenario::viewModes(const notation::INotationPtrList& notations) const
500 : {
501 0 : std::vector<ViewMode> modes;
502 0 : for (const INotationPtr& notation : notations) {
503 0 : modes.push_back(notation->painting()->viewMode());
504 : }
505 :
506 0 : return modes;
507 0 : }
508 :
509 0 : void ExportProjectScenario::setViewModes(const notation::INotationPtrList& notations,
510 : const std::vector<notation::ViewMode>& viewModes) const
511 : {
512 0 : IF_ASSERT_FAILED(notations.size() == viewModes.size()) {
513 0 : return;
514 : }
515 :
516 0 : for (size_t i = 0; i < notations.size(); ++i) {
517 0 : notations[i]->painting()->setViewMode(viewModes[i]);
518 : }
519 : }
520 :
521 0 : void ExportProjectScenario::setViewModes(const notation::INotationPtrList& notations, ViewMode viewMode) const
522 : {
523 0 : std::vector<notation::ViewMode> modes(notations.size(), viewMode);
524 0 : setViewModes(notations, modes);
525 0 : }
|