center all(?) early-startup dialogs
[ardour.git] / gtk2_ardour / startup_fsm.cc
1 /*
2  * Copyright (C) 2019 Paul Davis <paul@linuxaudiosystems.com>
3  *
4  * This program is free software; you can redistribute it and/or modify
5  * it under the terms of the GNU General Public License as published by
6  * the Free Software Foundation; either version 2 of the License, or
7  * (at your option) any later version.
8  *
9  * This program is distributed in the hope that it will be useful,
10  * but WITHOUT ANY WARRANTY; without even the implied warranty of
11  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12  * GNU General Public License for more details.
13  *
14  * You should have received a copy of the GNU General Public License along
15  * with this program; if not, write to the Free Software Foundation, Inc.,
16  * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
17  */
18
19 #include <vector>
20
21 #include <gtkmm/dialog.h>
22 #include <gtkmm/liststore.h>
23 #include <gtkmm/messagedialog.h>
24
25 #include "pbd/basename.h"
26 #include "pbd/file_archive.h"
27 #include "pbd/file_utils.h"
28 #include "pbd/i18n.h"
29
30 #include "ardour/audioengine.h"
31 #include "ardour/filename_extensions.h"
32 #include "ardour/filesystem_paths.h"
33 #include "ardour/profile.h"
34 #include "ardour/recent_sessions.h"
35 #include "ardour/rc_configuration.h"
36 #include "ardour/search_paths.h"
37 #include "ardour/session.h"
38 #include "ardour/session_utils.h"
39 #include "ardour/template_utils.h"
40
41 #include "gtkmm2ext/application.h"
42 #include <gtkmm2ext/doi.h>
43
44 #include "engine_dialog.h"
45 #include "new_user_wizard.h"
46 #include "opts.h"
47 #include "session_dialog.h"
48 #include "startup_fsm.h"
49
50 using namespace ARDOUR;
51 using namespace Gtk;
52 using namespace Gtkmm2ext;
53 using namespace PBD;
54
55 using std::string;
56 using std::vector;
57
58 StartupFSM::StartupFSM (EngineControl& amd)
59         : session_existing_sample_rate (0)
60         , session_is_new (false)
61         , new_user (NewUserWizard::required())
62         , new_session (true)
63         , _state (new_user ? NeedWizard : NeedSessionPath)
64         , new_user_wizard (0)
65         , audiomidi_dialog (amd)
66         , session_dialog (0)
67 {
68         Application* app = Application::instance ();
69
70         app->ShouldQuit.connect (sigc::mem_fun (*this, &StartupFSM::queue_finish));
71         app->ShouldLoad.connect (sigc::mem_fun (*this, &StartupFSM::load_from_application_api));
72
73         /* this may cause the delivery of ShouldLoad etc if we were invoked in
74          * particular ways. It will happen when the event loop runs again.
75          */
76
77         app->ready ();
78 }
79
80 StartupFSM::~StartupFSM ()
81 {
82         delete session_dialog;
83 }
84
85 void
86 StartupFSM::queue_finish ()
87 {
88         _signal_response (ExitProgram);
89 }
90
91 void
92 StartupFSM::start ()
93 {
94         if (new_user) {
95                 /* show new user wizard */
96                 _state = NeedSessionPath;
97                 show_new_user_wizard ();
98         } else {
99                 /* pretend we just showed the new user wizard and we're done
100                    with it
101                 */
102                 dialog_response_handler (RESPONSE_OK, NewUserDialog);
103         }
104 }
105
106
107 void
108 StartupFSM::end()
109 {
110
111 }
112
113 void
114 StartupFSM::dialog_response_handler (int response, StartupFSM::DialogID dialog_id)
115 {
116         const bool new_session_required = (ARDOUR_COMMAND_LINE::new_session || (!ARDOUR::Profile->get_mixbus() && new_user));
117
118         switch (_state) {
119         case NeedSessionPath:
120                 switch (dialog_id) {
121                 case NewUserDialog:
122
123                         current_dialog_connection.disconnect ();
124                         delete_when_idle (new_user_wizard);
125
126                         switch (response) {
127                         case RESPONSE_OK:
128                                 break;
129                         default:
130                                 exit (1);
131                         }
132
133                         /* new user wizard done, now lets get session params
134                          * either from the command line (if given) or a dialog
135                          * (if nothing given on the command line or if the
136                          * command line arguments don't work for some reason
137                          */
138
139                         if (ARDOUR_COMMAND_LINE::session_name.empty()) {
140
141                                 /* nothing given on the command line ... show new session dialog */
142
143                                 session_path = string();
144                                 session_name = string();
145                                 session_template = string();
146
147                                 _state = NeedSessionPath;
148                                 session_dialog = new SessionDialog (new_session_required, string(), string(), string(), false);
149                                 show_session_dialog ();
150
151                         } else {
152
153                                 if (get_session_parameters_from_command_line (new_session_required)) {
154
155                                         /* command line arguments all OK. Get engine parameters */
156
157                                         _state = NeedEngineParams;
158
159                                         if (!new_session_required && session_existing_sample_rate > 0) {
160                                                 audiomidi_dialog.set_desired_sample_rate (session_existing_sample_rate);
161                                         }
162
163                                         show_audiomidi_dialog ();
164
165                                 } else {
166
167                                         /* command line arguments not good. Use
168                                          * dialog, but prime the dialog with
169                                          * the information we set up in
170                                          * get_session_parameters_from_command_line()
171                                          */
172
173                                         _state = NeedSessionPath;
174                                         session_dialog = new SessionDialog (new_session_required, session_name, session_path, session_template, false);
175                                         show_session_dialog ();
176                                 }
177                         }
178                         break;
179
180                 case NewSessionDialog:
181                         switch (response) {
182                         case RESPONSE_OK:
183                         case RESPONSE_ACCEPT:
184                                 switch (check_session_parameters (new_session_required)) {
185                                 case -1:
186                                         /* Unrecoverable error */
187                                         _signal_response (ExitProgram);
188                                         break;
189                                 case 1:
190                                         /* do nothing - keep dialog up for a
191                                          * retry. Errors were addressed by
192                                          * ::check_session_parameters()
193                                          */
194                                         break;
195                                 case 0:
196                                         _state = NeedEngineParams;
197                                         session_dialog->hide ();
198                                         delete session_dialog;
199                                         session_dialog = 0;
200                                         current_dialog_connection.disconnect();
201                                         if (!session_is_new && session_existing_sample_rate > 0) {
202                                                 audiomidi_dialog.set_desired_sample_rate (session_existing_sample_rate);
203                                         }
204                                         show_audiomidi_dialog ();
205                                         break;
206                                 }
207                                 break;
208
209                         default:
210                                 _signal_response (ExitProgram);
211                                 break;
212                         }
213                         break;
214
215                 default:
216                         /* ERROR */
217                         break;
218                 }
219                 break;
220
221         case NeedEngineParams:
222                 switch (dialog_id) {
223                 case AudioMIDISetup:
224                         switch (response) {
225                         case RESPONSE_OK:
226                         case RESPONSE_ACCEPT:
227                                 if (AudioEngine::instance()->running()) {
228                                         audiomidi_dialog.hide ();
229                                         current_dialog_connection.disconnect();
230                                         _signal_response (LoadSession);
231                                 } else {
232                                         /* just keep going */
233                                 }
234                                 break;
235                         default:
236                                 _signal_response (ExitProgram);
237                         }
238                         break;
239                 default:
240                         /* ERROR */
241                         break;
242                 }
243
244         case NeedWizard:
245                 /* ERROR */
246                 break;
247         }
248 }
249
250 void
251 StartupFSM::show_new_user_wizard ()
252 {
253         new_user_wizard = new NewUserWizard;
254         current_dialog_connection = new_user_wizard->signal_response().connect (sigc::bind (sigc::mem_fun (*this, &StartupFSM::dialog_response_handler), NewUserDialog));
255         new_user_wizard->set_position (WIN_POS_CENTER);
256         new_user_wizard->present ();
257 }
258
259 void
260 StartupFSM::show_session_dialog ()
261 {
262         current_dialog_connection = session_dialog->signal_response().connect (sigc::bind (sigc::mem_fun (*this, &StartupFSM::dialog_response_handler), NewSessionDialog));
263         session_dialog->set_position (WIN_POS_CENTER);
264         session_dialog->present ();
265 }
266
267 void
268 StartupFSM::show_audiomidi_dialog ()
269 {
270         current_dialog_connection = audiomidi_dialog.signal_response().connect (sigc::bind (sigc::mem_fun (*this, &StartupFSM::dialog_response_handler), AudioMIDISetup));
271         audiomidi_dialog.set_position (WIN_POS_CENTER);
272         audiomidi_dialog.present ();
273 }
274
275 bool
276 StartupFSM::get_session_parameters_from_command_line (bool new_session_required)
277 {
278         return get_session_parameters_from_path (ARDOUR_COMMAND_LINE::session_name, ARDOUR_COMMAND_LINE::load_template, new_session_required);
279 }
280
281 bool
282 StartupFSM::get_session_parameters_from_path (string const & path, string const & template_name, bool new_session_required)
283 {
284         if (path.empty()) {
285                 /* use GUI to ask the user */
286                 return false;
287         }
288
289         if (Glib::file_test (path.c_str(), Glib::FILE_TEST_EXISTS)) {
290
291                 session_is_new = false;
292
293                 if (new_session_required) {
294                         /* wait! it already exists */
295
296                         if (!ask_about_loading_existing_session (path)) {
297                                 return false;
298                         } else {
299                                 /* load it anyway */
300                         }
301                 }
302
303                 session_name = basename_nosuffix (path);
304
305                 if (Glib::file_test (path.c_str(), Glib::FILE_TEST_IS_REGULAR)) {
306                         /* session/snapshot file, change path to be dir */
307                         session_path = Glib::path_get_dirname (path);
308                 } else {
309                         session_path = path;
310                 }
311
312                 float sr;
313                 SampleFormat fmt;
314                 string program_version;
315
316                 const string statefile_path = Glib::build_filename (session_path, session_name + ARDOUR::statefile_suffix);
317                 if (Session::get_info_from_path (statefile_path, sr, fmt, program_version)) {
318                         /* exists but we can't read it correctly */
319                         error << string_compose (_("Cannot get existing session information from %1"), statefile_path) << endmsg;
320                         return false;
321                 }
322
323                 session_existing_sample_rate = sr;
324                 return true;
325
326         }
327
328         /*  Everything after this involves a new session
329          *
330          *  ... did the  user give us a path or just a name?
331          */
332
333         if (path.find (G_DIR_SEPARATOR) == string::npos) {
334                 /* user gave session name with no path info, use
335                    default session folder.
336                 */
337                 session_name = ARDOUR_COMMAND_LINE::session_name;
338                 session_path = Glib::build_filename (Config->get_default_session_parent_dir (), session_name);
339         } else {
340                 session_name = basename_nosuffix (path);
341                 session_path = path;
342         }
343
344
345         if (!template_name.empty()) {
346
347                 /* Allow the user to specify a template via path or name on the
348                  * command line
349                  */
350
351                 bool have_resolved_template_name = false;
352
353                 /* compare by name (path may or may not be UTF-8) */
354
355                 vector<TemplateInfo> templates;
356                 find_session_templates (templates, false);
357                 for (vector<TemplateInfo>::iterator x = templates.begin(); x != templates.end(); ++x) {
358                         if ((*x).name == template_name) {
359                                 session_template = (*x).path;
360                                 have_resolved_template_name = true;
361                                 break;
362                         }
363                 }
364
365                 /* look up script by name */
366                 LuaScriptList scripts (LuaScripting::instance ().scripts (LuaScriptInfo::SessionInit));
367                 LuaScriptList& as (LuaScripting::instance ().scripts (LuaScriptInfo::EditorAction));
368                 for (LuaScriptList::const_iterator s = as.begin(); s != as.end(); ++s) {
369                         if ((*s)->subtype & LuaScriptInfo::SessionSetup) {
370                                 scripts.push_back (*s);
371                         }
372                 }
373                 std::sort (scripts.begin(), scripts.end(), LuaScripting::Sorter());
374                 for (LuaScriptList::const_iterator s = scripts.begin(); s != scripts.end(); ++s) {
375                         if ((*s)->name == template_name) {
376                                 session_template = "urn:ardour:" + (*s)->path;
377                                 have_resolved_template_name = true;
378                                 break;
379                         }
380                 }
381
382                 if (!have_resolved_template_name) {
383                         /* this will produce a more or less meaninful error later:
384                          * "ERROR: Could not open session template [abs-path to user-config dir]"
385                          */
386                         session_template = Glib::build_filename (ARDOUR::user_template_directory (), template_name);
387                 }
388         }
389
390         /* We don't know what this is, because the session is new and the
391          * command line doesn't let us specify it. The user will get to decide
392          * in the audio/MIDI dialog.
393          */
394
395         session_existing_sample_rate = 0;
396         session_is_new = true;
397
398         /* this is an arbitrary default value but since the user insists on
399          * starting a new session from the command line, it will do as well as
400          * any other possible value. I mean, seriously, what else could it be
401          * by default?
402          */
403
404         bus_profile.master_out_channels = 2;
405
406         return true;
407 }
408
409 /** return values:
410  * -1: failure
411  *  1: failure but user can retry
412  *  0: success, seesion parameters ready for use
413  */
414 int
415 StartupFSM::check_session_parameters (bool must_be_new)
416 {
417         bool requested_new = false;
418
419         session_name = session_dialog->session_name (requested_new);
420         session_path = session_dialog->session_folder ();
421
422         if (must_be_new) {
423                 assert (requested_new);
424         }
425
426         if (!must_be_new) {
427
428                 /* See if the specified session is a session archive */
429
430                 int rv = ARDOUR::inflate_session (session_name, Config->get_default_session_parent_dir(), session_path, session_name);
431                 if (rv < 0) {
432                         MessageDialog msg (*session_dialog, string_compose (_("Extracting session-archive failed: %1"), inflate_error (rv)));
433                         msg.run ();
434
435                         return 1;
436                 } else if (rv == 0) {
437                         /* names are good (and session is unarchived/inflated */
438                         return 0;
439                 }
440         }
441
442         /* check for ".ardour" in statefile name, because we don't want
443          * it
444          *
445          * XXX Note this wierd conflation of a
446          * file-name-without-a-suffix and the session name. It's not
447          * really a session name at all, but rather the suffix-free
448          * name of a statefile (snapshot).
449          */
450
451         const string::size_type suffix_at = session_name.find (statefile_suffix);
452
453         if (suffix_at != string::npos) {
454                 session_name = session_name.substr (0, suffix_at);
455         }
456
457         /* this shouldn't happen, but we catch it just in case it does */
458
459         if (session_name.empty()) {
460                 return 1; /* keep running dialog */
461         }
462
463         if (session_dialog->use_session_template()) {
464                 session_template = session_dialog->session_template_name();
465         }
466
467         if (session_name[0] == G_DIR_SEPARATOR ||
468 #ifdef PLATFORM_WINDOWS
469             // Windows file system .. detect absolute path
470             // C:/*
471             (session_name.length() > 3 && session_name[1] == ':' && session_name[2] == G_DIR_SEPARATOR)
472 #else
473             // Sensible file systems
474             // /* or ./* or ../*
475             (session_name.length() > 2 && session_name[0] == '.' && session_name[1] == G_DIR_SEPARATOR) ||
476             (session_name.length() > 3 && session_name[0] == '.' && session_name[1] == '.' && session_name[2] == G_DIR_SEPARATOR)
477 #endif
478                 )
479         {
480
481                 /* user typed absolute path or cwd-relative path
482                    specified into session name field. So ... infer
483                    session path and name from what was given.
484                 */
485
486                 session_path = Glib::path_get_dirname (session_name);
487                 session_name = Glib::path_get_basename (session_name);
488
489         } else {
490
491                 /* session name is just a name */
492         }
493
494         /* check if name is legal */
495
496         const char illegal = Session::session_name_is_legal (session_name);
497
498         if (illegal) {
499                 MessageDialog msg (*session_dialog,
500                                    string_compose (_("To ensure compatibility with various systems\n"
501                                                      "session names may not contain a '%1' character"),
502                                                    illegal));
503                 msg.run ();
504                 ARDOUR_COMMAND_LINE::session_name = ""; // cancel that
505                 return 1; /* keep running dialog */
506         }
507
508
509         /* check if the currently-exists status matches whether or not
510          * it should be new
511          */
512
513         if (Glib::file_test (session_path, Glib::FileTest (G_FILE_TEST_EXISTS | G_FILE_TEST_IS_DIR))) {
514
515                 if (requested_new /*&& !nsm*/) {
516
517                         std::string existing = Glib::build_filename (session_path, session_name);
518
519                         if (!ask_about_loading_existing_session (existing)) {
520                                 session_dialog->clear_name ();
521                                 return 1; /* try again */
522                         }
523                 }
524
525                 session_is_new = false;
526
527         } else {
528
529                 /* does not exist at present */
530
531                 if (!requested_new) {
532                         // pop_back_splash (session_dialog);
533                         MessageDialog msg (string_compose (_("There is no existing session at \"%1\""), session_path));
534                         msg.run ();
535                         session_dialog->clear_name();
536                         return 1;
537                 }
538
539                 session_is_new = true;
540         }
541
542         float sr;
543         SampleFormat fmt;
544         string program_version;
545         const string statefile_path = Glib::build_filename (session_path, session_name + ARDOUR::statefile_suffix);
546
547         if (!session_is_new) {
548
549                 if (Session::get_info_from_path (statefile_path, sr, fmt, program_version)) {
550                         /* exists but we can't read it */
551                         return -1;
552                 }
553
554                 session_existing_sample_rate = sr;
555
556         } else {
557
558                 bus_profile.master_out_channels = session_dialog->master_channel_count ();
559         }
560
561         return 0;
562 }
563
564 void
565 StartupFSM::copy_demo_sessions ()
566 {
567         // TODO: maybe IFF brand_new_user
568         if (ARDOUR::Profile->get_mixbus () && Config->get_copy_demo_sessions ()) {
569                 std::string dspd (Config->get_default_session_parent_dir());
570                 Searchpath ds (ARDOUR::ardour_data_search_path());
571                 ds.add_subdirectory_to_paths ("sessions");
572                 vector<string> demos;
573                 find_files_matching_pattern (demos, ds, ARDOUR::session_archive_suffix);
574
575                 ARDOUR::RecentSessions rs;
576                 ARDOUR::read_recent_sessions (rs);
577
578                 for (vector<string>::iterator i = demos.begin(); i != demos.end (); ++i) {
579                         /* "demo-session" must be inside "demo-session.<session_archive_suffix>" */
580                         std::string name = basename_nosuffix (basename_nosuffix (*i));
581                         std::string path = Glib::build_filename (dspd, name);
582                         /* skip if session-dir already exists */
583                         if (Glib::file_test(path.c_str(), Glib::FILE_TEST_IS_DIR)) {
584                                 continue;
585                         }
586                         /* skip sessions that are already in 'recent'.
587                          * eg. a new user changed <session-default-dir> shorly after installation
588                          */
589                         for (ARDOUR::RecentSessions::iterator r = rs.begin(); r != rs.end(); ++r) {
590                                 if ((*r).first == name) {
591                                         continue;
592                                 }
593                         }
594                         try {
595                                 PBD::FileArchive ar (*i);
596                                 if (0 == ar.inflate (dspd)) {
597                                         store_recent_sessions (name, path);
598                                         info << string_compose (_("Copied Demo Session %1."), name) << endmsg;
599                                 }
600                         } catch (...) {
601                                 /* relax ? */
602                         }
603                 }
604         }
605 }
606
607 void
608 StartupFSM::load_from_application_api (const std::string& path)
609 {
610         /* macOS El Capitan (and probably later) now somehow passes the command
611            line arguments to an app via the openFile delegate protocol. Ardour
612            already does its own command line processing, and having both
613            pathways active causes crashes. So, if the command line was already
614            set, do nothing here.
615         */
616
617         if (!ARDOUR_COMMAND_LINE::session_name.empty()) {
618                 return;
619         }
620
621         /* Cancel SessionDialog if it's visible to make macOS delegates work.
622          *
623          * there's a race condition here: we connect to app->ShouldLoad
624          * and then at some point (might) show a session dialog. The race is
625          * caused by the non-deterministic interaction between the macOS event
626          * loop(s) and the GDK one(s).
627          *
628          *  - ShouldLoad does not arrive before we show the session dialog
629          *          -> here we should hide the session dialog, then use the
630          *             supplied path as if it was provided on the command line
631          *  - ShouldLoad signal arrives before we show a session dialog
632          *          -> don't bother showing the session dialog, just use the
633          *             supplied path as if it was provided on the command line
634          *
635          */
636
637         if (session_dialog) {
638                 session_dialog->hide ();
639                 delete_when_idle (session_dialog);
640                 session_dialog = 0;
641         }
642
643         /* no command line argument given ... must just be via
644          * desktop/finder/window manager API (e.g. double click on "foo.ardour"
645          * icon)
646          */
647
648         if (get_session_parameters_from_path (path, string(), false)) {
649                 _signal_response (LoadSession);
650                 return;
651         }
652
653         /* given parameters failed for some reason. This is probably true
654          * anyway, but force it to be true and then carry on with whatever the
655          * main event loop is doing.
656          */
657
658         _state = NeedSessionPath;
659 }
660
661 bool
662 StartupFSM::ask_about_loading_existing_session (const std::string& session_path)
663 {
664         std::string str = string_compose (_("This session\n%1\nalready exists. Do you want to open it?"), session_path);
665
666         MessageDialog msg (str,
667                            false,
668                            Gtk::MESSAGE_WARNING,
669                            Gtk::BUTTONS_YES_NO,
670                            true);
671
672
673         msg.set_name (X_("OpenExistingDialog"));
674         msg.set_title (_("Open Existing Session"));
675         msg.set_wmclass (X_("existing_session"), PROGRAM_NAME);
676         msg.set_position (Gtk::WIN_POS_CENTER);
677         // pop_back_splash (msg);
678
679         switch (msg.run()) {
680         case RESPONSE_YES:
681                 return true;
682                 break;
683         }
684         return false;
685 }