Robin Gareus
566596e383
On macOS, `context->get_targets ()` may be empty when dragging. In this case the Clip-Picker assumed that a slot is about to be dropped and switched to the local clip library. This in turn cleared selection and it was no lnger possible to drag any clip out of the library.
1039 lines
31 KiB
C++
1039 lines
31 KiB
C++
/*
|
|
* Copyright (C) 2019-2021 Robin Gareus <robin@gareus.org>
|
|
*
|
|
* 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 <cassert>
|
|
#include <vector>
|
|
|
|
#include <gtkmm/stock.h>
|
|
|
|
#include "pbd/basename.h"
|
|
#include "pbd/file_utils.h"
|
|
#include "pbd/openuri.h"
|
|
#include "pbd/pathexpand.h"
|
|
#include "pbd/search_path.h"
|
|
#include "pbd/unwind.h"
|
|
|
|
#include "ardour/audiofilesource.h"
|
|
#include "ardour/audioregion.h"
|
|
#include "ardour/auditioner.h"
|
|
#include "ardour/clip_library.h"
|
|
#include "ardour/directory_names.h"
|
|
#include "ardour/filesystem_paths.h"
|
|
#include "ardour/midi_region.h"
|
|
#include "ardour/plugin_insert.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 "widgets/tooltips.h"
|
|
#include "widgets/ardour_icon.h"
|
|
|
|
#include "ardour_ui.h"
|
|
#include "plugin_ui.h"
|
|
#include "timers.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"))
|
|
, _auditioner_combo (InstrumentSelector::ForAuditioner)
|
|
, _clip_library_listed (false)
|
|
, _ignore_list_dir (false)
|
|
, _seeking (false)
|
|
, _audition_plugnui (0)
|
|
{
|
|
/* 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));
|
|
|
|
_open_library_btn.set_name ("generic button");
|
|
_open_library_btn.set_icon (ArdourWidgets::ArdourIcon::Folder);
|
|
_open_library_btn.signal_clicked.connect (sigc::mem_fun (*this, &TriggerClipPicker::open_library));
|
|
_open_library_btn.set_no_show_all ();
|
|
|
|
_show_plugin_btn.set_name ("generic button");
|
|
_show_plugin_btn.set_icon (ArdourWidgets::ArdourIcon::PsetBrowse);
|
|
_show_plugin_btn.signal_clicked.connect (sigc::mem_fun (*this, &TriggerClipPicker::audition_show_plugin_ui));
|
|
_show_plugin_btn.set_sensitive (false);
|
|
|
|
_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));
|
|
|
|
auditioner_combo_changed();
|
|
_auditioner_combo.signal_changed().connect(sigc::mem_fun(*this, &TriggerClipPicker::auditioner_combo_changed) );
|
|
|
|
ArdourWidgets::set_tooltip (_play_btn, _("Audition selected clip"));
|
|
ArdourWidgets::set_tooltip (_stop_btn, _("Stop the audition"));
|
|
ArdourWidgets::set_tooltip (_open_library_btn, _("Open clip library folder"));
|
|
ArdourWidgets::set_tooltip (_auditioner_combo, _("Select the Synth used for auditioning"));
|
|
ArdourWidgets::set_tooltip (_show_plugin_btn, _("Show the GUI for the Auditioner Synth"));
|
|
ArdourWidgets::set_tooltip (_clip_dir_menu, _("Click to select a clip folder and edit your available clip folders"));
|
|
|
|
format_text.set_alignment(Gtk::ALIGN_LEFT, Gtk::ALIGN_CENTER);
|
|
channels_value.set_alignment(Gtk::ALIGN_LEFT, Gtk::ALIGN_CENTER);
|
|
_midi_prop_table.attach (format_text, 0, 1, 0, 1, EXPAND | FILL, SHRINK);
|
|
_midi_prop_table.attach (channels_value, 0, 1, 1, 2, EXPAND | FILL, SHRINK);
|
|
_midi_prop_table.attach (_auditioner_combo, 0, 3, 2, 3, EXPAND | FILL, SHRINK);
|
|
_midi_prop_table.attach (_show_plugin_btn, 3, 4, 2, 3, SHRINK, SHRINK);
|
|
_midi_prop_table.set_border_width (4);
|
|
_midi_prop_table.set_spacings (4);
|
|
|
|
/* Layout */
|
|
int r = 0;
|
|
_auditable.set_homogeneous(false);
|
|
_auditable.attach (_play_btn, 0, 1, r,r+1, SHRINK, SHRINK);
|
|
_auditable.attach (_stop_btn, 1, 2, r,r+1, SHRINK, SHRINK);
|
|
_auditable.attach (_autoplay_btn, 2, 3, r,r+1, EXPAND | FILL, SHRINK); r++;
|
|
_auditable.attach (_seek_slider, 0, 4, r,r+1, EXPAND | FILL, SHRINK); r++;
|
|
_auditable.attach (_midi_prop_table, 0, 4, r,r+1, EXPAND | FILL, SHRINK);
|
|
_auditable.set_border_width (4);
|
|
_auditable.set_spacings (4);
|
|
|
|
_scroller.set_policy (POLICY_AUTOMATIC, POLICY_AUTOMATIC);
|
|
_scroller.add (_view);
|
|
|
|
Gtk::Table *dir_table = manage(new Gtk::Table());
|
|
dir_table->set_border_width(4);
|
|
dir_table->set_spacings(4);
|
|
dir_table->attach (_clip_dir_menu, 0, 1, 0, 1, EXPAND | FILL, SHRINK);
|
|
dir_table->attach (_open_library_btn, 1, 2, 0, 1, SHRINK, SHRINK);
|
|
|
|
pack_start (*dir_table, false, false);
|
|
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<TargetEntry> 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<Gtk::TargetEntry> target_table;
|
|
target_table.push_back (Gtk::TargetEntry ("x-ardour/region.pbdid", Gtk::TARGET_SAME_APP));
|
|
target_table.push_back (TargetEntry ("text/uri-list"));
|
|
_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 ());
|
|
LibraryClipAdded.connect (_clip_added_connection, invalidator (*this), boost::bind (&TriggerClipPicker::clip_added, this, _1, _2), gui_context ());
|
|
|
|
/* cache value */
|
|
_clip_library_dir = clip_library_dir ();
|
|
|
|
/* show off */
|
|
_scroller.show ();
|
|
_view.show ();
|
|
_clip_dir_menu.show ();
|
|
_auditable.show ();
|
|
|
|
/* fill treeview with data */
|
|
_clip_dir_menu.items ().front ().activate ();
|
|
}
|
|
|
|
TriggerClipPicker::~TriggerClipPicker ()
|
|
{
|
|
_idle_connection.disconnect ();
|
|
}
|
|
|
|
void
|
|
TriggerClipPicker::auditioner_combo_changed()
|
|
{
|
|
if (_session) {
|
|
_session->the_auditioner()->set_audition_synth_info( _auditioner_combo.selected_instrument() );
|
|
}
|
|
}
|
|
|
|
|
|
void
|
|
TriggerClipPicker::parameter_changed (std::string const& p)
|
|
{
|
|
if (p == "sample-lib-path") {
|
|
refill_dropdown ();
|
|
} else if (p == "clip-library-dir") {
|
|
_clip_library_dir = clip_library_dir ();
|
|
refill_dropdown ();
|
|
}
|
|
}
|
|
|
|
void
|
|
TriggerClipPicker::clip_added (std::string const&, void* src)
|
|
{
|
|
if (!_clip_library_listed) {
|
|
_clip_library_dir = clip_library_dir ();
|
|
refill_dropdown ();
|
|
}
|
|
if (src == this) {
|
|
list_dir (clip_library_dir ());
|
|
} else {
|
|
list_dir (_current_path);
|
|
}
|
|
}
|
|
|
|
/* ****************************************************************************
|
|
* Paths Dropdown Callbacks
|
|
*/
|
|
|
|
void
|
|
TriggerClipPicker::edit_path ()
|
|
{
|
|
Gtk::Window* tlw = dynamic_cast<Gtk::Window*> (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 ()
|
|
{
|
|
_clip_dir_menu.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 (_clip_dir_menu.items ().size () > 0);
|
|
if (!Config->get_sample_lib_path ().empty ()) {
|
|
_clip_dir_menu.AddMenuElem (Menu_Helpers::SeparatorElem ());
|
|
Searchpath cpath (Config->get_sample_lib_path ());
|
|
for (auto const& f : cpath) {
|
|
maybe_add_dir (f);
|
|
}
|
|
}
|
|
|
|
_clip_library_listed = maybe_add_dir (clip_library_dir (false));
|
|
|
|
_clip_dir_menu.AddMenuElem (Menu_Helpers::SeparatorElem ());
|
|
_clip_dir_menu.AddMenuElem (Menu_Helpers::MenuElem (_("Edit..."), sigc::mem_fun (*this, &TriggerClipPicker::edit_path)));
|
|
_clip_dir_menu.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);
|
|
}
|
|
|
|
bool
|
|
TriggerClipPicker::maybe_add_dir (std::string const& dir)
|
|
{
|
|
if (dir.empty () || !Glib::file_test (dir, Glib::FILE_TEST_IS_DIR | Glib::FILE_TEST_EXISTS)) {
|
|
return false;
|
|
}
|
|
|
|
_clip_dir_menu.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);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/* ****************************************************************************
|
|
* Treeview Callbacks
|
|
*/
|
|
|
|
void
|
|
TriggerClipPicker::drag_begin (Glib::RefPtr<Gdk::DragContext> 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<Gdk::Bitmap> (), 4, h / 2);
|
|
}
|
|
}
|
|
|
|
void
|
|
TriggerClipPicker::drag_end (Glib::RefPtr<Gdk::DragContext> 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) {
|
|
_play_btn.set_sensitive (false);
|
|
_midi_prop_table.hide();
|
|
} 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] && !_autoplay_btn.get_active ());
|
|
|
|
std::string path = (*i)[_columns.path];
|
|
if (SMFSource::valid_midi_file (path)) {
|
|
/* TODO: if it's a really big file, we could skip this check */
|
|
boost::shared_ptr<SMFSource> ms;
|
|
try {
|
|
ms = boost::dynamic_pointer_cast<SMFSource> (
|
|
SourceFactory::createExternal (DataType::MIDI, *_session,
|
|
path, 0, Source::Flag (0), false));
|
|
} catch (const std::exception& e) {
|
|
error << string_compose(_("Could not read file: %1 (%2)."),
|
|
path, e.what()) << endmsg;
|
|
}
|
|
|
|
if (ms) {
|
|
if (ms->smf_format()==0) {
|
|
format_text.set_text ("MIDI Type 0");
|
|
} else {
|
|
format_text.set_text (string_compose( _("%1 (%2 Tracks)"), ms->smf_format()==2 ? X_("MIDI Type 2") : X_("MIDI Type 1"), ms->num_tracks()));
|
|
}
|
|
channels_value.set_text (string_compose(
|
|
_("Channel(s) used: %1 - %2 "),
|
|
ARDOUR_UI_UTILS::midi_channels_as_string (ms->used_channels()),
|
|
ms->has_pgm_change() ? _("with pgms") : X_("")
|
|
));
|
|
|
|
_midi_prop_table.show();
|
|
}
|
|
} else {
|
|
_midi_prop_table.hide();
|
|
}
|
|
}
|
|
}
|
|
|
|
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<Gdk::DragContext> const&, SelectionData& data, guint, guint time)
|
|
{
|
|
if (data.get_target () != "text/uri-list") {
|
|
return;
|
|
}
|
|
std::vector<std::string> 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<Gdk::DragContext> const& context, int, int, guint time)
|
|
{
|
|
for (auto i : context->get_targets ()) {
|
|
if (i == "x-ardour/region.pbdid") {
|
|
/* prepare for export to local clip library */
|
|
if (!_clip_library_dir.empty () && _current_path != _clip_library_dir) {
|
|
list_dir (_clip_library_dir);
|
|
}
|
|
context->drag_status (Gdk::ACTION_COPY, time);
|
|
return true;
|
|
}
|
|
}
|
|
|
|
/* drag from clip-picker (to slots), or
|
|
* drag of an external folder to the clip-picker (add to sample_lib_path)
|
|
*/
|
|
context->drag_status (Gdk::ACTION_LINK, time);
|
|
return true;
|
|
}
|
|
|
|
void
|
|
TriggerClipPicker::drag_data_received (Glib::RefPtr<Gdk::DragContext> const& context, int /*x*/, int y, Gtk::SelectionData const& data, guint /*info*/, guint time)
|
|
{
|
|
if (data.get_target () == "x-ardour/region.pbdid") {
|
|
PBD::ID rid (data.get_data_as_string ());
|
|
boost::shared_ptr<Region> region = RegionFactory::region_by_id (rid);
|
|
if (export_to_clip_library (region, this)) {
|
|
context->drag_finish (true, false, time);
|
|
} else {
|
|
context->drag_finish (true, false, time);
|
|
}
|
|
} else {
|
|
bool changed = false;
|
|
std::string path;
|
|
std::string path_to_list;
|
|
std::vector<std::string> paths;
|
|
|
|
std::vector<std::string> a = PBD::parse_path (Config->get_sample_lib_path ());
|
|
if (ARDOUR_UI_UTILS::convert_drop_to_paths (paths, data)) {
|
|
for (std::vector<std::string>::const_iterator s = paths.begin (); s != paths.end (); ++s) {
|
|
if (Glib::file_test (*s, Glib::FILE_TEST_IS_DIR)) {
|
|
if (std::find (a.begin(), a.end(), *s) == a.end()) {
|
|
a.push_back (*s);
|
|
changed = true;
|
|
}
|
|
path_to_list = *s;
|
|
}
|
|
}
|
|
if (changed) {
|
|
size_t j = 0;
|
|
for (std::vector<std::string>::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);
|
|
}
|
|
if (!path_to_list.empty ()) {
|
|
list_dir (path_to_list);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/* ****************************************************************************
|
|
* 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<Gtk::Window*> (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<std::string> a = PBD::parse_path (Config->get_sample_lib_path ());
|
|
if (std::find (a.begin(), a.end(), _fcd.get_filename ()) != a.end()) {
|
|
list_dir (_fcd.get_filename ());
|
|
break;
|
|
}
|
|
a.push_back (_fcd.get_filename ());
|
|
for (std::vector<std::string>::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 (_ignore_list_dir) {
|
|
return;
|
|
}
|
|
/* do not recurse when calling _clip_dir_menu.set_active() */
|
|
PBD::Unwinder<bool> uw (_ignore_list_dir, true);
|
|
|
|
if (!Glib::file_test (path, Glib::FILE_TEST_IS_DIR)) {
|
|
assert (0);
|
|
return;
|
|
}
|
|
|
|
if (!pc) {
|
|
_view.set_model (Glib::RefPtr<Gtk::TreeStore>(0));
|
|
_model->clear ();
|
|
_clip_dir_menu.set_active (display_name (path));
|
|
}
|
|
|
|
_current_path = path;
|
|
|
|
if (_clip_library_dir == path) {
|
|
_open_library_btn.show ();
|
|
} else {
|
|
_open_library_btn.hide ();
|
|
}
|
|
|
|
std::vector<std::string> dirs;
|
|
std::vector<std::string> 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;
|
|
}
|
|
|
|
if (!pc) {
|
|
_view.set_model (_model);
|
|
}
|
|
}
|
|
|
|
void
|
|
TriggerClipPicker::open_library ()
|
|
{
|
|
PBD::open_folder (_clip_library_dir);
|
|
}
|
|
|
|
/* ****************************************************************************
|
|
* Auditioner
|
|
*/
|
|
|
|
void
|
|
TriggerClipPicker::set_session (Session* s)
|
|
{
|
|
SessionHandlePtr::set_session (s);
|
|
|
|
_play_btn.set_sensitive (false);
|
|
_stop_btn.set_sensitive (false);
|
|
_midi_prop_table.hide (); //only shown when a valid smf is chosen
|
|
|
|
if (!_session) {
|
|
_seek_slider.set_sensitive (false);
|
|
_auditioner_connections.drop_connections ();
|
|
_processor_connections.drop_connections ();
|
|
audition_processor_going_away ();
|
|
} 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 ());
|
|
_session->the_auditioner ()->processors_changed.connect (_auditioner_connections, invalidator (*this), boost::bind (&TriggerClipPicker::audition_processors_changed, this), gui_context ());
|
|
audition_processors_changed (); /* set sensitivity */
|
|
}
|
|
}
|
|
|
|
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<Region> r;
|
|
|
|
if (SMFSource::valid_midi_file (path)) {
|
|
boost::shared_ptr<SMFSource> ms = boost::dynamic_pointer_cast<SMFSource> (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<MidiRegion> (RegionFactory::create (boost::dynamic_pointer_cast<Source> (ms), plist, false));
|
|
assert (r);
|
|
|
|
} else {
|
|
SourceList srclist;
|
|
boost::shared_ptr<AudioFileSource> 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<AudioFileSource> (SourceFactory::createExternal (DataType::AUDIO, *_session, path, n, Source::Flag (ARDOUR::AudioFileSource::NoPeakFile), false));
|
|
if (afs->sample_rate () != _session->nominal_sample_rate ()) {
|
|
boost::shared_ptr<SrcFileSource> 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<AudioFileSource> (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<AudioRegion> (RegionFactory::create (srclist, plist, false));
|
|
}
|
|
|
|
r->set_position (timepos_t ());
|
|
|
|
_session->audition_region (r);
|
|
}
|
|
|
|
void
|
|
TriggerClipPicker::audition_processor_idle ()
|
|
{
|
|
if (!_session || _session->deletion_in_progress () || !_session->the_auditioner ()) {
|
|
return;
|
|
}
|
|
assert (_session && _session->the_auditioner ());
|
|
ARDOUR_UI::instance ()->get_process_buffers ();
|
|
_session->the_auditioner ()->idle_synth_update ();
|
|
ARDOUR_UI::instance ()->drop_process_buffers ();
|
|
}
|
|
|
|
bool
|
|
TriggerClipPicker::audition_processor_viz (bool show)
|
|
{
|
|
if (show) {
|
|
_idle_connection = Timers::fps_connect (sigc::mem_fun (*this, &TriggerClipPicker::audition_processor_idle));
|
|
} else {
|
|
_idle_connection.disconnect ();
|
|
}
|
|
return false;
|
|
}
|
|
|
|
void
|
|
TriggerClipPicker::audition_show_plugin_ui ()
|
|
{
|
|
if (!_audition_plugnui) {
|
|
boost::shared_ptr<PluginInsert> plugin_insert = boost::dynamic_pointer_cast<PluginInsert> (_session->the_auditioner ()->the_instrument ());
|
|
if (plugin_insert) {
|
|
_audition_plugnui = new PluginUIWindow (plugin_insert);
|
|
_audition_plugnui->set_session (_session);
|
|
_audition_plugnui->show_all ();
|
|
_audition_plugnui->set_title (/* generate_processor_title (plugin_insert)*/ _("Audition Synth"));
|
|
plugin_insert->DropReferences.connect (_processor_connections, invalidator (*this), boost::bind (&TriggerClipPicker::audition_processor_going_away, this), gui_context());
|
|
|
|
_audition_plugnui->signal_map_event ().connect (sigc::hide (sigc::bind (sigc::mem_fun (*this, &TriggerClipPicker::audition_processor_viz), true)));
|
|
_audition_plugnui->signal_unmap_event ().connect (sigc::hide (sigc::bind (sigc::mem_fun (*this, &TriggerClipPicker::audition_processor_viz), false)));
|
|
}
|
|
}
|
|
if (_audition_plugnui) {
|
|
_audition_plugnui->present ();
|
|
}
|
|
}
|
|
|
|
void
|
|
TriggerClipPicker::audition_processor_going_away ()
|
|
{
|
|
if (_audition_plugnui) {
|
|
_idle_connection.disconnect ();
|
|
delete _audition_plugnui;
|
|
}
|
|
_audition_plugnui = 0;
|
|
}
|
|
|
|
void
|
|
TriggerClipPicker::audition_processors_changed ()
|
|
{
|
|
if (!_session || _session->deletion_in_progress () || !_session->the_auditioner ()) {
|
|
_show_plugin_btn.set_sensitive (false);
|
|
set_tooltip (_show_plugin_btn, "You must first play one midi file to show the plugin's GUI");
|
|
return;
|
|
}
|
|
|
|
if (_session && _session->the_auditioner ()->get_audition_synth_info()) {
|
|
boost::shared_ptr<PluginInsert> plugin_insert = boost::dynamic_pointer_cast<PluginInsert> (_session->the_auditioner ()->the_instrument ());
|
|
if (plugin_insert) {
|
|
set_tooltip (_show_plugin_btn, "Show the selected audition-instrument's GUI");
|
|
_show_plugin_btn.set_sensitive (true);
|
|
}
|
|
}
|
|
}
|