/* * Copyright (C) 2025 Robin Gareus * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ #include #include #include "pbd/basename.h" #include "pbd/file_utils.h" #include "pbd/replace_all.h" #include "ardour/directory_names.h" #include "ardour/filename_extensions.h" #include "ardour/filesystem_paths.h" #include "ardour/recent_sessions.h" #include "ardour/session.h" #include "gtkmm2ext/utils.h" #include "widgets/ardour_button.h" #include "widgets/ardour_dropdown.h" #include "widgets/ardour_spacer.h" #include "strip_import_dialog.h" #include "pbd/i18n.h" using namespace Gtk; using namespace std; using namespace PBD; using namespace ARDOUR; using namespace ArdourWidgets; StripImportDialog::StripImportDialog (Session* s) : ArdourDialog (_("Import Track/Bus State")) , _add_rid (0) , _add_eid (0) { set_session (s); _open_button = manage (new Button (Stock::GO_FORWARD)); _ok_button = manage (new Button (Stock::OK)); get_action_area ()->pack_start (_info_text); add_button (Stock::CANCEL, RESPONSE_CANCEL); get_action_area ()->pack_end (*_open_button); get_action_area ()->pack_end (*_ok_button); _open_button->show (); _ok_button->hide (); _open_button->signal_clicked ().connect (mem_fun (*this, &StripImportDialog::maybe_switch_to_import_page), false); _ok_button->signal_clicked ().connect (mem_fun (*this, &StripImportDialog::ok_activated), false); _open_button->set_sensitive (false); _ok_button->set_sensitive (false); get_vbox ()->pack_start (_page_file); setup_file_page (); } StripImportDialog::~StripImportDialog () { _notebook_connection.disconnect (); _chooser_connection.disconnect (); } /* **************************************************************************** * Page One .. pick file to import */ void StripImportDialog::setup_file_page () { /* file-chooser */ _chooser.set_size_request (450, 300); _chooser.set_current_folder (poor_mans_glob (Config->get_default_session_parent_dir ())); FileFilter tracks_filter; tracks_filter.add_pattern (string_compose (X_("*%1"), routestate_suffix)); tracks_filter.set_name (string_compose (_("%1 tracks"), PROGRAM_NAME)); _chooser.add_filter (tracks_filter); FileFilter template_filter; template_filter.add_pattern (string_compose (X_("*%1"), template_suffix)); template_filter.set_name (string_compose (_("%1 tracks"), PROGRAM_NAME)); _chooser.add_filter (template_filter); FileFilter session_filter; session_filter.add_pattern (string_compose (X_("*%1"), ARDOUR::statefile_suffix)); session_filter.set_name (string_compose (_("%1 sessions"), PROGRAM_NAME)); _chooser.add_filter (session_filter); FileFilter all_filter; all_filter.add_pattern (string_compose (X_("*%1"), ARDOUR::statefile_suffix)); all_filter.add_pattern (string_compose (X_("*%1"), template_suffix)); all_filter.add_pattern (string_compose (X_("*%1"), routestate_suffix)); all_filter.set_name (_("All supported files")); _chooser.add_filter (all_filter); _chooser.set_filter (all_filter); Gtkmm2ext::add_volume_shortcuts (_chooser); _chooser_connection = _chooser.signal_selection_changed ().connect (mem_fun (*this, &StripImportDialog::file_selection_changed)); _chooser.signal_file_activated ().connect (sigc::mem_fun (*this, &StripImportDialog::maybe_switch_to_import_page)); _notebook.append_page (_chooser, _("File")); guint page = 1; /* recent Sessions */ ARDOUR::RecentSessions rs; ARDOUR::read_recent_sessions (rs); if (!rs.empty ()) { _recent_model = TreeStore::create (_columns); /* setup model - compare to SessionDialog::redisplay_recent_sessions */ for (auto const& [_, dir] : rs) { string dirname = dir; if (dirname.empty ()) { continue; } if (dirname[dirname.length () - 1] == '/') { dirname = dirname.substr (0, dirname.length () - 1); } /* check whether session still exists */ if (!Glib::file_test (dirname, Glib::FILE_TEST_EXISTS)) { /* session doesn't exist */ continue; } /* now get available states for this session */ vector state_file_names = Session::possible_states (dirname); if (state_file_names.empty ()) { /* no state file? */ continue; } TreeModel::Row row = *(_recent_model->append ()); if (state_file_names.size () > 1) { row[_columns.name] = PBD::basename_nosuffix (dirname); row[_columns.path] = ""; for (auto const& snap : state_file_names) { Gtk::TreeModel::Row child_row = *(_recent_model->append (row.children ())); child_row[_columns.name] = snap; child_row[_columns.path] = Glib::build_filename (dirname, snap + statefile_suffix); } } else { row[_columns.name] = state_file_names.front (); row[_columns.path] = Glib::build_filename (dirname, state_file_names.front () + statefile_suffix); } } _recent_treeview.set_model (_recent_model); _recent_treeview.append_column (_("Session Name"), _columns.name); _recent_treeview.set_headers_visible (true); _recent_treeview.get_selection ()->set_mode (SELECTION_SINGLE); _recent_treeview.get_selection ()->signal_changed ().connect (sigc::bind (sigc::mem_fun (*this, &StripImportDialog::treeview_selection_changed), &_recent_treeview, Snapshot)); _recent_treeview.signal_row_activated ().connect ([&] (const TreeModel::Path&, TreeViewColumn*) { maybe_switch_to_import_page (); }); _recent_scroller.set_policy (POLICY_AUTOMATIC, POLICY_AUTOMATIC); _recent_scroller.add (_recent_treeview); _notebook.append_page (_recent_scroller, _("Recent Sessions")); _notebook_type[page] = Snapshot; _notebook_content[page++] = &_recent_treeview; } /* template list */ vector templates; find_session_templates (templates, false); if (templates.size () > 0) { _template_model = ListStore::create (_columns); setup_model (_template_model, templates); _template_treeview.set_model (_template_model); _template_treeview.append_column (_("Name"), _columns.name); _template_treeview.set_headers_visible (true); _template_treeview.get_selection ()->signal_changed ().connect (sigc::bind (sigc::mem_fun (*this, &StripImportDialog::treeview_selection_changed), &_template_treeview, Template)); _template_treeview.signal_row_activated ().connect ([&] (const TreeModel::Path&, TreeViewColumn*) { maybe_switch_to_import_page (); }); _template_scroller.set_policy (POLICY_AUTOMATIC, POLICY_AUTOMATIC); _template_scroller.add (_template_treeview); _notebook.append_page (_template_scroller, _("Session Templates")); _notebook_type[page] = Template; _notebook_content[page++] = &_template_treeview; } templates.clear (); Searchpath local_path (_session->path ()); local_path.add_subdirectory_to_paths (routestates_dir_name); find_presets (local_path, templates); if (templates.size () > 0) { _local_pset_model = ListStore::create (_columns); setup_model (_local_pset_model, templates); _local_pset_treeview.set_model (_local_pset_model); _local_pset_treeview.append_column (_("Name"), _columns.name); _local_pset_treeview.set_headers_visible (true); _local_pset_treeview.get_selection ()->signal_changed ().connect (sigc::bind (sigc::mem_fun (*this, &StripImportDialog::treeview_selection_changed), &_local_pset_treeview, RouteState)); _local_pset_treeview.signal_row_activated ().connect ([&] (const TreeModel::Path&, TreeViewColumn*) { maybe_switch_to_import_page (); }); _local_pset_scroller.set_policy (POLICY_AUTOMATIC, POLICY_AUTOMATIC); _local_pset_scroller.add (_local_pset_treeview); _notebook.append_page (_local_pset_scroller, _("Local Strip Templates")); _notebook_type[page] = RouteState; _notebook_content[page++] = &_local_pset_treeview; } templates.clear (); Searchpath global_path (ardour_data_search_path ()); global_path.add_subdirectory_to_paths (routestates_dir_name); find_presets (global_path, templates); if (templates.size () > 0) { _global_pset_model = ListStore::create (_columns); setup_model (_global_pset_model, templates); _global_pset_treeview.set_model (_global_pset_model); _global_pset_treeview.append_column (_("Name"), _columns.name); _global_pset_treeview.set_headers_visible (true); _global_pset_treeview.get_selection ()->signal_changed ().connect (sigc::bind (sigc::mem_fun (*this, &StripImportDialog::treeview_selection_changed), &_global_pset_treeview, RouteState)); _global_pset_treeview.signal_row_activated ().connect ([&] (const TreeModel::Path&, TreeViewColumn*) { maybe_switch_to_import_page (); }); _global_pset_scroller.set_policy (POLICY_AUTOMATIC, POLICY_AUTOMATIC); _global_pset_scroller.add (_global_pset_treeview); _notebook.append_page (_global_pset_scroller, _("Global Strip Templates")); _notebook_type[page] = RouteState; _notebook_content[page++] = &_global_pset_treeview; } _notebook_connection = _notebook.signal_switch_page ().connect (sigc::mem_fun (*this, &StripImportDialog::page_changed)); _page_file.pack_start (_notebook); _page_file.show_all (); if (!rs.empty ()) { _notebook.set_current_page (1); } else if (page > 1) { _notebook.set_current_page (page - 1); } } void StripImportDialog::page_changed (GtkNotebookPage*, guint page) { switch (page) { case 0: file_selection_changed (); break; default: treeview_selection_changed (_notebook_content.at (page), _notebook_type.at (page)); break; } } void StripImportDialog::setup_model (Glib::RefPtr model, vector const& templates) { for (auto const& t : templates) { TreeModel::Row row; row = *(model->append ()); row[_columns.name] = t.name; row[_columns.path] = t.path; } } void StripImportDialog::find_presets (Searchpath const& search_path, vector& template_info) { vector templates; /* clang-format off */ find_paths_matching_filter (templates, search_path, [] (const string& str, void*) { return Glib::file_test (str, Glib::FILE_TEST_IS_DIR); }, 0, true, true); /* clang-format on */ if (templates.empty ()) { return; } for (vector::iterator i = templates.begin (); i != templates.end (); ++i) { string file = session_template_dir_to_file (*i); TemplateInfo rti; rti.name = Glib::path_get_basename (*i); rti.path = *i; template_info.push_back (rti); } std::sort (template_info.begin (), template_info.end ()); } void StripImportDialog::file_selection_changed () { parse_route_state (_chooser.get_filename ()); } static string template_dir_to_file (string const& dir, string const& suffix) { return Glib::build_filename (dir, Glib::path_get_basename (dir) + suffix); } void StripImportDialog::treeview_selection_changed (Gtk::TreeView* treeview, SelectionType t) { Gtk::TreeModel::const_iterator selection = treeview->get_selection ()->get_selected (); if (selection) { switch (t) { case Template: parse_route_state (template_dir_to_file ((*selection)[_columns.path], template_suffix)); break; case RouteState: parse_route_state (template_dir_to_file ((*selection)[_columns.path], routestate_suffix)); break; case Snapshot: parse_route_state ((*selection)[_columns.path]); break; } } else { parse_route_state (""); } } void StripImportDialog::parse_route_state (std::string const& path) { _extern_map.clear (); _path = path; if (path.empty ()) { goto out; } if (!Glib::file_test (path, Glib::FILE_TEST_IS_REGULAR)) { goto out; } _extern_map = _session->parse_route_state (path, _match_pbd_id); out: if (_extern_map.empty ()) { _info_text.set_text (""); _info_text.hide (); } else { _info_text.set_text (string_compose (P_("%1 Track", "%1 Tracks", _extern_map.size ()), _extern_map.size ())); _info_text.show (); } _open_button->set_sensitive (!_extern_map.empty ()); } void StripImportDialog::maybe_switch_to_import_page () { if (_extern_map.empty ()) { return; } // TODO cleanup managed items on _page_file /* -> next page */ setup_strip_import_page (); get_vbox ()->remove (_page_file); get_vbox ()->pack_start (_page_strip); _info_text.hide (); _open_button->hide (); _ok_button->show (); } /* **************************************************************************** * Page Two map Tracks/State */ void StripImportDialog::refill_import_table () { Gtk::Label* l; Gtkmm2ext::container_clear (_strip_table, true); _strip_table.set_spacings (3); Glib::RefPtr col_size_group (SizeGroup::create (SIZE_GROUP_HORIZONTAL)); l = manage (new Label (string_compose ("%1", _("Local Track/Bus")))); l->set_use_markup (); _strip_table.attach (*l, 0, 1, 0, 1, Gtk::FILL, Gtk::SHRINK); col_size_group->add_widget (*l); l = manage (new Label (string_compose ("%1", _("External State")))); l->set_use_markup (); _strip_table.attach (*l, 2, 3, 0, 1, Gtk::FILL, Gtk::SHRINK); col_size_group->add_widget (*l); /* clang-format off */ _strip_table.attach (*manage (new ArdourVSpacer (1.0)), 1, 2, 0, 1, SHRINK , EXPAND | FILL, 8, 4); _strip_table.attach (*manage (new ArdourHSpacer (1.0)), 0, 4, 1, 2, EXPAND | FILL, SHRINK, 4, 8); /* clang-format on */ std::vector> sorted_map; for (auto const& i : _import_map) { sorted_map.push_back (i); } std::sort (sorted_map.begin (), sorted_map.end (), [=] (auto& a, auto& b) { try { return _route_map.at (a.first).pi.order () < _route_map.at (b.first).pi.order (); } catch (...) { } return a.second < b.second; }); /* Refill table */ int r = 1; for (auto& [rid, eid] : sorted_map /*_import_map*/) { ++r; if (_route_map.find (rid) != _route_map.end ()) { l = manage (new Label (_route_map.at (rid).name, 0, 0.5)); } else { l = manage (new Label (_("New Track"), 0, 0.5)); l->set_use_markup (); } _strip_table.attach (*l, 0, 1, r, r + 1, EXPAND | FILL, SHRINK); #if 0 l = manage (new Label (_extern_map[eid], 1.0, 0.5)); _strip_table.attach (*l, 2, 3, r, r + 1, EXPAND | FILL, SHRINK); #else using namespace Menu_Helpers; ArdourDropdown* dd = manage (new ArdourDropdown ()); for (auto& [eid, einfo] : _extern_map) { dd->add_menu_elem (MenuElem (Gtkmm2ext::markup_escape_text (einfo.name), sigc::bind (sigc::mem_fun (*this, &StripImportDialog::change_mapping), dd, rid, eid, einfo.name))); } dd->set_text (_extern_map.at (eid).name); _strip_table.attach (*dd, 2, 3, r, r + 1, EXPAND | FILL, SHRINK); #endif ArdourButton* rm = manage (new ArdourButton ()); rm->set_icon (ArdourIcon::CloseCross); rm->signal_clicked.connect (sigc::bind (sigc::mem_fun (*this, &StripImportDialog::remove_mapping), rid)); _strip_table.attach (*rm, 3, 4, r, r + 1, Gtk::SHRINK, Gtk::SHRINK, 4, 2); } if (r > 1) { ++r; _strip_table.attach (*manage (new ArdourVSpacer (1.0)), 1, 2, 2, r, SHRINK, EXPAND | FILL, 8, 4); ++r; _strip_table.attach (*manage (new ArdourHSpacer (1.0)), 0, 4, r, r + 1, EXPAND | FILL, SHRINK, 4, 8); } ++r; /* Add options */ using namespace Menu_Helpers; _add_rid = PBD::ID (0); _add_eid = PBD::ID (0); int64_t next_id = std::numeric_limits::max () - 1; PBD::ID next_new = PBD::ID (next_id); while (_import_map.find (next_new) != _import_map.end ()) { --next_id; next_new = PBD::ID (next_id); } /* accumulate both dropdowns, so columns are equally spaced */ std::vector sizing_texts; _add_rid_dropdown = manage (new ArdourWidgets::ArdourDropdown ()); _add_rid_dropdown->add_menu_elem (MenuElem (_(" -- New Track -- "), sigc::bind (sigc::mem_fun (*this, &StripImportDialog::prepare_mapping), false, next_new, _("New Track")))); sizing_texts.push_back (_(" -- New Track -- ")); for (auto& [rid, rinfo] : _route_map) { if (_import_map.find (rid) != _import_map.end ()) { continue; } _add_rid_dropdown->add_menu_elem (MenuElem (Gtkmm2ext::markup_escape_text (rinfo.name), sigc::bind (sigc::mem_fun (*this, &StripImportDialog::prepare_mapping), false, rid, rinfo.name))); sizing_texts.push_back (rinfo.name); } _add_eid_dropdown = manage (new ArdourWidgets::ArdourDropdown ()); for (auto& [eid, einfo] : _extern_map) { _add_eid_dropdown->add_menu_elem (MenuElem (Gtkmm2ext::markup_escape_text (einfo.name), sigc::bind (sigc::mem_fun (*this, &StripImportDialog::prepare_mapping), true, eid, einfo.name))); sizing_texts.push_back (einfo.name); } _add_rid_dropdown->set_sizing_texts (sizing_texts); _add_eid_dropdown->set_sizing_texts (sizing_texts); col_size_group->add_widget (*_add_rid_dropdown); col_size_group->add_widget (*_add_eid_dropdown); _add_new_mapping = manage (new ArdourButton ()); _add_new_mapping->set_icon (ArdourIcon::PlusSign); _add_new_mapping->signal_clicked.connect (sigc::mem_fun (*this, &StripImportDialog::add_mapping)); /* clang-format off */ _strip_table.attach (*_add_rid_dropdown, 0, 1, r, r + 1, EXPAND | FILL, SHRINK); _strip_table.attach (*_add_eid_dropdown, 2, 3, r, r + 1, EXPAND | FILL, SHRINK); _strip_table.attach (*_add_new_mapping, 3, 4, r, r + 1, Gtk::SHRINK, SHRINK); /* clang-format on */ bool can_add = !_add_rid_dropdown->items ().empty () && !_add_eid_dropdown->items ().empty (); _add_rid_dropdown->set_sensitive (can_add); _add_eid_dropdown->set_sensitive (can_add); _add_new_mapping->set_sensitive (false); _ok_button->set_sensitive (!_import_map.empty ()); _strip_table.show_all (); } void StripImportDialog::idle_refill_import_table () { Glib::signal_idle ().connect ([&] () { refill_import_table (); return false; }, Glib::PRIORITY_HIGH_IDLE + 10); } void StripImportDialog::change_mapping (ArdourDropdown* dd, PBD::ID const& rid, PBD::ID const& eid, std::string const& name) { dd->set_text (name); _import_map[rid] = eid; } void StripImportDialog::prepare_mapping (bool ext, PBD::ID const& id, std::string const& name) { if (ext) { _add_eid_dropdown->set_text (name); _add_eid = id; } else { _add_rid_dropdown->set_text (name); _add_rid = id; } _add_new_mapping->set_sensitive (_add_rid != PBD::ID (0) && _add_eid != PBD::ID (0)); } void StripImportDialog::add_mapping () { assert (_add_rid != PBD::ID (0)); assert (_add_eid != PBD::ID (0)); _default_mapping = false; _import_map[_add_rid] = _add_eid; idle_refill_import_table (); } void StripImportDialog::remove_mapping (PBD::ID const& id) { if (1 == _import_map.erase (id)) { _default_mapping = false; idle_refill_import_table (); } } void StripImportDialog::clear_mapping () { _default_mapping = false; _import_map.clear (); idle_refill_import_table (); } void StripImportDialog::import_all_strips () { _default_mapping = false; _import_map.clear (); int64_t next_id = std::numeric_limits::max () - 1 - _extern_map.size (); for (auto& [eid, einfo] : _extern_map) { PBD::ID next_new = PBD::ID (next_id++); _import_map[next_new] = eid; } idle_refill_import_table (); } void StripImportDialog::set_default_mapping (bool and_idle_update) { _import_map.clear (); _default_mapping = true; if (_match_pbd_id) { /* try a 1:1 mapping */ for (auto& [eid, einfo] : _extern_map) { if (_route_map.find (eid) != _route_map.end ()) { _import_map[eid] = eid; } } } else { /* match by name */ for (auto& [eid, einfo] : _extern_map) { // TODO consider building a reverse [pointer] map for (auto& [rid, rinfo] : _route_map) { #ifdef MIXBUS if (einfo.mixbus > 0 && einfo.mixbus == rinfo.mixbus) { _import_map[rid] = eid; break; } #endif if (einfo == rinfo) { _import_map[rid] = eid; break; } } } } if (and_idle_update) { idle_refill_import_table (); } } void StripImportDialog::setup_strip_import_page () { _route_map.clear (); for (auto const& r : *_session->get_routes ()) { #ifdef MIXBUS _route_map.emplace (r->id (), Session::RouteImportInfo (r->name (), r->presentation_info (), c->mixbus ())); #else _route_map.emplace (r->id (), Session::RouteImportInfo (r->name (), r->presentation_info (), 0)); #endif } set_default_mapping (false); refill_import_table (); _clear_mapping = new ArdourButton (_("Clear Mapping")); _reset_mapping = new ArdourButton (_match_pbd_id ? _("Reset - auto-map by ID") : _("Reset - auto-map by name")); _import_strips = new ArdourButton (_("Import all as new tracks")); _clear_mapping->signal_clicked.connect (mem_fun (*this, &StripImportDialog::clear_mapping)); _import_strips->signal_clicked.connect (mem_fun (*this, &StripImportDialog::import_all_strips)); _reset_mapping->signal_clicked.connect (sigc::bind (mem_fun (*this, &StripImportDialog::set_default_mapping), true)); HBox* hbox = manage (new HBox ()); hbox->set_spacing (4); hbox->pack_start (*_clear_mapping, true, false); hbox->pack_start (*_import_strips, true, false); hbox->pack_start (*_reset_mapping, true, false); VBox* vbox = manage (new VBox ()); vbox->pack_start (_strip_table, false, false, 4); _strip_scroller.set_policy (POLICY_NEVER, POLICY_AUTOMATIC); _strip_scroller.add (*vbox); _page_strip.set_spacing (4); _page_strip.pack_start (_strip_scroller); _page_strip.pack_end (*hbox, false, false, 4); _page_strip.show_all (); _ok_button->set_sensitive (true); // XXX } void StripImportDialog::ok_activated () { _session->import_route_state (_path, _import_map); ArdourDialog::on_response (RESPONSE_ACCEPT); }