2 * Copyright (C) 2019 Paul Davis <paul@linuxaudiosystems.com>
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.
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.
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.
21 #include <gtkmm/dialog.h>
22 #include <gtkmm/liststore.h>
23 #include <gtkmm/messagedialog.h>
25 #include "pbd/basename.h"
26 #include "pbd/file_archive.h"
27 #include "pbd/file_utils.h"
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"
41 #include "gtkmm2ext/application.h"
42 #include <gtkmm2ext/doi.h>
44 #include "engine_dialog.h"
45 #include "new_user_wizard.h"
47 #include "session_dialog.h"
48 #include "startup_fsm.h"
50 using namespace ARDOUR;
52 using namespace Gtkmm2ext;
58 StartupFSM::StartupFSM (EngineControl& amd)
59 : session_existing_sample_rate (0)
60 , session_is_new (false)
61 , new_user (NewUserWizard::required())
63 , _state (new_user ? NeedWizard : NeedSessionPath)
65 , audiomidi_dialog (amd)
68 Application* app = Application::instance ();
70 app->ShouldQuit.connect (sigc::mem_fun (*this, &StartupFSM::queue_finish));
71 app->ShouldLoad.connect (sigc::mem_fun (*this, &StartupFSM::load_from_application_api));
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.
80 StartupFSM::~StartupFSM ()
82 delete session_dialog;
86 StartupFSM::queue_finish ()
88 _signal_response (ExitProgram);
95 /* show new user wizard */
96 _state = NeedSessionPath;
97 show_new_user_wizard ();
99 /* pretend we just showed the new user wizard and we're done
102 dialog_response_handler (RESPONSE_OK, NewUserDialog);
114 StartupFSM::dialog_response_handler (int response, StartupFSM::DialogID dialog_id)
116 const bool new_session_required = (ARDOUR_COMMAND_LINE::new_session || (!ARDOUR::Profile->get_mixbus() && new_user));
119 case NeedSessionPath:
123 current_dialog_connection.disconnect ();
124 delete_when_idle (new_user_wizard);
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
139 if (ARDOUR_COMMAND_LINE::session_name.empty()) {
141 /* nothing given on the command line ... show new session dialog */
143 session_path = string();
144 session_name = string();
145 session_template = string();
147 _state = NeedSessionPath;
148 session_dialog = new SessionDialog (new_session_required, string(), string(), string(), false);
149 show_session_dialog ();
153 if (get_session_parameters_from_command_line (new_session_required)) {
155 /* command line arguments all OK. Get engine parameters */
157 _state = NeedEngineParams;
159 if (!new_session_required && session_existing_sample_rate > 0) {
160 audiomidi_dialog.set_desired_sample_rate (session_existing_sample_rate);
163 show_audiomidi_dialog ();
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()
173 _state = NeedSessionPath;
174 session_dialog = new SessionDialog (new_session_required, session_name, session_path, session_template, false);
175 show_session_dialog ();
180 case NewSessionDialog:
183 case RESPONSE_ACCEPT:
184 switch (check_session_parameters (new_session_required)) {
186 /* Unrecoverable error */
187 _signal_response (ExitProgram);
190 /* do nothing - keep dialog up for a
191 * retry. Errors were addressed by
192 * ::check_session_parameters()
196 _state = NeedEngineParams;
197 session_dialog->hide ();
198 delete session_dialog;
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);
204 show_audiomidi_dialog ();
210 _signal_response (ExitProgram);
221 case NeedEngineParams:
226 case RESPONSE_ACCEPT:
227 if (AudioEngine::instance()->running()) {
228 audiomidi_dialog.hide ();
229 current_dialog_connection.disconnect();
230 _signal_response (LoadSession);
232 /* just keep going */
236 _signal_response (ExitProgram);
251 StartupFSM::show_new_user_wizard ()
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->present ();
259 StartupFSM::show_session_dialog ()
261 current_dialog_connection = session_dialog->signal_response().connect (sigc::bind (sigc::mem_fun (*this, &StartupFSM::dialog_response_handler), NewSessionDialog));
262 session_dialog->present ();
266 StartupFSM::show_audiomidi_dialog ()
268 current_dialog_connection = audiomidi_dialog.signal_response().connect (sigc::bind (sigc::mem_fun (*this, &StartupFSM::dialog_response_handler), AudioMIDISetup));
269 audiomidi_dialog.present ();
273 StartupFSM::get_session_parameters_from_command_line (bool new_session_required)
275 return get_session_parameters_from_path (ARDOUR_COMMAND_LINE::session_name, ARDOUR_COMMAND_LINE::load_template, new_session_required);
279 StartupFSM::get_session_parameters_from_path (string const & path, string const & template_name, bool new_session_required)
282 /* use GUI to ask the user */
286 if (Glib::file_test (path.c_str(), Glib::FILE_TEST_EXISTS)) {
288 session_is_new = false;
290 if (new_session_required) {
291 /* wait! it already exists */
293 if (!ask_about_loading_existing_session (path)) {
300 session_name = basename_nosuffix (path);
302 if (Glib::file_test (path.c_str(), Glib::FILE_TEST_IS_REGULAR)) {
303 /* session/snapshot file, change path to be dir */
304 session_path = Glib::path_get_dirname (path);
311 string program_version;
313 const string statefile_path = Glib::build_filename (session_path, session_name + ARDOUR::statefile_suffix);
314 if (Session::get_info_from_path (statefile_path, sr, fmt, program_version)) {
315 /* exists but we can't read it correctly */
316 error << string_compose (_("Cannot get existing session information from %1"), statefile_path) << endmsg;
320 session_existing_sample_rate = sr;
325 /* Everything after this involves a new session
327 * ... did the user give us a path or just a name?
330 if (path.find (G_DIR_SEPARATOR) == string::npos) {
331 /* user gave session name with no path info, use
332 default session folder.
334 session_name = ARDOUR_COMMAND_LINE::session_name;
335 session_path = Glib::build_filename (Config->get_default_session_parent_dir (), session_name);
337 session_name = basename_nosuffix (path);
342 if (!template_name.empty()) {
344 /* Allow the user to specify a template via path or name on the
348 bool have_resolved_template_name = false;
350 /* compare by name (path may or may not be UTF-8) */
352 vector<TemplateInfo> templates;
353 find_session_templates (templates, false);
354 for (vector<TemplateInfo>::iterator x = templates.begin(); x != templates.end(); ++x) {
355 if ((*x).name == template_name) {
356 session_template = (*x).path;
357 have_resolved_template_name = true;
362 /* look up script by name */
363 LuaScriptList scripts (LuaScripting::instance ().scripts (LuaScriptInfo::SessionInit));
364 LuaScriptList& as (LuaScripting::instance ().scripts (LuaScriptInfo::EditorAction));
365 for (LuaScriptList::const_iterator s = as.begin(); s != as.end(); ++s) {
366 if ((*s)->subtype & LuaScriptInfo::SessionSetup) {
367 scripts.push_back (*s);
370 std::sort (scripts.begin(), scripts.end(), LuaScripting::Sorter());
371 for (LuaScriptList::const_iterator s = scripts.begin(); s != scripts.end(); ++s) {
372 if ((*s)->name == template_name) {
373 session_template = "urn:ardour:" + (*s)->path;
374 have_resolved_template_name = true;
379 if (!have_resolved_template_name) {
380 /* this will produce a more or less meaninful error later:
381 * "ERROR: Could not open session template [abs-path to user-config dir]"
383 session_template = Glib::build_filename (ARDOUR::user_template_directory (), template_name);
387 /* We don't know what this is, because the session is new and the
388 * command line doesn't let us specify it. The user will get to decide
389 * in the audio/MIDI dialog.
392 session_existing_sample_rate = 0;
393 session_is_new = true;
395 /* this is an arbitrary default value but since the user insists on
396 * starting a new session from the command line, it will do as well as
397 * any other possible value. I mean, seriously, what else could it be
401 bus_profile.master_out_channels = 2;
408 * 1: failure but user can retry
409 * 0: success, seesion parameters ready for use
412 StartupFSM::check_session_parameters (bool must_be_new)
414 bool requested_new = false;
416 session_name = session_dialog->session_name (requested_new);
417 session_path = session_dialog->session_folder ();
420 assert (requested_new);
425 /* See if the specified session is a session archive */
427 int rv = ARDOUR::inflate_session (session_name, Config->get_default_session_parent_dir(), session_path, session_name);
429 MessageDialog msg (*session_dialog, string_compose (_("Extracting session-archive failed: %1"), inflate_error (rv)));
433 } else if (rv == 0) {
434 /* names are good (and session is unarchived/inflated */
439 /* check for ".ardour" in statefile name, because we don't want
442 * XXX Note this wierd conflation of a
443 * file-name-without-a-suffix and the session name. It's not
444 * really a session name at all, but rather the suffix-free
445 * name of a statefile (snapshot).
448 const string::size_type suffix_at = session_name.find (statefile_suffix);
450 if (suffix_at != string::npos) {
451 session_name = session_name.substr (0, suffix_at);
454 /* this shouldn't happen, but we catch it just in case it does */
456 if (session_name.empty()) {
457 return 1; /* keep running dialog */
460 if (session_dialog->use_session_template()) {
461 session_template = session_dialog->session_template_name();
464 if (session_name[0] == G_DIR_SEPARATOR ||
465 #ifdef PLATFORM_WINDOWS
466 // Windows file system .. detect absolute path
468 (session_name.length() > 3 && session_name[1] == ':' && session_name[2] == G_DIR_SEPARATOR)
470 // Sensible file systems
472 (session_name.length() > 2 && session_name[0] == '.' && session_name[1] == G_DIR_SEPARATOR) ||
473 (session_name.length() > 3 && session_name[0] == '.' && session_name[1] == '.' && session_name[2] == G_DIR_SEPARATOR)
478 /* user typed absolute path or cwd-relative path
479 specified into session name field. So ... infer
480 session path and name from what was given.
483 session_path = Glib::path_get_dirname (session_name);
484 session_name = Glib::path_get_basename (session_name);
488 /* session name is just a name */
491 /* check if name is legal */
493 const char illegal = Session::session_name_is_legal (session_name);
496 MessageDialog msg (*session_dialog,
497 string_compose (_("To ensure compatibility with various systems\n"
498 "session names may not contain a '%1' character"),
501 ARDOUR_COMMAND_LINE::session_name = ""; // cancel that
502 return 1; /* keep running dialog */
506 /* check if the currently-exists status matches whether or not
510 if (Glib::file_test (session_path, Glib::FileTest (G_FILE_TEST_EXISTS | G_FILE_TEST_IS_DIR))) {
512 if (requested_new /*&& !nsm*/) {
514 std::string existing = Glib::build_filename (session_path, session_name);
516 if (!ask_about_loading_existing_session (existing)) {
517 session_dialog->clear_name ();
518 return 1; /* try again */
522 session_is_new = false;
526 /* does not exist at present */
528 if (!requested_new) {
529 // pop_back_splash (session_dialog);
530 MessageDialog msg (string_compose (_("There is no existing session at \"%1\""), session_path));
532 session_dialog->clear_name();
536 session_is_new = true;
541 string program_version;
542 const string statefile_path = Glib::build_filename (session_path, session_name + ARDOUR::statefile_suffix);
544 if (!session_is_new) {
546 if (Session::get_info_from_path (statefile_path, sr, fmt, program_version)) {
547 /* exists but we can't read it */
551 session_existing_sample_rate = sr;
555 bus_profile.master_out_channels = session_dialog->master_channel_count ();
562 StartupFSM::copy_demo_sessions ()
564 // TODO: maybe IFF brand_new_user
565 if (ARDOUR::Profile->get_mixbus () && Config->get_copy_demo_sessions ()) {
566 std::string dspd (Config->get_default_session_parent_dir());
567 Searchpath ds (ARDOUR::ardour_data_search_path());
568 ds.add_subdirectory_to_paths ("sessions");
569 vector<string> demos;
570 find_files_matching_pattern (demos, ds, ARDOUR::session_archive_suffix);
572 ARDOUR::RecentSessions rs;
573 ARDOUR::read_recent_sessions (rs);
575 for (vector<string>::iterator i = demos.begin(); i != demos.end (); ++i) {
576 /* "demo-session" must be inside "demo-session.<session_archive_suffix>" */
577 std::string name = basename_nosuffix (basename_nosuffix (*i));
578 std::string path = Glib::build_filename (dspd, name);
579 /* skip if session-dir already exists */
580 if (Glib::file_test(path.c_str(), Glib::FILE_TEST_IS_DIR)) {
583 /* skip sessions that are already in 'recent'.
584 * eg. a new user changed <session-default-dir> shorly after installation
586 for (ARDOUR::RecentSessions::iterator r = rs.begin(); r != rs.end(); ++r) {
587 if ((*r).first == name) {
592 PBD::FileArchive ar (*i);
593 if (0 == ar.inflate (dspd)) {
594 store_recent_sessions (name, path);
595 info << string_compose (_("Copied Demo Session %1."), name) << endmsg;
605 StartupFSM::load_from_application_api (const std::string& path)
607 /* macOS El Capitan (and probably later) now somehow passes the command
608 line arguments to an app via the openFile delegate protocol. Ardour
609 already does its own command line processing, and having both
610 pathways active causes crashes. So, if the command line was already
611 set, do nothing here.
614 if (!ARDOUR_COMMAND_LINE::session_name.empty()) {
618 /* Cancel SessionDialog if it's visible to make macOS delegates work.
620 * there's a race condition here: we connect to app->ShouldLoad
621 * and then at some point (might) show a session dialog. The race is
622 * caused by the non-deterministic interaction between the macOS event
623 * loop(s) and the GDK one(s).
625 * - ShouldLoad does not arrive before we show the session dialog
626 * -> here we should hide the session dialog, then use the
627 * supplied path as if it was provided on the command line
628 * - ShouldLoad signal arrives before we show a session dialog
629 * -> don't bother showing the session dialog, just use the
630 * supplied path as if it was provided on the command line
634 if (session_dialog) {
635 session_dialog->hide ();
636 delete_when_idle (session_dialog);
640 /* no command line argument given ... must just be via
641 * desktop/finder/window manager API (e.g. double click on "foo.ardour"
645 if (get_session_parameters_from_path (path, string(), false)) {
646 _signal_response (LoadSession);
650 /* given parameters failed for some reason. This is probably true
651 * anyway, but force it to be true and then carry on with whatever the
652 * main event loop is doing.
655 _state = NeedSessionPath;
659 StartupFSM::ask_about_loading_existing_session (const std::string& session_path)
661 std::string str = string_compose (_("This session\n%1\nalready exists. Do you want to open it?"), session_path);
663 MessageDialog msg (str,
665 Gtk::MESSAGE_WARNING,
670 msg.set_name (X_("OpenExistingDialog"));
671 msg.set_title (_("Open Existing Session"));
672 msg.set_wmclass (X_("existing_session"), PROGRAM_NAME);
673 msg.set_position (Gtk::WIN_POS_CENTER);
674 // pop_back_splash (msg);