LCOV - code coverage report
Current view: top level - src/project/internal - exportprojectscenario.cpp Coverage Total Hit
Test: filtered_coverage.info Lines: 0.0 % 273 0
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              : 
      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, &currentFileNum, 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 : }
        

Generated by: LCOV version 2.0-1