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 : }
|