LCOV - code coverage report
Current view: top level - src/project/internal - openprojectscenario.cpp Coverage Total Hit
Test: filtered_coverage.info Lines: 78.8 % 548 432
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) 2022 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 "openprojectscenario.h"
      24              : 
      25              : #include <QFile>
      26              : #include <QTimer>
      27              : #include <QUrlQuery>
      28              : 
      29              : #include "async/async.h"
      30              : #include "translation.h"
      31              : 
      32              : #include "cloud/clouderrors.h"
      33              : #include "cloud/cloudtypes.h"
      34              : #include "engraving/engravingerrors.h"
      35              : #include "engraving/infrastructure/mscio.h"
      36              : 
      37              : #include "notation/imasternotation.h"
      38              : #include "notation/inotation.h"
      39              : 
      40              : #include "inotationproject.h"
      41              : #include "projecterrors.h"
      42              : #include "types/projecturis.h"
      43              : 
      44              : #include "log.h"
      45              : 
      46              : using namespace mu::project;
      47              : using namespace mu::notation;
      48              : using namespace muse;
      49              : using muse::async::Promise;
      50              : 
      51           44 : bool OpenProjectScenario::isBusy(BusyStatus status) const
      52              : {
      53           44 :     return m_busyStatuses.contains(status);
      54              : }
      55              : 
      56           71 : void OpenProjectScenario::setBusy(BusyStatus status, bool isBusy)
      57              : {
      58           71 :     bool wasBusy = m_busyStatuses.contains(status);
      59           71 :     if (wasBusy == isBusy) {
      60            0 :         return;
      61              :     }
      62              : 
      63           71 :     if (isBusy) {
      64           39 :         m_busyStatuses.insert(status);
      65              :     } else {
      66           32 :         m_busyStatuses.erase(status);
      67              :     }
      68              : 
      69           71 :     m_busyChanged.notify();
      70              : }
      71              : 
      72            0 : muse::async::Notification OpenProjectScenario::busyChanged() const
      73              : {
      74            0 :     return m_busyChanged;
      75              : }
      76              : 
      77           40 : Promise<Ret> OpenProjectScenario::runIfNotBusy(BusyStatus status, const std::function<Promise<Ret>()>& flow)
      78              : {
      79           40 :     if (isBusy(status)) {
      80            1 :         return resolvedPromise(make_ret(Ret::Code::Busy));
      81              :     }
      82              : 
      83           39 :     setBusy(status, true);
      84              : 
      85           78 :     return flow().then<Ret>(this, [this, status](const Ret& ret, auto resolve) {
      86           32 :         setBusy(status, false);
      87           32 :         return resolve(ret);
      88           39 :     });
      89              : }
      90              : 
      91            2 : Promise<RetVal<Val> > OpenProjectScenario::openDialog(const UriQuery& query) const
      92              : {
      93            4 :     return async::make_promise<RetVal<Val> >([this, query](auto resolve) {
      94            2 :         interactive()->open(query)
      95            4 :         .onResolve(this, [resolve](const Val& val) {
      96            1 :             (void)resolve(RetVal<Val>::make_ok(val));
      97              :         })
      98            2 :         .onReject(this, [resolve](int code, const std::string& err) {
      99            1 :             (void)resolve(RetVal<Val>(muse::make_ret(code, err)));
     100              :         });
     101              : 
     102            2 :         return Promise<RetVal<Val> >::dummy_result();
     103            4 :     });
     104              : }
     105              : 
     106            0 : static std::string cloudStatusCodeErrorMessage(const Ret& ret, bool withHelp = false)
     107              : {
     108            0 :     std::string message;
     109              : 
     110            0 :     switch (ret.code()) {
     111            0 :     case int(cloud::Err::Status400_InvalidRequest):
     112              :         //: %1 will be replaced with the error code that MuseScore.com returned; this might contain english text
     113              :         //: that is deliberately not translated
     114            0 :         message = muse::qtrc("project/cloud", "MuseScore.com returned an error code: %1.")
     115            0 :                   .arg("400 Invalid request").toStdString();
     116            0 :         break;
     117            0 :     case int(cloud::Err::Status401_AuthorizationRequired):
     118              :         //: %1 will be replaced with the error code that MuseScore.com returned; this might contain english text
     119              :         //: that is deliberately not translated
     120            0 :         message = muse::qtrc("project/cloud", "MuseScore.com returned an error code: %1.")
     121            0 :                   .arg("401 Authorization required").toStdString();
     122            0 :         break;
     123            0 :     case int(cloud::Err::Status422_ValidationFailed):
     124              :         //: %1 will be replaced with the error code that MuseScore.com returned; this might contain english text
     125              :         //: that is deliberately not translated
     126            0 :         message = muse::qtrc("project/cloud", "MuseScore.com returned an error code: %1.")
     127            0 :                   .arg("422 Validation failed").toStdString();
     128            0 :         break;
     129            0 :     case int(cloud::Err::Status429_RateLimitExceeded):
     130              :         //: %1 will be replaced with the error code that MuseScore.com returned; this might contain english text
     131              :         //: that is deliberately not translated
     132            0 :         message = muse::qtrc("project/cloud", "MuseScore.com returned an error code: %1.")
     133            0 :                   .arg("429 Rate limit exceeded").toStdString();
     134            0 :         break;
     135            0 :     case int(cloud::Err::Status500_InternalServerError):
     136              :         //: %1 will be replaced with the error code that MuseScore.com returned; this might contain english text
     137              :         //: that is deliberately not translated
     138            0 :         message = muse::qtrc("project/cloud", "MuseScore.com returned an error code: %1.")
     139            0 :                   .arg("500 Internal server error").toStdString();
     140            0 :         break;
     141            0 :     case int(cloud::Err::UnknownStatusCode): {
     142            0 :         if (const auto status = ret.data<int>("status", -1); status != -1) {
     143              :             //: %1 will be replaced with the error code that MuseScore.com returned, which is a number.
     144            0 :             message = muse::qtrc("project/cloud", "MuseScore.com returned an unknown error code: %1.")
     145            0 :                       .arg(status).toStdString();
     146              :         } else {
     147            0 :             message = muse::trc("project/cloud", "MuseScore.com returned an unknown error code.");
     148              :         }
     149            0 :     } break;
     150              :     }
     151              : 
     152            0 :     if (withHelp) {
     153            0 :         message += "\n\n" + muse::trc("project/cloud", "Please try again later, or get help for this problem on MuseScore.com.");
     154              :     }
     155              : 
     156            0 :     return message;
     157            0 : }
     158              : 
     159            4 : void OpenProjectScenario::showCloudOpenError(const Ret& ret) const
     160              : {
     161            4 :     std::string title = muse::trc("project", "Your score could not be opened");
     162            4 :     std::string message;
     163              : 
     164            4 :     switch (ret.code()) {
     165            1 :     case int(Err::InvalidCloudScoreId):
     166            1 :         message = muse::trc("project", "This score is invalid.");
     167            1 :         break;
     168            0 :     case int(Err::FileOpenError):
     169            0 :         message = muse::trc("project/cloud", "The file could not be downloaded to your disk.");
     170            0 :         break;
     171            0 :     case int(cloud::Err::Status403_AccountNotActivated):
     172            0 :         message = muse::trc("project/cloud", "Your MuseScore.com account needs to be verified first. "
     173            0 :                                              "Please activate your account via the link in the activation email.");
     174            0 :         break;
     175            0 :     case int(cloud::Err::Status403_NotOwner):
     176            0 :         message = muse::trc("project/cloud", "This score does not belong to this account. To access this score, make sure you are logged in "
     177            0 :                                              "to the desktop app with the account to which this score belongs.");
     178            0 :         break;
     179            0 :     case int(cloud::Err::Status404_NotFound):
     180            0 :         message = muse::trc("project/cloud", "The score could not be found, or cannot be accessed by your account.");
     181            0 :         break;
     182              : 
     183            0 :     case int(cloud::Err::Status400_InvalidRequest):
     184              :     case int(cloud::Err::Status401_AuthorizationRequired):
     185              :     case int(cloud::Err::Status422_ValidationFailed):
     186              :     case int(cloud::Err::Status429_RateLimitExceeded):
     187              :     case int(cloud::Err::Status500_InternalServerError):
     188              :     case int(cloud::Err::UnknownStatusCode):
     189            0 :         message = cloudStatusCodeErrorMessage(ret);
     190            0 :         break;
     191              : 
     192            1 :     case int(cloud::Err::NetworkError):
     193            1 :         message = muse::mtrc("project/cloud", "Could not connect to <a href=\"%1\">MuseScore.com</a>. "
     194              :                                               "Please check your internet connection or try again later.")
     195            1 :                   .arg(u"https://musescore.com").toStdString();
     196            1 :         break;
     197            2 :     default:
     198            2 :         message = muse::trc("project/cloud", "Please try again later.");
     199            2 :         break;
     200              :     }
     201              : 
     202            4 :     interactive()->warning(title, message);
     203            4 : }
     204              : 
     205            5 : bool OpenProjectScenario::isUrlSupported(const QUrl& url) const
     206              : {
     207            5 :     if (url.isLocalFile()) {
     208            2 :         return isFileSupported(muse::io::path_t(url));
     209              :     }
     210              : 
     211            3 :     if (url.scheme() == MUSESCORE_URL_SCHEME) {
     212            2 :         if (url.host() == OPEN_SCORE_URL_HOSTNAME) {
     213            1 :             return true;
     214              :         }
     215              :     }
     216              : 
     217            2 :     return false;
     218              : }
     219              : 
     220            5 : bool OpenProjectScenario::isFileSupported(const muse::io::path_t& path) const
     221              : {
     222            5 :     std::string suffix = io::suffix(path);
     223            5 :     if (engraving::isMuseScoreFile(suffix)) {
     224            2 :         return true;
     225              :     }
     226              : 
     227            3 :     if (readers()->reader(suffix)) {
     228            1 :         return true;
     229              :     }
     230              : 
     231            2 :     return false;
     232            5 : }
     233              : 
     234            1 : Promise<Ret> OpenProjectScenario::openProject(const muse::rcommand::Params& params)
     235              : {
     236            1 :     const QString url = params.at("url").toQString();
     237            1 :     const QString displayNameOverride = params.at("display_name").toQString();
     238              : 
     239            2 :     return openProject(ProjectFile(QUrl(url), displayNameOverride))
     240            1 :            .then<Ret>(this, [](const Ret& ret, auto resolve) {
     241            1 :         if (!ret) {
     242            0 :             LOGE() << ret.toString();
     243              :         }
     244              : 
     245            1 :         return resolve(ret);
     246            2 :     });
     247            1 : }
     248              : 
     249           10 : Promise<Ret> OpenProjectScenario::openProject(const ProjectFile& file)
     250              : {
     251           10 :     LOGI() << "Try open project: url = " << file.url.toString() << ", displayNameOverride = " << file.displayNameOverride;
     252              : 
     253           10 :     if (file.isNull()) {
     254            4 :         return selectScoreOpeningFile().then<Ret>(this, [this](const io::path_t& askedPath, auto resolve) {
     255            2 :             if (askedPath.empty()) {
     256            1 :                 return resolve(make_ret(Ret::Code::Cancel));
     257              :             }
     258              : 
     259            1 :             configuration()->setLastOpenedProjectsPath(io::dirpath(askedPath));
     260              : 
     261            1 :             openProject(askedPath).onResolve(this, [resolve](const Ret& ret) {
     262            1 :                 (void)resolve(ret);
     263              :             });
     264              : 
     265            1 :             return Promise<Ret>::dummy_result();
     266            2 :         });
     267              :     }
     268              : 
     269            8 :     if (file.url.isLocalFile()) {
     270            2 :         return openProject(file.path(), file.displayNameOverride);
     271              :     }
     272              : 
     273            6 :     if (file.url.scheme() == MUSESCORE_URL_SCHEME) {
     274            5 :         return openMuseScoreUrl(file.url);
     275              :     }
     276              : 
     277            1 :     return resolvedPromise(make_ret(Err::UnsupportedUrl));
     278              : }
     279              : 
     280           24 : Promise<Ret> OpenProjectScenario::openProject(const muse::io::path_t& givenPath, const QString& displayNameOverride)
     281              : {
     282           48 :     return runIfNotBusy(BusyStatus::Opening, [this, givenPath, displayNameOverride]() -> Promise<Ret> {
     283              :         //! Step 1. Take absolute path
     284           23 :         muse::io::path_t actualPath = fileSystem()->absoluteFilePath(givenPath);
     285           23 :         if (actualPath.empty()) {
     286              :             // We assume that a valid path has been specified to this method
     287            1 :             return resolvedPromise(make_ret(Ret::Code::UnknownError));
     288              :         }
     289              : 
     290              :         //! Step 2. If the project is already open in the current window, then just switch to showing the notation
     291           22 :         if (isProjectOpened(actualPath)) {
     292            1 :             return resolvedPromise(finishOpening());
     293              :         }
     294              : 
     295              :         //! Step 3. Check, if the project already opened in another window, then activate the window with the project
     296           21 :         if (multiwindowsProvider()->isProjectAlreadyOpened(actualPath)) {
     297            1 :             multiwindowsProvider()->activateWindowWithProject(actualPath);
     298            1 :             return resolvedPromise(make_ok());
     299              :         }
     300              : 
     301              :         //! Step 4. Check, if a any project is already open in the current window,
     302              :         //! then create a new instance
     303           20 :         if (projectSession()->project()) {
     304            3 :             QStringList args;
     305            3 :             args << actualPath.toQString();
     306              : 
     307            3 :             if (!displayNameOverride.isEmpty()) {
     308            2 :                 args << "--score-display-name-override" << displayNameOverride;
     309              :             }
     310              : 
     311            3 :             multiwindowsProvider()->openNewWindow(args);
     312            3 :             return resolvedPromise(make_ok());
     313            3 :         }
     314              : 
     315              :         //! Step 5. If it's a cloud project, download the latest version
     316           17 :         if (configuration()->isCloudProject(actualPath) && !configuration()->isLegacyCloudProject(actualPath)) {
     317            6 :             return museScoreComService()->authorization()->checkCloudIsAvailable()
     318            6 :                    .then<Ret>(this, [this, actualPath, displayNameOverride](const Ret& isCloudAvailable, auto resolve) {
     319            3 :                 if (isCloudAvailable) {
     320            2 :                     downloadAndOpenCloudProject(configuration()->cloudScoreIdFromPath(actualPath))
     321            1 :                     .onResolve(this, [resolve](const Ret& ret) {
     322            0 :                         (void)resolve(ret);
     323              :                     });
     324              : 
     325            1 :                     return Promise<Ret>::dummy_result();
     326              :                 }
     327              : 
     328            2 :                 if (fileSystem()->exists(actualPath)) {
     329            1 :                     doOpenCloudProjectOffline(actualPath, displayNameOverride).onResolve(this, [resolve](const Ret& ret) {
     330            1 :                         (void)resolve(ret);
     331              :                     });
     332              : 
     333            1 :                     return Promise<Ret>::dummy_result();
     334              :                 }
     335              : 
     336            1 :                 Ret ret = make_ret(cloud::Err::NetworkError);
     337            1 :                 showCloudOpenError(ret);
     338            1 :                 return resolve(ret);
     339            4 :             });
     340              :         }
     341              : 
     342              :         //! Step 6. Open project in the current window
     343           14 :         return doOpenProject(actualPath);
     344           71 :     });
     345              : }
     346              : 
     347           18 : Promise<RetVal<INotationProjectPtr> > OpenProjectScenario::loadProject(const muse::io::path_t& filePath)
     348              : {
     349           19 :     TRACEFUNC;
     350              : 
     351           18 :     const auto project = projectCreator()->newProject(iocContext());
     352           18 :     IF_ASSERT_FAILED(project) {
     353            0 :         return resolvedPromise(RetVal<INotationProjectPtr>(make_ret(Ret::Code::InternalError)));
     354              :     }
     355              : 
     356           18 :     const bool hasUnsavedChanges = projectAutoSaver()->projectHasUnsavedChanges(filePath);
     357              : 
     358           18 :     const muse::io::path_t loadPath = hasUnsavedChanges ? projectAutoSaver()->projectAutoSavePath(filePath) : filePath;
     359           18 :     const std::string format = io::suffix(filePath);
     360              : 
     361           18 :     return loadWithFallback(project, loadPath, format)
     362           36 :            .then<RetVal<INotationProjectPtr> >(this, [this, project, filePath, hasUnsavedChanges](const Ret& result, auto resolve) {
     363           18 :         if (!result) {
     364            8 :             return resolve(RetVal<INotationProjectPtr>(result));
     365              :         }
     366              : 
     367           10 :         if (hasUnsavedChanges) {
     368              :             //! NOTE: redirect the project to the original file path
     369            1 :             project->setPath(filePath);
     370              : 
     371            1 :             project->markAsUnsaved();
     372              :         }
     373              : 
     374           10 :         if (projectAutoSaver()->isAutosaveOfNewlyCreatedProject(filePath)) {
     375            1 :             project->markAsNewlyCreated();
     376              :         }
     377              : 
     378           10 :         return resolve(RetVal<INotationProjectPtr>::make_ok(project));
     379           18 :     });
     380           18 : }
     381              : 
     382           18 : Promise<Ret> OpenProjectScenario::loadWithFallback(const std::shared_ptr<INotationProject>& project,
     383              :                                                    const muse::io::path_t& loadPath,
     384              :                                                    const std::string& format)
     385              : {
     386           18 :     Ret result = project->load(loadPath, OpenParams(), format);
     387              : 
     388           18 :     if (result || result.code() == static_cast<int>(Ret::Code::Cancel)) {
     389           11 :         return resolvedPromise(result);
     390              :     }
     391              : 
     392           14 :     return shouldRetryLoadAfterError(result, loadPath)
     393           14 :            .then<Ret>(this, [project, loadPath, format, result](bool forceLoad, auto resolve) {
     394            7 :         if (!forceLoad) {
     395            4 :             return resolve(result);
     396              :         }
     397              : 
     398            3 :         OpenParams params;
     399            3 :         params.forceMode = forceLoad;
     400            3 :         return resolve(project->load(loadPath, params, format));
     401           10 :     });
     402           18 : }
     403              : 
     404           14 : Promise<Ret> OpenProjectScenario::doOpenProject(const muse::io::path_t& filePath)
     405              : {
     406           15 :     TRACEFUNC;
     407              : 
     408           28 :     return loadProject(filePath).then<Ret>(this, [this, filePath](const RetVal<INotationProjectPtr>& rv, auto resolve) {
     409           14 :         if (!rv.ret) {
     410            8 :             return resolve(rv.ret);
     411              :         }
     412              : 
     413            6 :         INotationProjectPtr project = rv.val;
     414              : 
     415            6 :         bool isNewlyCreated = projectAutoSaver()->isAutosaveOfNewlyCreatedProject(filePath);
     416            6 :         if (!isNewlyCreated) {
     417            5 :             recentFilesController()->prependRecentFile(makeRecentFile(project));
     418              :         }
     419              : 
     420            6 :         projectSession()->setProject(project);
     421              : 
     422            6 :         return resolve(finishOpening());
     423           34 :     });
     424           14 : }
     425              : 
     426            3 : Promise<Ret> OpenProjectScenario::doOpenCloudProject(const muse::io::path_t& filePath, const CloudProjectInfo& info, bool isOwner)
     427              : {
     428            6 :     return loadProject(filePath).then<Ret>(this, [this, filePath, info, isOwner](const RetVal<INotationProjectPtr>& rv, auto resolve) {
     429            3 :         if (!rv.ret) {
     430            0 :             return resolve(rv.ret);
     431              :         }
     432              : 
     433            3 :         INotationProjectPtr project = rv.val;
     434              : 
     435            3 :         if (isOwner) {
     436            2 :             project->setCloudInfo(info);
     437              :         } else {
     438            1 :             project->markAsNewlyCreated();
     439            1 :             project->setCloudInfo(CloudProjectInfo());
     440              :         }
     441              : 
     442            3 :         bool isNewlyCreated = projectAutoSaver()->isAutosaveOfNewlyCreatedProject(filePath)
     443            3 :                               || !isOwner;
     444            3 :         if (!isNewlyCreated) {
     445            2 :             recentFilesController()->prependRecentFile(makeRecentFile(project));
     446              :         }
     447              : 
     448            3 :         projectSession()->setProject(project);
     449              : 
     450            3 :         return resolve(finishOpening());
     451            9 :     });
     452              : }
     453              : 
     454            1 : Promise<Ret> OpenProjectScenario::doOpenCloudProjectOffline(const muse::io::path_t& filePath, const QString& displayNameOverride)
     455              : {
     456            2 :     return loadProject(filePath)
     457            2 :            .then<Ret>(this, [this, displayNameOverride](const RetVal<INotationProjectPtr>& rv, auto resolve) {
     458            1 :         if (!rv.ret) {
     459            0 :             return resolve(rv.ret);
     460              :         }
     461              : 
     462            1 :         INotationProjectPtr project = rv.val;
     463            1 :         CloudProjectInfo info = project->cloudInfo();
     464            1 :         info.name = displayNameOverride;
     465            1 :         project->setCloudInfo(info);
     466              : 
     467            1 :         recentFilesController()->prependRecentFile(makeRecentFile(project));
     468            1 :         projectSession()->setProject(project);
     469              : 
     470            1 :         return resolve(finishOpening());
     471            3 :     });
     472              : }
     473              : 
     474           11 : Ret OpenProjectScenario::finishOpening()
     475              : {
     476           11 :     const Uri pageUri = resolveNotationPageUri();
     477              : 
     478              :     //! Show MuseSounds / MuseSampler update if need
     479           11 :     auto showUpdateNotification = [this]() {
     480           11 :         QTimer::singleShot(1000, [this]() {
     481            0 :             if (museSoundsCheckUpdateScenario()->hasUpdate()) {
     482            0 :                 museSoundsCheckUpdateScenario()->showUpdate();
     483            0 :             } else if (!museSamplerCheckStatusScenario()->alreadyChecked()) {
     484            0 :                 museSamplerCheckStatusScenario()->checkAndShowStatusIfNeed();
     485              :             }
     486            0 :         });
     487           11 :     };
     488              : 
     489           11 :     if (pageUri == NOTATION_PAGE_URI) {
     490           11 :         if (interactive()->isOpened(NOTATION_PAGE_URI).val) {
     491           11 :             showUpdateNotification();
     492              :         } else {
     493            0 :             async::Channel<Uri> opened = interactive()->opened();
     494            0 :             opened.onReceive(this, [this, opened, showUpdateNotification](const Uri&) {
     495            0 :                 async::Async::call(this, [this, opened, showUpdateNotification]() {
     496            0 :                     async::Channel<Uri> mut = opened;
     497            0 :                     mut.disconnect(this);
     498              : 
     499            0 :                     showUpdateNotification();
     500            0 :                 });
     501            0 :             });
     502            0 :         }
     503              :     }
     504              : 
     505           22 :     return openPageIfNeed(pageUri);
     506           11 : }
     507              : 
     508           15 : Uri OpenProjectScenario::resolveNotationPageUri() const
     509              : {
     510           15 :     INotationProjectPtr project = projectSession()->project();
     511           15 :     if (!project || !project->cloudInfo().isValid()) {
     512           13 :         return NOTATION_PAGE_URI;
     513              :     }
     514              : 
     515            2 :     int scoreId = static_cast<int>(muse::cloud::idFromCloudUrl(project->cloudInfo().sourceUrl).toUint64());
     516            2 :     if (convertFileToScoreScenario()->isAwaitingReview(scoreId)) {
     517            1 :         return NOTATION_REVIEW_PAGE_URI;
     518              :     }
     519              : 
     520            1 :     return NOTATION_PAGE_URI;
     521           15 : }
     522              : 
     523           12 : Promise<Ret> OpenProjectScenario::downloadAndOpenCloudProject(int scoreId, const QString& hash, const QString& secret, bool isOwner)
     524              : {
     525           24 :     return runIfNotBusy(BusyStatus::Downloading, [this, scoreId, hash, secret, isOwner]() -> Promise<Ret> {
     526           12 :         if (!scoreId) {
     527              :             // Might happen when user tries to open score that saved as a cloud score but upload did not fully succeed
     528            1 :             LOGE() << "invalid cloud score id";
     529              : 
     530            1 :             Ret ret = make_ret(Err::InvalidCloudScoreId);
     531            1 :             showCloudOpenError(ret);
     532            1 :             return resolvedPromise(ret);
     533            1 :         }
     534              : 
     535           22 :         return ensureAuthorization().then<Ret>(this, [this, scoreId, hash, secret, isOwner](const RetVal<Val>& auth, auto resolve) {
     536           11 :             if (!auth.ret) {
     537            1 :                 return resolve(auth.ret);
     538              :             }
     539              : 
     540           10 :             doDownloadAndOpenCloudProject(scoreId, hash, secret, isOwner).onResolve(this, [resolve](const Ret& ret) {
     541            5 :                 (void)resolve(ret);
     542              :             });
     543              : 
     544           10 :             return Promise<Ret>::dummy_result();
     545           11 :         });
     546           24 :     });
     547              : }
     548              : 
     549           10 : Promise<Ret> OpenProjectScenario::doDownloadAndOpenCloudProject(int scoreId, const QString& hash, const QString& secret, bool isOwner)
     550              : {
     551           10 :     muse::io::path_t localPath = configuration()->cloudProjectPath(scoreId);
     552              : 
     553           10 :     if (!isOwner) {
     554            4 :         return downloadCloudProject(scoreId, localPath, hash, secret, CloudProjectInfo(), isOwner);
     555              :     }
     556              : 
     557           16 :     return museScoreComService()->downloadScoreInfo(scoreId)
     558           16 :            .then<Ret>(this, [this, scoreId, localPath, hash, secret, isOwner](const RetVal<muse::cloud::ScoreInfo>& scoreInfo,
     559              :                                                                               auto resolve) {
     560            8 :         if (!scoreInfo.ret) {
     561            1 :             LOGE() << "Error while downloading score info: " << scoreInfo.ret.toString();
     562            1 :             showCloudOpenError(scoreInfo.ret);
     563            1 :             return resolve(scoreInfo.ret);
     564              :         }
     565              : 
     566            7 :         CloudProjectInfo info;
     567            7 :         info.name = scoreInfo.val.title;
     568            7 :         info.visibility = scoreInfo.val.visibility;
     569            7 :         info.sourceUrl = scoreInfo.val.url;
     570            7 :         info.revisionId = scoreInfo.val.revisionId;
     571              : 
     572            7 :         RetVal<CloudProjectInfo> localInfo = mscMetaReader()->readCloudProjectInfo(localPath);
     573              : 
     574            7 :         if (localInfo.ret) {
     575            2 :             if (localInfo.val.revisionId == scoreInfo.val.revisionId) {
     576            1 :                 doOpenCloudProject(localPath, info, isOwner).onResolve(this, [resolve](const Ret& ret) {
     577            1 :                     (void)resolve(ret);
     578              :                 });
     579              : 
     580            1 :                 return Promise<Ret>::dummy_result();
     581              :             }
     582              :         } else {
     583            5 :             LOGE() << localInfo.ret;
     584              :         }
     585              : 
     586            6 :         downloadCloudProject(scoreId, localPath, hash, secret, info, isOwner).onResolve(this, [resolve](const Ret& ret) {
     587            2 :             (void)resolve(ret);
     588              :         });
     589              : 
     590            6 :         return Promise<Ret>::dummy_result();
     591           15 :     });
     592           10 : }
     593              : 
     594            8 : Promise<Ret> OpenProjectScenario::downloadCloudProject(int scoreId, const muse::io::path_t& localPath, const QString& hash,
     595              :                                                        const QString& secret, const CloudProjectInfo& info, bool isOwner)
     596              : {
     597           16 :     return async::make_promise<Ret>([this, scoreId, localPath, hash, secret, info, isOwner](auto resolve) {
     598              :         // TODO(cloud): conflict checking (don't recklessly overwrite the existing file)
     599            8 :         auto projectData = std::make_shared<QFile>(localPath.toQString());
     600            8 :         if (!projectData->open(QIODevice::WriteOnly)) {
     601            0 :             Ret ret = make_ret(Err::FileOpenError);
     602            0 :             showCloudOpenError(ret);
     603            0 :             return resolve(ret);
     604            0 :         }
     605              : 
     606            8 :         m_projectBeingDownloaded.scoreId = scoreId;
     607            8 :         m_projectBeingDownloaded.progress = museScoreComService()->downloadScore(scoreId, projectData, hash, secret);
     608              : 
     609            8 :         m_projectBeingDownloaded.progress->finished().onReceive(this, [this, localPath, info, isOwner, resolve](const ProgressResult& res) {
     610            3 :             m_projectBeingDownloaded = {};
     611            3 :             m_projectBeingDownloadedChanged.notify();
     612              : 
     613            3 :             if (!res.ret) {
     614            1 :                 LOGE() << res.ret.toString();
     615            1 :                 showCloudOpenError(res.ret);
     616            1 :                 (void)resolve(res.ret);
     617            1 :                 return;
     618              :             }
     619              : 
     620            2 :             doOpenCloudProject(localPath, info, isOwner).onResolve(this, [resolve](const Ret& ret) {
     621            2 :                 (void)resolve(ret);
     622              :             });
     623              :         });
     624              : 
     625            8 :         m_projectBeingDownloadedChanged.notify();
     626              : 
     627            8 :         return Promise<Ret>::dummy_result();
     628           24 :     });
     629              : }
     630              : 
     631            5 : Promise<Ret> OpenProjectScenario::openMuseScoreUrl(const QUrl& url)
     632              : {
     633            5 :     if (url.host() == OPEN_SCORE_URL_HOSTNAME) {
     634            4 :         return openScoreFromMuseScoreCom(url);
     635              :     }
     636              : 
     637            1 :     return resolvedPromise(make_ret(Err::UnsupportedUrl));
     638              : }
     639              : 
     640            4 : Promise<Ret> OpenProjectScenario::openScoreFromMuseScoreCom(const QUrl& url)
     641              : {
     642            4 :     if (isBusy(BusyStatus::Downloading)) {
     643              :         // TODO: instead of ignoring the open request, queue it?
     644            0 :         return resolvedPromise(make_ret(Ret::Code::Busy));
     645              :     }
     646              : 
     647            8 :     return runIfNotBusy(BusyStatus::Opening, [this, url]() -> Promise<Ret> {
     648              :         // Retrieve score id from URL
     649            4 :         bool ok = false;
     650            4 :         int scoreId = url.fileName().toInt(&ok);
     651            4 :         if (!ok || scoreId <= 0) {
     652            1 :             return resolvedPromise(make_ret(Err::MalformedOpenScoreUrl));
     653              :         }
     654              : 
     655              :         // Ensure logged in
     656            6 :         return ensureAuthorization().then<Ret>(this, [this, url, scoreId](const RetVal<Val>& auth, auto resolve) {
     657            3 :             if (!auth.ret) {
     658            0 :                 return resolve(auth.ret);
     659              :             }
     660              : 
     661              :             // Check if user is owner
     662            3 :             museScoreComService()->downloadScoreInfo(scoreId)
     663            3 :             .onResolve(this, [this, url, scoreId, resolve](const RetVal<muse::cloud::ScoreInfo>& scoreInfo) {
     664            3 :                 if (!scoreInfo.ret) {
     665            0 :                     LOGE() << "Error while downloading score info: " << scoreInfo.ret.toString();
     666            0 :                     showCloudOpenError(scoreInfo.ret);
     667              : 
     668            0 :                     (void)resolve(scoreInfo.ret);
     669            0 :                     return;
     670              :                 }
     671              : 
     672            3 :                 openScoreFromMuseScoreCom(url, scoreId, scoreInfo.val).onResolve(this, [resolve](const Ret& ret) {
     673            2 :                     (void)resolve(ret);
     674              :                 });
     675              :             });
     676              : 
     677            3 :             return Promise<Ret>::dummy_result();
     678            3 :         });
     679            4 :     });
     680              : }
     681              : 
     682            3 : Promise<Ret> OpenProjectScenario::openScoreFromMuseScoreCom(const QUrl& url, int scoreId, const muse::cloud::ScoreInfo& scoreInfo)
     683              : {
     684            6 :     return async::make_promise<Ret>([this, url, scoreId, scoreInfo](auto resolve) {
     685            3 :         bool isOwner = QString::number(scoreInfo.owner.id) == museScoreComService()->authorization()->accountInfo().id;
     686              : 
     687              :         // If yes, score will be opened as regular cloud score; check if not yet opened
     688            3 :         if (isOwner) {
     689            1 :             muse::io::path_t projectPath = configuration()->cloudProjectPath(scoreId);
     690              : 
     691              :             // either in this instance
     692            1 :             if (isProjectOpened(projectPath)) {
     693            0 :                 return resolve(finishOpening());
     694              :             }
     695              : 
     696              :             // or in another one
     697            1 :             if (multiwindowsProvider()->isProjectAlreadyOpened(projectPath)) {
     698            1 :                 multiwindowsProvider()->activateWindowWithProject(projectPath);
     699            1 :                 return resolve(muse::make_ok());
     700              :             }
     701            1 :         }
     702              : 
     703              :         // Check if this instance already has an open project
     704            2 :         if (projectSession()->project()) {
     705            1 :             QStringList args;
     706            1 :             args << url.toString();
     707              : 
     708            1 :             if (!scoreInfo.title.isEmpty()) {
     709            1 :                 args << "--score-display-name-override" << scoreInfo.title;
     710              :             }
     711              : 
     712            1 :             multiwindowsProvider()->openNewWindow(args);
     713            1 :             return resolve(muse::make_ok());
     714            1 :         }
     715              : 
     716            1 :         QUrlQuery query(url);
     717            1 :         QString hash = query.queryItemValue("h");
     718            1 :         QString secret = query.queryItemValue("secret");
     719              : 
     720            1 :         downloadAndOpenCloudProject(scoreId, hash, secret, isOwner).onResolve(this, [resolve](const Ret& ret) {
     721            0 :             (void)resolve(ret);
     722              :         });
     723              : 
     724            1 :         return Promise<Ret>::dummy_result();
     725            7 :     });
     726              : }
     727              : 
     728            0 : const ProjectBeingDownloaded& OpenProjectScenario::projectBeingDownloaded() const
     729              : {
     730            0 :     return m_projectBeingDownloaded;
     731              : }
     732              : 
     733            0 : muse::async::Notification OpenProjectScenario::projectBeingDownloadedChanged() const
     734              : {
     735            0 :     return m_projectBeingDownloadedChanged;
     736              : }
     737              : 
     738           11 : Ret OpenProjectScenario::openPageIfNeed(Uri pageUri)
     739              : {
     740           11 :     if (!interactive()->isOpened(pageUri).val) {
     741            0 :         interactive()->open(pageUri);
     742              :     }
     743           11 :     return make_ret(Ret::Code::Ok);
     744              : }
     745              : 
     746           23 : bool OpenProjectScenario::isProjectOpened(const muse::io::path_t& scorePath) const
     747              : {
     748           23 :     auto project = projectSession()->project();
     749           23 :     if (!project) {
     750           19 :         return false;
     751              :     }
     752              : 
     753            4 :     LOGD() << "project->path: " << project->path() << ", check path: " << scorePath;
     754            4 :     if (project->path() == scorePath) {
     755            1 :         return true;
     756              :     }
     757              : 
     758            3 :     return false;
     759           23 : }
     760              : 
     761            8 : RecentFile OpenProjectScenario::makeRecentFile(INotationProjectPtr project)
     762              : {
     763            8 :     RecentFile file;
     764            8 :     file.path = project->path();
     765              : 
     766            8 :     if (project->isCloudProject()) {
     767            0 :         file.displayNameOverride = project->cloudInfo().name;
     768              :     }
     769              : 
     770            8 :     return file;
     771            0 : }
     772              : 
     773            0 : Promise<Ret> OpenProjectScenario::revertToLastSaved()
     774              : {
     775            0 :     TRACEFUNC;
     776              : 
     777            0 :     auto currentProject = projectSession()->project();
     778            0 :     if (!currentProject) {
     779            0 :         return resolvedPromise(make_ret(Err::NoProjectError));
     780              :     }
     781              : 
     782            0 :     muse::io::path_t filePath = currentProject->path();
     783              : 
     784            0 :     bool hasUnsavedChanges = projectAutoSaver()->projectHasUnsavedChanges(filePath);
     785            0 :     if (hasUnsavedChanges) {
     786            0 :         muse::io::path_t autoSavePath = projectAutoSaver()->projectAutoSavePath(filePath);
     787            0 :         fileSystem()->remove(autoSavePath);
     788            0 :     }
     789              : 
     790            0 :     return runIfNotBusy(BusyStatus::Opening, [this, filePath]() {
     791            0 :         return doOpenProject(filePath);
     792            0 :     });
     793            0 : }
     794              : 
     795            7 : Promise<bool> OpenProjectScenario::shouldRetryLoadAfterError(const Ret& ret, const muse::io::path_t& filepath)
     796              : {
     797            7 :     if (ret) {
     798            0 :         return resolvedPromise(true);
     799              :     }
     800              : 
     801            7 :     switch (static_cast<engraving::Err>(ret.code())) {
     802            2 :     case engraving::Err::FileTooOld:
     803              :     case engraving::Err::FileOld300Format:
     804            2 :         return askIfUserAgreesToOpenProjectWithIncompatibleVersion(ret.text());
     805            2 :     case engraving::Err::FileTooNew:
     806            2 :         warnFileTooNew(filepath);
     807            2 :         return resolvedPromise(configuration()->disableVersionChecking());
     808            1 :     case engraving::Err::FileCorrupted:
     809            2 :         return askIfUserAgreesToOpenCorruptedProject(io::filename(filepath).toString(), ret.text());
     810            1 :     case engraving::Err::FileCriticallyCorrupted:
     811            1 :         warnProjectCriticallyCorrupted(io::filename(filepath).toString(), ret.text());
     812            2 :         return resolvedPromise(false);
     813            1 :     default:
     814            1 :         warnProjectCannotBeOpened(ret, filepath);
     815            1 :         break;
     816              :     }
     817              : 
     818            2 :     return resolvedPromise(false);
     819              : }
     820              : 
     821            2 : Promise<bool> OpenProjectScenario::askIfUserAgreesToOpenProjectWithIncompatibleVersion(const std::string& errorText)
     822              : {
     823            2 :     IInteractive::ButtonData openAnywayBtn(IInteractive::Button::CustomButton, muse::trc("project", "Open anyway"), true /*accent*/);
     824              : 
     825           12 :     return interactive()->warning(errorText, IInteractive::Text(), {
     826            2 :         interactive()->buttonData(IInteractive::Button::Cancel),
     827              :         openAnywayBtn
     828              :     }, openAnywayBtn.btn)
     829            4 :            .then<bool>(this, [openAnywayBtn](const IInteractive::Result& res, auto resolve) {
     830            2 :         return resolve(res.isButton(openAnywayBtn.btn));
     831            4 :     });
     832            6 : }
     833              : 
     834            2 : void OpenProjectScenario::warnFileTooNew(const muse::io::path_t& filepath)
     835              : {
     836            8 :     interactive()->error(muse::qtrc("project", "Cannot read file %1").arg(io::toNativeSeparators(filepath).toQString()).toStdString(),
     837            2 :                          muse::mtrc("project", "This file was saved using a newer version of MuseScore Studio. "
     838              :                                                "Please visit <a href=\"%1\">MuseScore.org</a> to obtain the latest version.")
     839            4 :                          .arg(u"https://musescore.org").toStdString());
     840            2 : }
     841              : 
     842            1 : Promise<bool> OpenProjectScenario::askIfUserAgreesToOpenCorruptedProject(const String& projectName, const std::string& errorText)
     843              : {
     844            1 :     std::string title = muse::mtrc("project", "File “%1” is corrupted").arg(projectName).toStdString();
     845            1 :     IInteractive::Text text;
     846            1 :     text.text = muse::trc("project", "This file contains errors that could cause MuseScore Studio to malfunction.");
     847            1 :     text.detailedText = errorText;
     848              : 
     849            1 :     IInteractive::ButtonData openAnywayBtn(IInteractive::Button::CustomButton, muse::trc("project", "Open anyway"), true /*accent*/);
     850              : 
     851            6 :     return interactive()->warning(title, text, {
     852            1 :         interactive()->buttonData(IInteractive::Button::Cancel),
     853              :         openAnywayBtn
     854              :     }, openAnywayBtn.btn)
     855            2 :            .then<bool>(this, [openAnywayBtn](const IInteractive::Result& res, auto resolve) {
     856            1 :         return resolve(res.isButton(openAnywayBtn.btn));
     857            2 :     });
     858            3 : }
     859              : 
     860            1 : void OpenProjectScenario::warnProjectCriticallyCorrupted(const String& projectName, const std::string& errorText)
     861              : {
     862            1 :     std::string title = muse::mtrc("project", "File “%1” is corrupted and cannot be opened").arg(projectName).toStdString();
     863            1 :     IInteractive::Text text;
     864            1 :     text.text = muse::trc("project", "Get help for this issue on MuseScore.org.");
     865            1 :     text.detailedText = errorText;
     866              : 
     867            1 :     IInteractive::ButtonData getHelpBtn(IInteractive::Button::CustomButton, muse::trc("project", "Get help"), true /*accent*/);
     868              : 
     869            6 :     interactive()->error(title, text, {
     870            1 :         interactive()->buttonData(IInteractive::Button::Cancel),
     871              :         getHelpBtn
     872            1 :     }, getHelpBtn.btn).onResolve(this, [this, getHelpBtn](const IInteractive::Result& res) {
     873            1 :         if (res.isButton(getHelpBtn.btn)) {
     874            0 :             platformInteractive()->openUrl(configuration()->supportForumUrl());
     875              :         }
     876            1 :     });
     877            3 : }
     878              : 
     879            1 : void OpenProjectScenario::warnProjectCannotBeOpened(const Ret& ret, const muse::io::path_t& filepath)
     880              : {
     881            1 :     std::string title = muse::mtrc("project", "Cannot read file %1").arg(io::toNativeSeparators(filepath).toString()).toStdString();
     882            1 :     std::string body;
     883              : 
     884            1 :     switch (ret.code()) {
     885            1 :     case int(engraving::Err::FileNotFound):
     886            1 :         body = muse::trc("project", "This file does not exist or cannot be accessed at the moment.");
     887            1 :         break;
     888            0 :     case int(engraving::Err::FileOpenError):
     889            0 :         body = muse::trc("project",
     890            0 :                          "This file could not be opened. Please make sure that MuseScore Studio has permission to read this file.");
     891            0 :         break;
     892            0 :     default:
     893            0 :         if (!ret.text().empty()) {
     894            0 :             body = ret.text();
     895              :         } else {
     896            0 :             body = muse::trc("project", "An error occurred while reading this file.");
     897              :         }
     898              :     }
     899              : 
     900            1 :     interactive()->error(title, body);
     901            1 : }
     902              : 
     903            2 : async::Promise<io::path_t> OpenProjectScenario::selectScoreOpeningFile() const
     904              : {
     905              :     std::string allExt = "*.mscz *.mxl *.musicxml *.xml *.mid *.midi *.kar *.md *.mgu *.sgu *.cap *.capx "
     906            2 :                          "*.ove *.scw *.bmw *.bww *.gtp *.gp3 *.gp4 *.gp5 *.gpx *.gp *.ptb *.mei *.mnx *.json *.tef *.mscx *.mscs *.mscz~";
     907              : 
     908            4 :     std::vector<std::string> filter { muse::trc("project", "All supported files") + " (" + allExt + ")",
     909            2 :                                       muse::trc("project", "MuseScore files") + " (*.mscz)",
     910            2 :                                       muse::trc("project", "MusicXML files") + " (*.mxl *.musicxml *.xml)",
     911            2 :                                       muse::trc("project", "MIDI files") + " (*.mid *.midi *.kar)",
     912            2 :                                       muse::trc("project", "MNX files [experimental]") + " (*.mnx *.json)",
     913            2 :                                       muse::trc("project", "MuseData files") + " (*.md)",
     914            2 :                                       muse::trc("project", "Capella files") + " (*.cap *.capx)",
     915            2 :                                       muse::trc("project", "BB files [experimental]") + " (*.mgu *.sgu)",
     916            2 :                                       muse::trc("project", "Overture / Score Writer files [experimental]") + " (*.ove *.scw)",
     917            2 :                                       muse::trc("project", "Bagpipe Music Writer files [experimental]") + " (*.bmw *.bww)",
     918            2 :                                       muse::trc("project", "Guitar Pro files") + " (*.gtp *.gp3 *.gp4 *.gp5 *.gpx *.gp)",
     919            2 :                                       muse::trc("project", "Power Tab Editor files [experimental]") + " (*.ptb)",
     920            2 :                                       muse::trc("project", "MEI files") + " (*.mei)",
     921            2 :                                       muse::trc("project", "TablEdit files [experimental]") + " (*.tef)",
     922            2 :                                       muse::trc("project", "Uncompressed MuseScore folders [experimental]") + " (*.mscx)",
     923            2 :                                       muse::trc("project", "MuseScore developer files") + " (*.mscs)",
     924           38 :                                       muse::trc("project", "MuseScore backup files") + " (*.mscz~)" };
     925              : 
     926            2 :     muse::io::path_t defaultDir = configuration()->lastOpenedProjectsPath();
     927              : 
     928            2 :     if (defaultDir.empty()) {
     929            2 :         defaultDir = configuration()->userProjectsPath();
     930              :     }
     931              : 
     932            2 :     if (defaultDir.empty()) {
     933            2 :         defaultDir = configuration()->defaultUserProjectsPath();
     934              :     }
     935              : 
     936            4 :     return interactive()->selectOpeningFile(muse::trc("project", "Open"), defaultDir, filter);
     937            6 : }
     938              : 
     939           14 : Promise<RetVal<Val> > OpenProjectScenario::ensureAuthorization() const
     940              : {
     941           14 :     bool userAuthorized = museScoreComService()->authorization()->userAuthorized().val;
     942              : 
     943           14 :     if (userAuthorized) {
     944           12 :         return resolvedPromise(RetVal<Val>::make_ok(Val()));
     945              :     }
     946              : 
     947            2 :     const std::string dialogText = muse::trc("project/save", "Log in or create a free account on MuseScore.com to open this score.");
     948              : 
     949            2 :     UriQuery query("muse://cloud/requireauthorization");
     950            6 :     query.addParam("text", Val(dialogText));
     951            6 :     query.addParam("cloudCode", Val(muse::cloud::MUSESCORE_COM_CLOUD_CODE));
     952            6 :     query.addParam("publishingScore", Val(false));
     953            2 :     return openDialog(query);
     954            2 : }
        

Generated by: LCOV version 2.0-1