/* * Copyright (C) 2019-2021 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 #include "pbd/basename.h" #include "pbd/file_utils.h" #include "pbd/pathexpand.h" #include "pbd/search_path.h" #include "ardour/audiofilesource.h" #include "ardour/audioregion.h" #include "ardour/auditioner.h" #include "ardour/directory_names.h" #include "ardour/filesystem_paths.h" #include "ardour/midi_region.h" #include "ardour/region_factory.h" #include "ardour/smf_source.h" #include "ardour/source_factory.h" #include "ardour/srcfilesource.h" #include "gtkmm2ext/gui_thread.h" #include "gtkmm2ext/menu_elems.h" #include "gtkmm2ext/utils.h" #include "widgets/paths_dialog.h" #include "trigger_clip_picker.h" #include "ui_config.h" #include "pbd/i18n.h" using namespace Gtk; using namespace PBD; using namespace ARDOUR; TriggerClipPicker::TriggerClipPicker () : _fcd (_("Select Sample Folder"), FILE_CHOOSER_ACTION_SELECT_FOLDER) , _seek_slider (0, 1000, 1) , _autoplay_btn (_("Auto-play")) , _seeking (false) { /* Setup Dropdown / File Browser */ #ifdef __APPLE__ try { /* add_shortcut_folder throws an exception if the folder being added already has a shortcut */ _fcd.add_shortcut_folder_uri ("file:///Library/GarageBand/Apple Loops"); _fcd.add_shortcut_folder_uri ("file:///Library/Audio/Apple Loops"); _fcd.add_shortcut_folder_uri ("file:///Library/Application Support/GarageBand/Instrument Library/Sampler/Sampler Files"); } catch (Glib::Error & e) {} #endif Gtkmm2ext::add_volume_shortcuts (_fcd); _fcd.add_button (Stock::CANCEL, RESPONSE_CANCEL); _fcd.add_button (Stock::ADD, RESPONSE_ACCEPT); _fcd.add_button (Stock::OPEN, RESPONSE_OK); refill_dropdown (); /* Audition */ _autoplay_btn.set_active (UIConfiguration::instance ().get_autoplay_clips ()); _seek_slider.set_draw_value (false); _seek_slider.add_events (Gdk::BUTTON_PRESS_MASK | Gdk::BUTTON_RELEASE_MASK); _seek_slider.signal_button_press_event ().connect (sigc::mem_fun (*this, &TriggerClipPicker::seek_button_press), false); _seek_slider.signal_button_release_event ().connect (sigc::mem_fun (*this, &TriggerClipPicker::seek_button_release), false); _play_btn.set_name ("generic button"); _play_btn.set_icon (ArdourWidgets::ArdourIcon::TransportPlay); _play_btn.signal_clicked.connect (sigc::mem_fun (*this, &TriggerClipPicker::audition_selected)); _stop_btn.set_name ("generic button"); _stop_btn.set_icon (ArdourWidgets::ArdourIcon::TransportStop); _stop_btn.signal_clicked.connect (sigc::mem_fun (*this, &TriggerClipPicker::stop_audition)); _play_btn.set_sensitive (false); _stop_btn.set_sensitive (false); _autoplay_btn.set_can_focus(false); _autoplay_btn.signal_toggled ().connect (sigc::mem_fun (*this, &TriggerClipPicker::autoplay_toggled)); /* Layout */ _auditable.set_homogeneous(false); _auditable.attach (_play_btn, 0, 1, 0, 1, SHRINK, SHRINK); _auditable.attach (_stop_btn, 1, 2, 0, 1, SHRINK, SHRINK); _auditable.attach (_autoplay_btn, 2, 3, 0, 1, EXPAND | FILL, SHRINK); _auditable.attach (_seek_slider, 0, 3, 1, 2, EXPAND | FILL, SHRINK); _auditable.set_border_width (4); _auditable.set_spacings (4); _scroller.set_policy (POLICY_AUTOMATIC, POLICY_AUTOMATIC); _scroller.add (_view); pack_start (_dir, false, false, 4); pack_start (_scroller); pack_start (_auditable, false, false); /* TreeView */ _model = TreeStore::create (_columns); _view.set_model (_model); _view.append_column (_("File Name"), _columns.name); _view.set_headers_visible (false); //TODO: show headers when we have size/tags/etc _view.set_reorderable (false); _view.get_selection ()->set_mode (SELECTION_MULTIPLE); /* DnD source */ std::vector dnd; dnd.push_back (TargetEntry ("text/uri-list")); _view.drag_source_set (dnd, Gdk::MODIFIER_MASK, Gdk::ACTION_COPY); _view.get_selection ()->signal_changed ().connect (sigc::mem_fun (*this, &TriggerClipPicker::row_selected)); _view.signal_row_activated ().connect (sigc::mem_fun (*this, &TriggerClipPicker::row_activated)); _view.signal_test_expand_row ().connect (sigc::mem_fun (*this, &TriggerClipPicker::test_expand)); _view.signal_row_collapsed ().connect (sigc::mem_fun (*this, &TriggerClipPicker::row_collapsed)); _view.signal_drag_data_get ().connect (sigc::mem_fun (*this, &TriggerClipPicker::drag_data_get)); _view.signal_cursor_changed ().connect (sigc::mem_fun (*this, &TriggerClipPicker::cursor_changed)); _view.signal_drag_end ().connect (sigc::mem_fun (*this, &TriggerClipPicker::drag_end)); /* DnD target */ std::vector target_table; target_table.push_back (Gtk::TargetEntry ("x-ardour/region.pbdid", Gtk::TARGET_SAME_APP)); _view.drag_dest_set (target_table, DEST_DEFAULT_ALL, Gdk::ACTION_COPY); _view.signal_drag_begin ().connect (sigc::mem_fun (*this, &TriggerClipPicker::drag_begin)); _view.signal_drag_motion ().connect (sigc::mem_fun (*this, &TriggerClipPicker::drag_motion)); _view.signal_drag_data_received ().connect (sigc::mem_fun (*this, &TriggerClipPicker::drag_data_received)); Config->ParameterChanged.connect (_config_connection, invalidator (*this), boost::bind (&TriggerClipPicker::parameter_changed, this, _1), gui_context ()); /* show off */ _scroller.show (); _view.show (); _dir.show (); _auditable.show_all (); /* fill treeview with data */ _dir.items ().front ().activate (); } TriggerClipPicker::~TriggerClipPicker () { } void TriggerClipPicker::parameter_changed (std::string const& p) { if (p == "sample-lib-path") { refill_dropdown (); } } /* **************************************************************************** * Paths Dropdown Callbacks */ void TriggerClipPicker::edit_path () { Gtk::Window* tlw = dynamic_cast (get_toplevel ()); assert (tlw); ArdourWidgets::PathsDialog pd (*tlw, _("Edit Sample Library Path"), Config->get_sample_lib_path (), ""); if (pd.run () != Gtk::RESPONSE_ACCEPT) { return; } Config->set_sample_lib_path (pd.get_serialized_paths ()); } void TriggerClipPicker::refill_dropdown () { _dir.clear_items (); _root_paths.clear (); /* Bundled Content */ Searchpath spath (ardour_data_search_path ()); spath.add_subdirectory_to_paths (media_dir_name); for (auto const& f : spath) { maybe_add_dir (f); } /* User config folder */ maybe_add_dir (Glib::build_filename (user_config_directory (), media_dir_name)); /* Anything added by Gtkmm2ext::add_volume_shortcuts */ for (auto const& f : _fcd.list_shortcut_folders ()) { maybe_add_dir (f); } /* Custom Paths */ assert (_dir.items ().size () > 0); if (!Config->get_sample_lib_path ().empty ()) { _dir.AddMenuElem (Menu_Helpers::SeparatorElem ()); Searchpath cpath (Config->get_sample_lib_path ()); for (auto const& f : cpath) { maybe_add_dir (f); } } _dir.AddMenuElem (Menu_Helpers::SeparatorElem ()); _dir.AddMenuElem (Menu_Helpers::MenuElem (_("Edit..."), sigc::mem_fun (*this, &TriggerClipPicker::edit_path))); _dir.AddMenuElem (Menu_Helpers::MenuElem (_("Other..."), sigc::mem_fun (*this, &TriggerClipPicker::open_dir))); } static bool is_subfolder (std::string const& parent, std::string dir) { assert (Glib::file_test (dir, Glib::FILE_TEST_IS_DIR | Glib::FILE_TEST_EXISTS)); assert (Glib::file_test (parent, Glib::FILE_TEST_IS_DIR | Glib::FILE_TEST_EXISTS)); if (parent.size () > dir.size ()) { return false; } if (parent == dir) { return false; } if (dir == Glib::path_get_dirname (dir)) { /* dir must be root */ return false; } while (parent.size () < dir.size ()) { /* step up, compare with parent */ dir = Glib::path_get_dirname (dir); if (parent == dir) { return true; } } return false; } static std::string display_name (std::string const& dir) { std::string metadata = Glib::build_filename (dir, ".daw-meta.xml"); if (Glib::file_test (metadata, Glib::FILE_TEST_IS_REGULAR | Glib::FILE_TEST_EXISTS)) { XMLTree tree; if (tree.read (metadata) && tree.root()->name () == "DAWDirectory") { XMLNode* root = tree.root(); std::string type; if (root->get_property ("type", type)) { if (type == "bundled") { return string_compose (_("%1 Bundled Content"), PROGRAM_NAME); } } #if ENABLE_NLS if (ARDOUR::translations_are_enabled ()) { for (XMLNodeList::const_iterator n = root->children ("title").begin (); n != root->children ("title").end (); ++n) { std::string lang; if (!(*n)->get_property ("lang", lang)) { continue; } if (lang != "en_US") { // TODO: get current lang continue; } return (*n)->child_content (); } } #endif /* pick first title, if any */ XMLNode* child = root->child ("title"); if (child) { return child->child_content (); } } } return Glib::path_get_basename (dir); } void TriggerClipPicker::maybe_add_dir (std::string const& dir) { if (!Glib::file_test (dir, Glib::FILE_TEST_IS_DIR | Glib::FILE_TEST_EXISTS)) { return; } _dir.AddMenuElem (Gtkmm2ext::MenuElemNoMnemonic (display_name (dir), sigc::bind (sigc::mem_fun (*this, &TriggerClipPicker::list_dir), dir, (Gtk::TreeNodeChildren*)0))); /* check if a parent path of the given dir already exists, * or if this new path is parent to any existing ones. */ bool insert = true; auto it = _root_paths.begin (); while (it != _root_paths.end ()) { bool erase = false; if (it->size () > dir.size()) { if (is_subfolder (dir, *it)) { erase = true; } } else if (is_subfolder (*it, dir)) { insert = false; break; } if (erase) { auto it2 = it; ++it; _root_paths.erase (it2); } else { ++it; } } if (insert) { _root_paths.insert (dir); } } /* **************************************************************************** * Treeview Callbacks */ void TriggerClipPicker::drag_begin (Glib::RefPtr const& context) { TreeView::Selection::ListHandle_Path rows = _view.get_selection ()->get_selected_rows (); if (!rows.empty()) { Glib::RefPtr< Gdk::Pixmap > pix = _view.create_row_drag_icon (*rows.begin ()); int w, h; pix->get_size (w, h); context->set_icon (pix->get_colormap (), pix, Glib::RefPtr (), 4, h / 2); } } void TriggerClipPicker::drag_end (Glib::RefPtr const&) { _session->cancel_audition (); } void TriggerClipPicker::cursor_changed () { if (!_session || !_autoplay_btn.get_active ()) { return; } _session->cancel_audition (); TreeModel::Path p; TreeViewColumn* col = NULL; _view.get_cursor (p, col); TreeModel::iterator i = _model->get_iter (p); /* This also plays the file if the cursor change deselects the row. * However, checking if `i` is _view.get_selection () does not reliably work from this context. */ if (i && (*i)[_columns.file]) { audition ((*i)[_columns.path]); } } void TriggerClipPicker::row_selected () { if (!_session) { return; } if (!_autoplay_btn.get_active ()) { _session->cancel_audition (); } if (_view.get_selection ()->count_selected_rows () < 1 || _autoplay_btn.get_active ()) { _play_btn.set_sensitive (false); } else { TreeView::Selection::ListHandle_Path rows = _view.get_selection ()->get_selected_rows (); TreeIter i = _model->get_iter (*rows.begin ()); _play_btn.set_sensitive ((*i)[_columns.file]); } } void TriggerClipPicker::row_activated (TreeModel::Path const& p, TreeViewColumn*) { TreeModel::iterator i = _model->get_iter (p); if (i && (*i)[_columns.file]) { audition ((*i)[_columns.path]); } else if (i) { list_dir ((*i)[_columns.path]); } } bool TriggerClipPicker::test_expand (TreeModel::iterator const& i, Gtk::TreeModel::Path const&) { TreeModel::Row row = *i; if (row[_columns.read]) { /* already expanded */ return false; /* OK */ } row[_columns.read] = true; /* remove stub */ _model->erase (row.children ().begin ()); list_dir (row[_columns.path], &row.children ()); return row.children ().size () == 0; } void TriggerClipPicker::row_collapsed (TreeModel::iterator const& i, Gtk::TreeModel::Path const&) { /* forget about expanded sub-view, refresh when expanded again */ TreeModel::Row row = *i; row[_columns.read] = false; Gtk::TreeIter ti; while ((ti = row.children ().begin ()) != row.children ().end ()) { _model->erase (ti); } /* add stub child */ row = *(_model->append (row.children ())); row[_columns.read] = false; } void TriggerClipPicker::drag_data_get (Glib::RefPtr const&, SelectionData& data, guint, guint time) { if (data.get_target () != "text/uri-list") { return; } std::vector uris; TreeView::Selection::ListHandle_Path rows = _view.get_selection ()->get_selected_rows (); for (TreeView::Selection::ListHandle_Path::iterator i = rows.begin (); i != rows.end (); ++i) { TreeIter iter; if ((iter = _model->get_iter (*i))) { if ((*iter)[_columns.file]) { uris.push_back (Glib::filename_to_uri ((*iter)[_columns.path])); } } } data.set_uris (uris); } bool TriggerClipPicker::drag_motion (Glib::RefPtr const& context, int, int y, guint time) { //list_dir ("/tmp/"); context->drag_status (Gdk::ACTION_COPY, time); return true; } void TriggerClipPicker::drag_data_received (Glib::RefPtr const& context, int /*x*/, int y, Gtk::SelectionData const& data, guint /*info*/, guint time) { if (data.get_target () != "x-ardour/region.pbdid") { return; } PBD::ID rid (data.get_data_as_string ()); boost::shared_ptr region = RegionFactory::region_by_id (rid); if (boost::dynamic_pointer_cast (region)) { //region->do_export ("/tmp/foo.flac"); context->drag_finish (true, false, time); } else if (boost::dynamic_pointer_cast (region)) { //region->do_export ("/tmp/foo.mid"); context->drag_finish (true, false, time); } context->drag_finish (true, false, time); } /* **************************************************************************** * Dir Listing */ static bool audio_midi_suffix (const std::string& str) { if (AudioFileSource::safe_audio_file_extension (str)) { return true; } if (SMFSource::safe_midi_file_extension (str)) { return true; } return false; } void TriggerClipPicker::open_dir () { Gtk::Window* tlw = dynamic_cast (get_toplevel ()); assert (tlw); _fcd.set_transient_for (*tlw); int result = _fcd.run (); _fcd.hide (); switch (result) { case RESPONSE_OK: list_dir (_fcd.get_filename ()); break; case RESPONSE_ACCEPT: if (Glib::file_test (_fcd.get_filename (), Glib::FILE_TEST_IS_DIR)) { size_t j = 0; std::string path; std::vector a = PBD::parse_path (Config->get_sample_lib_path ()); a.push_back (_fcd.get_filename ()); for (std::vector::const_iterator i = a.begin (); i != a.end (); ++i, ++j) { if (j > 0) path += G_SEARCHPATH_SEPARATOR; path += *i; } Config->set_sample_lib_path (path); list_dir (_fcd.get_filename ()); } break; default: break; } } void TriggerClipPicker::list_dir (std::string const& path, Gtk::TreeNodeChildren const* pc) { if (!Glib::file_test (path, Glib::FILE_TEST_IS_DIR)) { assert (0); return; } if (!pc) { _model->clear (); _dir.set_active (display_name (path)); } _current_path = path; std::vector dirs; std::vector files; try { Glib::Dir dir (path); for (Glib::DirIterator i = dir.begin (); i != dir.end (); ++i) { std::string fullpath = Glib::build_filename (path, *i); std::string basename = *i; if (basename.size () == 0 || basename[0] == '.') { continue; } if (Glib::file_test (fullpath, Glib::FILE_TEST_IS_DIR)) { dirs.push_back (*i); continue; } if (audio_midi_suffix (fullpath)) { files.push_back (*i); } } } catch (Glib::FileError const& err) { } std::sort (dirs.begin (), dirs.end ()); std::sort (files.begin (), files.end ()); if (!pc) { if (_root_paths.find (_current_path) == _root_paths.end ()) { TreeModel::Row row = *(_model->append ()); row[_columns.name] = ".."; row[_columns.path] = Glib::path_get_dirname (_current_path); row[_columns.read] = false; row[_columns.file] = false; } } for (auto& f : dirs) { TreeModel::Row row; if (pc) { row = *(_model->append (*pc)); } else { row = *(_model->append ()); } row[_columns.name] = f; row[_columns.path] = Glib::build_filename (path, f); row[_columns.read] = false; row[_columns.file] = false; /* add stub child */ row = *(_model->append (row.children ())); row[_columns.read] = false; } for (auto& f : files) { TreeModel::Row row; if (pc) { row = *(_model->append (*pc)); } else { row = *(_model->append ()); } row[_columns.name] = f; row[_columns.path] = Glib::build_filename (path, f); row[_columns.read] = false; row[_columns.file] = true; } } /* **************************************************************************** * Auditioner */ void TriggerClipPicker::set_session (Session* s) { SessionHandlePtr::set_session (s); _play_btn.set_sensitive (false); _stop_btn.set_sensitive (false); if (!_session) { _seek_slider.set_sensitive (false); _auditioner_connections.drop_connections (); } else { _auditioner_connections.drop_connections (); _session->AuditionActive.connect (_auditioner_connections, invalidator (*this), boost::bind (&TriggerClipPicker::audition_active, this, _1), gui_context ()); _session->the_auditioner ()->AuditionProgress.connect (_auditioner_connections, invalidator (*this), boost::bind (&TriggerClipPicker::audition_progress, this, _1, _2), gui_context ()); } } void TriggerClipPicker::autoplay_toggled () { UIConfiguration::instance ().set_autoplay_clips (_autoplay_btn.get_active ()); row_selected (); /* maybe cancel audition, update sensitivity */ } void TriggerClipPicker::stop_audition () { if (_session) { _session->cancel_audition (); } } void TriggerClipPicker::audition_active (bool active) { _play_btn.set_sensitive (!active && !_autoplay_btn.get_active ()); _stop_btn.set_sensitive (active); _seek_slider.set_sensitive (active); if (!active) { _seek_slider.set_value (0); _seeking = false; } } void TriggerClipPicker::audition_progress (ARDOUR::samplecnt_t pos, ARDOUR::samplecnt_t len) { if (!_seeking) { _seek_slider.set_value (1000.0 * pos / len); _seek_slider.set_sensitive (true); } } bool TriggerClipPicker::seek_button_press (GdkEventButton*) { _seeking = true; return false; } bool TriggerClipPicker::seek_button_release (GdkEventButton*) { _seeking = false; _session->the_auditioner ()->seek_to_percent (_seek_slider.get_value () / 10.0); _seek_slider.set_sensitive (false); return false; } void TriggerClipPicker::audition_selected () { if (_view.get_selection ()->count_selected_rows () < 1) { return; } TreeView::Selection::ListHandle_Path rows = _view.get_selection ()->get_selected_rows (); TreeIter i = _model->get_iter (*rows.begin ()); audition ((*i)[_columns.path]); } void TriggerClipPicker::audition (std::string const& path) { if (!_session) { return; } _session->cancel_audition (); if (!Glib::file_test (path, Glib::FILE_TEST_EXISTS)) { warning << string_compose (_("Could not read file: %1 (%2)."), path, strerror (errno)) << endmsg; return; } boost::shared_ptr r; if (SMFSource::valid_midi_file (path)) { boost::shared_ptr ms = boost::dynamic_pointer_cast (SourceFactory::createExternal (DataType::MIDI, *_session, path, 0, Source::Flag (0), false)); std::string rname = region_name_from_path (ms->path (), false); PropertyList plist; plist.add (ARDOUR::Properties::start, timepos_t (Temporal::Beats ())); plist.add (ARDOUR::Properties::length, ms->length ()); plist.add (ARDOUR::Properties::name, rname); plist.add (ARDOUR::Properties::layer, 0); r = boost::dynamic_pointer_cast (RegionFactory::create (boost::dynamic_pointer_cast (ms), plist, false)); assert (r); } else { SourceList srclist; boost::shared_ptr afs; bool old_sbp = AudioSource::get_build_peakfiles (); /* don't even think of building peakfiles for these files */ SoundFileInfo info; std::string error_msg; if (!AudioFileSource::get_soundfile_info (path, info, error_msg)) { error << string_compose (_("Cannot get info from audio file %1 (%2)"), path, error_msg) << endmsg; return; } AudioSource::set_build_peakfiles (false); for (uint16_t n = 0; n < info.channels; ++n) { try { afs = boost::dynamic_pointer_cast (SourceFactory::createExternal (DataType::AUDIO, *_session, path, n, Source::Flag (ARDOUR::AudioFileSource::NoPeakFile), false)); if (afs->sample_rate () != _session->nominal_sample_rate ()) { boost::shared_ptr sfs (new SrcFileSource (*_session, afs, ARDOUR::SrcGood)); srclist.push_back (sfs); } else { srclist.push_back (afs); } } catch (failed_constructor& err) { error << _("Could not access soundfile: ") << path << endmsg; AudioSource::set_build_peakfiles (old_sbp); return; } } AudioSource::set_build_peakfiles (old_sbp); if (srclist.empty ()) { return; } afs = boost::dynamic_pointer_cast (srclist[0]); std::string rname = region_name_from_path (afs->path (), false); PropertyList plist; plist.add (ARDOUR::Properties::start, timepos_t (0)); plist.add (ARDOUR::Properties::length, srclist[0]->length ()); plist.add (ARDOUR::Properties::name, rname); plist.add (ARDOUR::Properties::layer, 0); r = boost::dynamic_pointer_cast (RegionFactory::create (srclist, plist, false)); } r->set_position (timepos_t ()); _session->audition_region (r); }