LCOV - code coverage report
Current view: top level - src/appshell/internal - appshellcommandscontroller.cpp Coverage Total Hit
Test: filtered_coverage.info Lines: 0.0 % 249 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              : #include "appshellcommandscontroller.h"
      23              : 
      24              : #include <QApplication>
      25              : #include <QCloseEvent>
      26              : #include <QDragEnterEvent>
      27              : #include <QDragMoveEvent>
      28              : #include <QDropEvent>
      29              : #include <QFileOpenEvent>
      30              : #include <QWindow>
      31              : #include <QMimeData>
      32              : 
      33              : #include "global/async/async.h"
      34              : #include "global/types/ret.h"
      35              : #include "global/translation.h"
      36              : #include "global/containers.h"
      37              : 
      38              : #include "audio/common/soundfonttypes.h"
      39              : 
      40              : #include "../appshellcommands.h"
      41              : #include "project/projectcommands.h"
      42              : #include "playback/playbackcommands.h"
      43              : 
      44              : #include "log.h"
      45              : 
      46              : using namespace muse;
      47              : using namespace mu::appshell;
      48              : using namespace mu::playback;
      49              : 
      50              : static const std::map<rcommand::Command, DockName> s_dockToggleCommands = {
      51              :     { DOCK_TOGGLE_PLAYBACK_COMMAND, PLAYBACK_TOOLBAR_NAME },
      52              :     { DOCK_TOGGLE_NOTEINPUT_COMMAND, NOTE_INPUT_BAR_NAME },
      53              :     { DOCK_TOGGLE_PALETTES_COMMAND, PALETTES_PANEL_NAME },
      54              :     { DOCK_TOGGLE_INSTRUMENTS_COMMAND, LAYOUT_PANEL_NAME },
      55              :     { DOCK_TOGGLE_PROPERTIES_COMMAND, PROPERTIES_PANEL_NAME },
      56              :     { DOCK_TOGGLE_SELECTION_FILTER_COMMAND, SELECTION_FILTERS_PANEL_NAME },
      57              :     { DOCK_TOGGLE_UNDO_HISTORY_COMMAND, UNDO_HISTORY_PANEL_NAME },
      58              :     { DOCK_TOGGLE_NAVIGATOR_COMMAND, NOTATION_NAVIGATOR_PANEL_NAME },
      59              :     { DOCK_TOGGLE_BRAILLE_COMMAND, NOTATION_BRAILLE_PANEL_NAME },
      60              :     { DOCK_TOGGLE_TIMELINE_COMMAND, TIMELINE_PANEL_NAME },
      61              :     { DOCK_TOGGLE_MIXER_COMMAND, MIXER_PANEL_NAME },
      62              :     { DOCK_TOGGLE_PIANO_KEYBOARD_COMMAND, PIANO_KEYBOARD_PANEL_NAME },
      63              :     { DOCK_TOGGLE_PERCUSSION_COMMAND, PERCUSSION_PANEL_NAME },
      64              :     { DOCK_TOGGLE_STATUSBAR_COMMAND, NOTATION_STATUSBAR_NAME },
      65              : };
      66              : 
      67            0 : void AppshellCommandsController::preInit()
      68              : {
      69            0 :     qApp->installEventFilter(this);
      70            0 : }
      71              : 
      72            0 : void AppshellCommandsController::init()
      73              : {
      74            0 :     auto cd = commandDispatcher();
      75            0 :     cd->onRequest(this, APP_QUIT_COMMAND, [this](const rcommand::Params& params) { return quit(params); });
      76            0 :     cd->onRequest(this, APP_RESTART_COMMAND, [this]() { restart(); return muse::make_ok(); });
      77            0 :     cd->onRequest(this, APP_FULLSCREEN_COMMAND, [this]() { toggleFullScreen(); return muse::make_ok(); });
      78              : 
      79            0 :     cd->onRequest(this, APP_ABOUT_MUSESCORE_COMMAND, [this]() { openAboutDialog(); return muse::make_ok(); });
      80            0 :     cd->onRequest(this, APP_ABOUT_QT_COMMAND, [this]() { openAboutQtDialog(); return muse::make_ok(); });
      81            0 :     cd->onRequest(this, APP_ABOUT_MUSICXML_COMMAND, [this]() { openAboutMusicXMLDialog(); return muse::make_ok(); });
      82            0 :     cd->onRequest(this, APP_ONLINE_HANDBOOK_COMMAND, [this]() { openOnlineHandbookPage(); return muse::make_ok(); });
      83            0 :     cd->onRequest(this, APP_ASK_HELP_COMMAND, [this]() { openAskForHelpPage(); return muse::make_ok(); });
      84              : 
      85            0 :     cd->onRequest(this, APP_ACCESSIBILITY_STATEMENT_COMMAND, [this]() { openAccessibilityStatementPage(); return muse::make_ok(); });
      86            0 :     cd->onRequest(this, APP_PREFERENCES_COMMAND, [this]() { openPreferencesDialog(); return muse::make_ok(); });
      87            0 :     cd->onRequest(this, APP_REVERT_TO_FACTORY_COMMAND, [this]() { revertToFactorySettings(); return muse::make_ok(); });
      88            0 :     cd->onRequest(this, APP_EXTENSIONS_COMMAND, [this]() { openExtensions(); return muse::make_ok(); });
      89              : 
      90            0 :     for (const auto& [command, dockName] : s_dockToggleCommands) {
      91            0 :         cd->onRequest(this, command, [this, dockName]() {
      92            0 :             m_dockToggleRequested.send(dockName);
      93            0 :             return muse::make_ok();
      94              :         });
      95              :     }
      96            0 : }
      97              : 
      98            0 : bool AppshellCommandsController::eventFilter(QObject* watched, QEvent* event)
      99              : {
     100            0 :     if ((event->type() == QEvent::Close && watched == qWindow())
     101            0 :         || event->type() == QEvent::Quit) {
     102            0 :         event->setAccepted(false);
     103              : 
     104            0 :         quit(false);
     105              : 
     106            0 :         return true;
     107              :     }
     108              : 
     109            0 :     if (watched == qApp) {
     110            0 :         if (event->type() == QEvent::FileOpen) {
     111            0 :             const QFileOpenEvent* openEvent = static_cast<const QFileOpenEvent*>(event);
     112            0 :             const QUrl url = openEvent->url();
     113              : 
     114            0 :             if (openProjectScenario()->isUrlSupported(url)) {
     115            0 :                 if (startupScenario()->startupCompleted()) {
     116            0 :                     commandDispatcher()->dispatch(project::PROJECT_OPEN_COMMAND, { { "url", muse::Val(url.toString()) } });
     117              :                 } else {
     118            0 :                     startupScenario()->setStartupScoreFile(project::ProjectFile { url });
     119              :                 }
     120              : 
     121            0 :                 return true;
     122              :             }
     123            0 :         }
     124              :     }
     125              : 
     126            0 :     if (watched == qWindow()) {
     127            0 :         if (event->type() == QEvent::DragEnter) {
     128            0 :             if (onDragEnterEvent(static_cast<QDragEnterEvent*>(event))) {
     129            0 :                 return true;
     130              :             }
     131            0 :         } else if (event->type() == QEvent::DragMove) {
     132            0 :             if (onDragMoveEvent(static_cast<QDragMoveEvent*>(event))) {
     133            0 :                 return true;
     134              :             }
     135            0 :         } else if (event->type() == QEvent::Drop) {
     136            0 :             if (onDropEvent(static_cast<QDropEvent*>(event))) {
     137            0 :                 return true;
     138              :             }
     139              :         }
     140              :     }
     141              : 
     142            0 :     return QObject::eventFilter(watched, event);
     143            0 : }
     144              : 
     145            0 : muse::rcommand::Command AppshellCommandsController::dockToggleCommand(const DockName& dockName) const
     146              : {
     147            0 :     return muse::key(s_dockToggleCommands, dockName);
     148              : }
     149              : 
     150            0 : DockName AppshellCommandsController::commandDockName(const muse::rcommand::Command& command) const
     151              : {
     152            0 :     return muse::value(s_dockToggleCommands, command);
     153              : }
     154              : 
     155            0 : muse::async::Channel<DockName> AppshellCommandsController::dockToggleRequested() const
     156              : {
     157            0 :     return m_dockToggleRequested;
     158              : }
     159              : 
     160            0 : QWindow* AppshellCommandsController::qWindow() const
     161              : {
     162            0 :     return mainWindow() ? mainWindow()->qWindow() : nullptr;
     163              : }
     164              : 
     165            0 : AppshellCommandsController::DragTarget AppshellCommandsController::dragTarget(const QUrl& url) const
     166              : {
     167            0 :     if (openProjectScenario()->isUrlSupported(url)) {
     168            0 :         return DragTarget::ProjectFile;
     169            0 :     } else if (url.isLocalFile()) {
     170            0 :         muse::io::path_t filePath = url.toLocalFile();
     171            0 :         if (muse::audio::synth::isSoundFont(filePath)) {
     172            0 :             return DragTarget::SoundFont;
     173            0 :         } else if (extensionInstaller()->isFileSupported(filePath)) {
     174            0 :             return DragTarget::Extension;
     175              :         } else {
     176            0 :             bool scorePageWithProjectOpen = interactive()->currentUri().val == NOTATION_URI
     177            0 :                                             && projectSession()->project() != nullptr;
     178            0 :             if (!scorePageWithProjectOpen && convertFileToScoreScenario()->isFileSupported(filePath)) {
     179            0 :                 return DragTarget::ConvertibleFile;
     180              :             }
     181              :         }
     182            0 :     }
     183            0 :     return DragTarget::Unknown;
     184              : }
     185              : 
     186            0 : bool AppshellCommandsController::onDragEnterEvent(QDragEnterEvent* event)
     187              : {
     188            0 :     return onDragMoveEvent(event);
     189              : }
     190              : 
     191            0 : bool AppshellCommandsController::onDragMoveEvent(QDragMoveEvent* event)
     192              : {
     193            0 :     const QMimeData* mime = event->mimeData();
     194            0 :     QList<QUrl> urls = mime->urls();
     195            0 :     if (urls.count() > 0) {
     196            0 :         const QUrl& url = urls.front();
     197            0 :         DragTarget target = dragTarget(url);
     198            0 :         if (target != DragTarget::Unknown) {
     199            0 :             event->setDropAction(Qt::LinkAction);
     200            0 :             event->acceptProposedAction();
     201            0 :             return true;
     202              :         }
     203              :     }
     204              : 
     205            0 :     return false;
     206            0 : }
     207              : 
     208            0 : bool AppshellCommandsController::onDropEvent(QDropEvent* event)
     209              : {
     210            0 :     const QMimeData* mime = event->mimeData();
     211            0 :     QList<QUrl> urls = mime->urls();
     212            0 :     if (urls.count() > 0) {
     213            0 :         const QUrl& url = urls.front();
     214              : 
     215            0 :         bool shouldBeHandled = true;
     216            0 :         DragTarget target = dragTarget(url);
     217            0 :         switch (target) {
     218            0 :         case DragTarget::ProjectFile: {
     219            0 :             async::Async::call(this, [this, url]() {
     220            0 :                     openProjectScenario()->openProject(url)
     221            0 :                     .onResolve(this, [](const Ret& ret) {
     222            0 :                         if (!ret) {
     223            0 :                             LOGE() << ret.toString();
     224              :                         }
     225            0 :                     });
     226            0 :                 });
     227            0 :         } break;
     228            0 :         case DragTarget::SoundFont: {
     229            0 :             muse::io::path_t filePath = url.toLocalFile();
     230            0 :             async::Async::call(this, [this, filePath]() {
     231            0 :                     soundFontInstallScenario()->installSoundFont(Uri::fromLocalFile(filePath));
     232            0 :                 });
     233            0 :         } break;
     234            0 :         case DragTarget::Extension: {
     235            0 :             muse::io::path_t filePath = url.toLocalFile();
     236            0 :             async::Async::call(this, [this, filePath]() {
     237            0 :                     extensionInstaller()->installExtension(filePath);
     238            0 :                 });
     239            0 :         } break;
     240            0 :         case DragTarget::ConvertibleFile: {
     241            0 :             muse::io::paths_t paths;
     242            0 :             for (const QUrl& u : urls) {
     243            0 :                 if (u.isLocalFile()) {
     244            0 :                     paths.push_back(muse::io::path_t(u.toLocalFile()));
     245              :                 }
     246              :             }
     247            0 :             async::Async::call(this, [this, paths]() {
     248            0 :                     convertFileToScoreScenario()->convertFiles(paths);
     249            0 :                 });
     250            0 :         } break;
     251            0 :         case DragTarget::Unknown:
     252            0 :             shouldBeHandled = false;
     253            0 :             break;
     254              :         }
     255              : 
     256            0 :         if (shouldBeHandled) {
     257            0 :             event->accept();
     258              :         } else {
     259            0 :             event->ignore();
     260              :         }
     261              : 
     262            0 :         return shouldBeHandled;
     263              :     }
     264              : 
     265            0 :     return false;
     266            0 : }
     267              : 
     268            0 : muse::async::Promise<muse::Ret> AppshellCommandsController::quit(const muse::rcommand::Params& params)
     269              : {
     270            0 :     bool isAllInstances = params.at("all_instances", Val(true)).toBool();
     271            0 :     muse::io::path_t installatorPath = params.at("installer_path").toString();
     272            0 :     return quit(isAllInstances, installatorPath);
     273            0 : }
     274              : 
     275            0 : muse::async::Promise<muse::Ret> AppshellCommandsController::quit(bool isAllInstances, const muse::io::path_t& installerPath)
     276              : {
     277            0 :     if (m_quiting) {
     278            0 :         return resolvedPromise(muse::make_ret(Ret::Code::Busy));
     279              :     }
     280              : 
     281            0 :     m_quiting = true;
     282              : 
     283            0 :     return muse::async::make_promise<Ret>([this, isAllInstances, installerPath](auto resolve) {
     284            0 :         closeProjectScenario()->closeOpenedProject(false)
     285            0 :         .onResolve(this, [this, isAllInstances, installerPath, resolve](const Ret& ret) {
     286            0 :             if (!ret) {
     287            0 :                 LOGD() << "quit cancelled: " << ret.toString();
     288            0 :                 m_quiting = false;
     289            0 :                 (void)resolve(ret);
     290            0 :                 return;
     291              :             }
     292              : 
     293              :             //! NOTE `doQuit` destroys this window, so let's answer before it
     294            0 :             (void)resolve(muse::make_ok());
     295              : 
     296            0 :             doQuit(isAllInstances, installerPath);
     297              :         });
     298              : 
     299            0 :         return muse::async::Promise<Ret>::dummy_result();
     300            0 :     });
     301              : }
     302              : 
     303            0 : muse::async::Promise<muse::Ret> AppshellCommandsController::resolvedPromise(const muse::Ret& ret)
     304              : {
     305            0 :     return muse::async::make_promise<Ret>([ret](auto resolve) {
     306            0 :         return resolve(ret);
     307            0 :     });
     308              : }
     309              : 
     310            0 : void AppshellCommandsController::doQuit(bool isAllInstances, const muse::io::path_t& installerPath)
     311              : {
     312            0 :     if (multiwindowsProvider()->isFirstWindow() && !installerPath.empty()) {
     313              :         //! NOTE: All windows are quitting to complete the update, apply it
     314              :         //! in-place, falling back to handing the package to the user.
     315            0 :         bool applied = false;
     316            0 :         if (appUpdateService()->canAutoInstall()) {
     317            0 :             applied = bool(appUpdateService()->installUpdate());
     318              :         }
     319              : 
     320            0 :         if (!applied) {
     321              : #if defined(Q_OS_LINUX)
     322            0 :             platformInteractive()->revealInFileBrowser(installerPath);
     323              : #else
     324              :             platformInteractive()->openUrl(QUrl::fromLocalFile(installerPath.toQString()));
     325              : #endif
     326              :         }
     327              :     }
     328              : 
     329            0 :     if (!multiwindowsProvider()->isFirstWindow()) {
     330            0 :         multiwindowsProvider()->notifyAboutWindowWasQuited();
     331              :     }
     332              : 
     333              :     //! NOTE the following destroys the IoC context and `this` with it,
     334              :     //! so don't access `this` after this point!
     335            0 :     if (isAllInstances) {
     336            0 :         multiwindowsProvider()->quitForAll(iocContext());
     337              :     } else {
     338            0 :         multiwindowsProvider()->quitWindow(iocContext());
     339              :     }
     340            0 : }
     341              : 
     342            0 : void AppshellCommandsController::restart()
     343              : {
     344            0 :     closeProjectScenario()->closeOpenedProject(false)
     345            0 :     .onResolve(this, [this](const Ret& ret) {
     346            0 :         if (!ret) {
     347            0 :             LOGD() << "restart cancelled: " << ret.toString();
     348            0 :             return;
     349              :         }
     350              : 
     351            0 :         if (multiwindowsProvider()->windowCount() == 1) {
     352            0 :             application()->restart();
     353              :         } else {
     354            0 :             multiwindowsProvider()->quitAllAndRestartLast();
     355              : 
     356            0 :             QCoreApplication::exit();
     357              :         }
     358              :     });
     359            0 : }
     360              : 
     361            0 : void AppshellCommandsController::toggleFullScreen()
     362              : {
     363            0 :     mainWindow()->toggleFullScreen();
     364            0 : }
     365              : 
     366            0 : void AppshellCommandsController::openAboutDialog()
     367              : {
     368            0 :     interactive()->open("musescore://about/musescore");
     369            0 : }
     370              : 
     371            0 : void AppshellCommandsController::openAboutQtDialog()
     372              : {
     373            0 :     QApplication::aboutQt();
     374            0 : }
     375              : 
     376            0 : void AppshellCommandsController::openAboutMusicXMLDialog()
     377              : {
     378            0 :     interactive()->open("musescore://about/musicxml");
     379            0 : }
     380              : 
     381            0 : void AppshellCommandsController::openOnlineHandbookPage()
     382              : {
     383            0 :     std::string handbookUrl = configuration()->handbookUrl();
     384            0 :     platformInteractive()->openUrl(handbookUrl);
     385            0 : }
     386              : 
     387            0 : void AppshellCommandsController::openAskForHelpPage()
     388              : {
     389            0 :     std::string askForHelpUrl = configuration()->askForHelpUrl();
     390            0 :     platformInteractive()->openUrl(askForHelpUrl);
     391            0 : }
     392              : 
     393            0 : void AppshellCommandsController::openAccessibilityStatementPage()
     394              : {
     395            0 :     std::string accessibilityStatementUrl = configuration()->accessibilityStatementUrl();
     396            0 :     platformInteractive()->openUrl(accessibilityStatementUrl);
     397            0 : }
     398              : 
     399            0 : void AppshellCommandsController::openPreferencesDialog()
     400              : {
     401            0 :     if (!playbackState()->isPlaying()) {
     402            0 :         doOpenPreferencesDialog();
     403              :     } else {
     404            0 :         commandDispatcher()->dispatch(STOP_COMMAND)
     405            0 :         .onResolve(this, [this](const muse::rcommand::Response& ) {
     406            0 :             doOpenPreferencesDialog();
     407            0 :         });
     408              :     }
     409            0 : }
     410              : 
     411            0 : void AppshellCommandsController::doOpenPreferencesDialog()
     412              : {
     413            0 :     if (multiwindowsProvider()->isPreferencesAlreadyOpened()) {
     414            0 :         multiwindowsProvider()->activateWindowWithOpenedPreferences();
     415            0 :         return;
     416              :     }
     417              : 
     418            0 :     interactive()->open("muse://preferences");
     419              : }
     420              : 
     421            0 : void AppshellCommandsController::revertToFactorySettings()
     422              : {
     423            0 :     std::string title = muse::trc("appshell", "Are you sure you want to revert to factory settings?");
     424              :     std::string question = muse::trc("appshell", "This action will reset all your app preferences and delete all custom palettes and custom shortcuts. "
     425              :                                                  "The list of recent scores will also be cleared.\n\n"
     426            0 :                                                  "This action will not delete any of your scores.");
     427              : 
     428            0 :     IInteractive::ButtonData cancelBtn = interactive()->buttonData(IInteractive::Button::Cancel);
     429            0 :     cancelBtn.accent = true;
     430              : 
     431            0 :     int revertBtn = int(IInteractive::Button::Apply);
     432            0 :     auto promise = interactive()->warning(title, question,
     433              :                                           { cancelBtn,
     434            0 :                                             IInteractive::ButtonData(revertBtn, muse::trc("appshell", "Revert")) },
     435              :                                           cancelBtn.btn, { muse::IInteractive::Option::WithIcon },
     436            0 :                                           muse::trc("appshell", "Revert to factory settings"));
     437              : 
     438            0 :     promise.onResolve(this, [this](const IInteractive::Result& res) {
     439            0 :         if (res.isButton(IInteractive::Button::Cancel)) {
     440            0 :             return;
     441              :         }
     442              : 
     443              :         static constexpr bool KEEP_DEFAULT_SETTINGS = false;
     444              :         static constexpr bool NOTIFY_ABOUT_CHANGES = false;
     445              :         static constexpr bool NOTIFY_OTHER_INSTANCES = false;
     446            0 :         configuration()->revertToFactorySettings(KEEP_DEFAULT_SETTINGS, NOTIFY_ABOUT_CHANGES, NOTIFY_OTHER_INSTANCES);
     447              : 
     448            0 :         std::string title = muse::trc("appshell", "Would you like to restart MuseScore Studio now?");
     449            0 :         std::string question = muse::trc("appshell", "MuseScore Studio needs to be restarted for these changes to take effect.");
     450              : 
     451            0 :         int restartBtn = int(IInteractive::Button::Apply);
     452            0 :         auto promise = interactive()->question(title, question,
     453            0 :                                                { interactive()->buttonData(IInteractive::Button::Cancel),
     454            0 :                                                  IInteractive::ButtonData(restartBtn, muse::trc("appshell", "Restart"), true) },
     455            0 :                                                restartBtn);
     456              : 
     457            0 :         promise.onResolve(this, [this](const IInteractive::Result& res) {
     458            0 :             if (!res.isButton(IInteractive::Button::Cancel)) {
     459            0 :                 restart();
     460              :             }
     461            0 :         });
     462            0 :     });
     463            0 : }
     464              : 
     465            0 : void AppshellCommandsController::openExtensions()
     466              : {
     467            0 :     interactive()->open("musescore://home?section=plugins");
     468            0 : }
        

Generated by: LCOV version 2.0-1