Line data Source code
1 : /*
2 : * SPDX-License-Identifier: GPL-3.0-only
3 : * MuseScore-Studio-CLA-applies
4 : *
5 : * MuseScore Studio
6 : * Music Composition & Notation
7 : *
8 : * Copyright (C) 2021 MuseScore Limited and others
9 : *
10 : * This program is free software: you can redistribute it and/or modify
11 : * it under the terms of the GNU General Public License version 3 as
12 : * published by the Free Software Foundation.
13 : *
14 : * This program is distributed in the hope that it will be useful,
15 : * but WITHOUT ANY WARRANTY; without even the implied warranty of
16 : * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 : * GNU General Public License for more details.
18 : *
19 : * You should have received a copy of the GNU General Public License
20 : * along with this program. If not, see <https://www.gnu.org/licenses/>.
21 : */
22 :
23 : #include "projectactionscontroller.h"
24 :
25 : #include <QBuffer>
26 : #include <QFileInfo>
27 : #include <QTemporaryFile>
28 : #include <QUrl>
29 : #include <QUrlQuery>
30 : #include <QTimer>
31 : #include <qobject.h>
32 :
33 : #include "async/async.h"
34 : #include "defer.h"
35 : #include "types/ret.h"
36 : #include "rcommand/commandtypes.h"
37 :
38 : #include "notation/imasternotation.h"
39 : #include "notation/inotationinteraction.h"
40 : #include "notation/inotationselection.h"
41 :
42 : #include "inotationproject.h"
43 : #include "types/projecturis.h"
44 :
45 : #include "../projectcommands.h"
46 : #include "appshell/appshellcommands.h"
47 :
48 : #include "log.h"
49 :
50 : using namespace mu;
51 : using namespace mu::project;
52 : using namespace mu::notation;
53 : using namespace muse;
54 :
55 0 : void ProjectActionsController::init()
56 : {
57 0 : auto d = commandDispatcher();
58 :
59 0 : d->onRequest(this, PROJECT_NEW_COMMAND, [this]() { return newProject(); });
60 0 : d->onRequest(this, PROJECT_OPEN_COMMAND, [this](const rcommand::Params& params) { return openProject(params); });
61 0 : d->onRequest(this, PROJECT_CLOSE_COMMAND, [this]() { return closeProject(); });
62 :
63 0 : d->onRequest(this, PROJECT_SAVE_COMMAND, [this]() { return runAsync(saveProject(SaveMode::Save)); });
64 0 : d->onRequest(this, PROJECT_SAVE_AS_COMMAND, [this]() { return runAsync(saveProject(SaveMode::SaveAs)); });
65 0 : d->onRequest(this, PROJECT_SAVE_A_COPY_COMMAND, [this]() { return runAsync(saveProject(SaveMode::SaveCopy)); });
66 0 : d->onRequest(this, PROJECT_SAVE_SELECTION_COMMAND, [this]() {
67 0 : return runAsync(saveProject(SaveMode::SaveSelection, SaveLocationType::Local));
68 : });
69 0 : d->onRequest(this, PROJECT_SAVE_TO_CLOUD_COMMAND, [this]() { return runAsync(saveProject(SaveMode::Save, SaveLocationType::Cloud)); });
70 0 : d->onRequest(this, PROJECT_SAVE_AT_COMMAND, [this](const rcommand::Params& params) { return runAsync(saveProjectAt(params)); });
71 :
72 0 : d->onRequest(this, PROJECT_PUBLISH_COMMAND, [this]() { return runAsync(publish()); });
73 0 : d->onRequest(this, PROJECT_SHARE_AUDIO_COMMAND, [this]() { return runAsync(sharedAudio()); });
74 :
75 0 : d->onRequest(this, PROJECT_EXPORT_COMMAND, [this]() { return exportScore(); });
76 0 : d->onRequest(this, PROJECT_CONVERT_TO_SCORE_COMMAND, [this]() { return convertFileToScore(); });
77 :
78 0 : d->onRequest(this, PROJECT_PRINT_COMMAND, [this]() { return printScore(); });
79 0 : d->onRequest(this, PROJECT_CLEAR_RECENT_COMMAND, [this]() { return clearRecentScores(); });
80 0 : d->onRequest(this, PROJECT_CONTINUE_LAST_SESSION_COMMAND, [this]() { return continueLastSession(); });
81 0 : d->onRequest(this, PROJECT_PROPERTIES_COMMAND, [this]() { return openProjectProperties(); });
82 :
83 0 : saveProjectScenario()->busyChanged().onNotify(this, [this]() {
84 0 : m_busyChanged.notify();
85 0 : });
86 :
87 0 : openProjectScenario()->busyChanged().onNotify(this, [this]() {
88 0 : m_busyChanged.notify();
89 0 : });
90 :
91 0 : closeProjectScenario()->busyChanged().onNotify(this, [this]() {
92 0 : m_busyChanged.notify();
93 0 : });
94 :
95 : // listen changes
96 0 : projectSession()->projectChanged().onNotify(this, [this]() {
97 0 : auto project = projectSession()->project();
98 0 : if (project) {
99 0 : project->needSaveChanged().onNotify(this, [this]() {
100 0 : m_needSaveChanged.notify();
101 0 : });
102 : }
103 :
104 0 : auto notation = projectSession()->currentNotation();
105 0 : if (notation) {
106 0 : notation->interaction()->selectionChanged().onNotify(this, [this]() {
107 0 : m_hasSelectionChanged.notify();
108 0 : });
109 : }
110 0 : });
111 0 : }
112 :
113 0 : bool ProjectActionsController::hasProject() const
114 : {
115 0 : return currentNotationProject() != nullptr;
116 : }
117 :
118 0 : muse::async::Notification ProjectActionsController::hasProjectChanged() const
119 : {
120 0 : return projectSession()->projectChanged();
121 : }
122 :
123 0 : bool ProjectActionsController::needSave() const
124 : {
125 0 : return currentNotationProject() ? currentNotationProject()->isNeedSave() : false;
126 : }
127 :
128 0 : muse::async::Notification ProjectActionsController::needSaveChanged() const
129 : {
130 0 : return m_needSaveChanged;
131 : }
132 :
133 0 : bool ProjectActionsController::isBusy(BusyStatus status) const
134 : {
135 0 : return m_busyStatuses.contains(status)
136 0 : || saveProjectScenario()->isBusy(status)
137 0 : || openProjectScenario()->isBusy(status)
138 0 : || closeProjectScenario()->isBusy(status);
139 : }
140 :
141 0 : void ProjectActionsController::setBusy(BusyStatus status, bool isBusy)
142 : {
143 0 : bool wasBusy = m_busyStatuses.contains(status);
144 0 : if (wasBusy == isBusy) {
145 0 : return;
146 : }
147 :
148 0 : if (isBusy) {
149 0 : m_busyStatuses.insert(status);
150 : } else {
151 0 : m_busyStatuses.erase(status);
152 : }
153 :
154 0 : m_busyChanged.notify();
155 : }
156 :
157 0 : muse::async::Notification ProjectActionsController::busyChanged() const
158 : {
159 0 : return m_busyChanged;
160 : }
161 :
162 0 : bool ProjectActionsController::hasSelection() const
163 : {
164 0 : return currentNotationSelection() ? !currentNotationSelection()->isNone() : false;
165 : }
166 :
167 0 : muse::async::Notification ProjectActionsController::hasSelectionChanged() const
168 : {
169 0 : return m_hasSelectionChanged;
170 : }
171 :
172 0 : INotationProjectPtr ProjectActionsController::currentNotationProject() const
173 : {
174 0 : return projectSession()->project();
175 : }
176 :
177 0 : IMasterNotationPtr ProjectActionsController::currentMasterNotation() const
178 : {
179 0 : return currentNotationProject() ? currentNotationProject()->masterNotation() : nullptr;
180 : }
181 :
182 0 : INotationPtr ProjectActionsController::currentNotation() const
183 : {
184 0 : return currentMasterNotation() ? currentMasterNotation()->notation() : nullptr;
185 : }
186 :
187 0 : INotationInteractionPtr ProjectActionsController::currentInteraction() const
188 : {
189 0 : return currentNotation() ? currentNotation()->interaction() : nullptr;
190 : }
191 :
192 0 : INotationSelectionPtr ProjectActionsController::currentNotationSelection() const
193 : {
194 0 : return currentNotation() ? currentInteraction()->selection() : nullptr;
195 : }
196 :
197 0 : muse::Ret ProjectActionsController::openPageIfNeed(muse::Uri pageUri)
198 : {
199 0 : if (!interactive()->isOpened(pageUri).val) {
200 0 : interactive()->open(pageUri);
201 : }
202 0 : return make_ret(Ret::Code::Ok);
203 : }
204 :
205 0 : muse::Ret ProjectActionsController::openProject(const muse::io::path_t& path, const QString& displayNameOverride)
206 : {
207 0 : return runAsync(openProjectScenario()->openProject(path, displayNameOverride));
208 : }
209 :
210 0 : muse::Ret ProjectActionsController::openProject(const muse::rcommand::Params& params)
211 : {
212 0 : return runAsync(openProjectScenario()->openProject(params));
213 : }
214 :
215 0 : bool ProjectActionsController::isProjectOpened(const muse::io::path_t& scorePath) const
216 : {
217 0 : auto project = projectSession()->project();
218 0 : if (!project) {
219 0 : return false;
220 : }
221 :
222 0 : LOGD() << "project->path: " << project->path() << ", check path: " << scorePath;
223 0 : if (project->path() == scorePath) {
224 0 : return true;
225 : }
226 :
227 0 : return false;
228 0 : }
229 :
230 0 : bool ProjectActionsController::isAnyProjectOpened() const
231 : {
232 0 : auto project = projectSession()->project();
233 0 : if (project) {
234 0 : return true;
235 : }
236 0 : return false;
237 0 : }
238 :
239 0 : muse::Ret ProjectActionsController::newProject()
240 : {
241 : //! NOTE This method is synchronous,
242 : //! but inside `multiwindowsProvider` there can be an event loop
243 : //! to wait for the responses from other instances, accordingly,
244 : //! the events (like user click) can be executed and this method can be called several times,
245 : //! before the end of the current call.
246 : //! So we ignore all subsequent calls until the current one completes.
247 0 : if (isBusy(BusyStatus::Opening)) {
248 0 : return make_ret(Ret::Code::Busy);
249 : }
250 0 : setBusy(BusyStatus::Opening, true);
251 :
252 0 : DEFER {
253 0 : setBusy(BusyStatus::Opening, false);
254 0 : };
255 :
256 0 : if (projectSession()->project()) {
257 0 : if (multiwindowsProvider()->isHasWindowWithoutProject()) {
258 0 : multiwindowsProvider()->activateWindowWithoutProject({ QString::fromStdString(PROJECT_NEW_COMMAND.toString()) });
259 0 : return make_ok();
260 : }
261 0 : QStringList args;
262 0 : args << "--session-type" << "start-with-new";
263 0 : multiwindowsProvider()->openNewWindow(args);
264 0 : return make_ok();
265 0 : }
266 :
267 0 : auto promise = interactive()->open(NEW_SCORE_URI);
268 0 : promise.onResolve(this, [this](const Val&) {
269 0 : Ret ret = openProjectScenario()->finishOpening();
270 :
271 0 : if (!ret) {
272 0 : LOGE() << ret.toString();
273 : }
274 0 : });
275 :
276 0 : return make_ok();
277 0 : }
278 :
279 0 : muse::Ret ProjectActionsController::closeProject()
280 : {
281 0 : bool anyInstanceWithoutProject = multiwindowsProvider()->isHasWindowWithoutProject();
282 :
283 0 : return runAsync(closeProjectScenario()->closeOpenedProject(true)
284 0 : .then<Ret>(this, [this, anyInstanceWithoutProject](const Ret& ret, auto resolve) {
285 0 : if (ret && anyInstanceWithoutProject) {
286 : //! NOTE: we need to call `quit` in the next event loop due to controlling the lifecycle of this method
287 0 : async::Async::call(this, [this]() {
288 0 : commandDispatcher()->dispatch(appshell::APP_QUIT_COMMAND, { { "all_instances", muse::Val(false) } });
289 : });
290 0 : multiwindowsProvider()->activateWindowWithoutProject();
291 : }
292 :
293 0 : return resolve(ret);
294 0 : }));
295 : }
296 :
297 : //! Commands report that the flow has started; its outcome is shown to the user by the scenario itself
298 0 : muse::Ret ProjectActionsController::runAsync(async::Promise<Ret> flow)
299 : {
300 0 : flow.onResolve(this, [](const Ret& ret) {
301 0 : if (!ret) {
302 0 : LOGD() << ret.toString();
303 : }
304 0 : });
305 :
306 0 : return make_ok();
307 : }
308 :
309 0 : async::Promise<Ret> ProjectActionsController::saveProject(SaveMode saveMode, SaveLocationType saveLocationType, bool force)
310 : {
311 0 : return saveProjectScenario()->saveProject(saveMode, saveLocationType, force);
312 : }
313 :
314 0 : async::Promise<Ret> ProjectActionsController::publish()
315 : {
316 0 : return saveProjectScenario()->publish();
317 : }
318 :
319 0 : async::Promise<Ret> ProjectActionsController::sharedAudio()
320 : {
321 0 : return saveProjectScenario()->shareAudio();
322 : }
323 :
324 0 : async::Promise<Ret> ProjectActionsController::saveProjectAt(const muse::rcommand::Params& params)
325 : {
326 0 : return saveProjectScenario()->saveProjectAt(params);
327 : }
328 :
329 0 : muse::Ret ProjectActionsController::convertFileToScore()
330 : {
331 0 : convertFileToScoreScenario()->convertFiles();
332 0 : return make_ok();
333 : }
334 :
335 0 : muse::Ret ProjectActionsController::clearRecentScores()
336 : {
337 0 : recentFilesController()->clearRecentFiles();
338 0 : return make_ok();
339 : }
340 :
341 0 : muse::Ret ProjectActionsController::continueLastSession()
342 : {
343 0 : const RecentFilesList& recentScorePaths = recentFilesController()->recentFilesList();
344 :
345 0 : if (recentScorePaths.empty()) {
346 0 : Ret ret = openPageIfNeed(HOME_PAGE_URI);
347 0 : if (!ret) {
348 0 : LOGE() << ret.toString();
349 : }
350 0 : return ret;
351 0 : }
352 :
353 0 : muse::io::path_t lastScorePath = recentScorePaths.front().path;
354 0 : return openProject(lastScorePath);
355 0 : }
356 :
357 0 : muse::Ret ProjectActionsController::exportScore()
358 : {
359 0 : static const Uri EXPORT_URI("musescore://project/export");
360 0 : if (!interactive()->isOpened(EXPORT_URI).val) {
361 0 : interactive()->open(EXPORT_URI);
362 : }
363 0 : return make_ok();
364 : }
365 :
366 0 : muse::Ret ProjectActionsController::printScore()
367 : {
368 0 : INotationPtr notation = projectSession()->currentNotation();
369 0 : if (!notation) {
370 0 : return make_ret(Ret::Code::InternalError);
371 : }
372 :
373 0 : printProvider()->printNotation(notation);
374 0 : return make_ok();
375 0 : }
376 :
377 0 : muse::Ret ProjectActionsController::openProjectProperties()
378 : {
379 0 : interactive()->open(PROJECT_PROPERTIES_URI);
380 0 : return make_ok();
381 : }
|