/* Copyright (C) 2019 Nikolaus Gullotta 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 "ardour/filesystem_paths.h" #include "ardour/session_state_utils.h" #include "ardour/session_directory.h" #include #include #include #include #include #include #include "editor.h" #include "mixer_snapshot_dialog.h" #include "mixer_snapshot_substitution_dialog.h" #include "utils.h" #include "pbd/basename.h" #include "pbd/file_utils.h" #include "pbd/gstdio_compat.h" #include "pbd/i18n.h" #include "widgets/tooltips.h" #include "widgets/choice.h" #include "widgets/prompter.h" using namespace Gtk; using namespace PBD; using namespace std; using namespace ARDOUR; using namespace ArdourWidgets; using namespace ARDOUR_UI_UTILS; struct ColumnInfo { int index; int sort_idx; AlignmentEnum al; const char* label; const char* tooltip; }; MixerSnapshotDialog::MixerSnapshotDialog(Session* s) : ArdourWindow(_("Mixer Snapshot Manager:")) { global_model = ListStore::create(_columns); local_model = ListStore::create(_columns); bootstrap_display_and_model(global_display, global_scroller, global_model, true); bootstrap_display_and_model(local_display, local_scroller, local_model, false); //needs to happen after bootstrap add(top_level_view_box); //DnD stuff vector target_table; target_table.push_back(TargetEntry("string")); global_display.drag_dest_set(target_table); local_display.drag_dest_set(target_table); global_display.signal_drag_data_received().connect(sigc::bind(sigc::mem_fun(*this, &MixerSnapshotDialog::display_drag_data_received), true)); local_display.signal_drag_data_received().connect(sigc::bind(sigc::mem_fun(*this, &MixerSnapshotDialog::display_drag_data_received), false)); global_display.signal_button_press_event().connect(sigc::bind(sigc::mem_fun(*this, &MixerSnapshotDialog::button_press), true), false); local_display.signal_button_press_event().connect(sigc::bind(sigc::mem_fun(*this, &MixerSnapshotDialog::button_press), false), false); set_session(s); } void MixerSnapshotDialog::set_session(Session* s) { if(s) { ArdourWindow::set_session(s); global_snap_path = Glib::build_filename(user_config_directory(-1), "mixer_snapshots"); local_snap_path = Glib::build_filename(_session->session_directory().root_path(), "mixer_snapshots"); } refill(); } bool MixerSnapshotDialog::ensure_directory(bool global) { string path = global ? global_snap_path : local_snap_path; if(!Glib::file_test(path.c_str(), Glib::FILE_TEST_EXISTS & Glib::FILE_TEST_IS_DIR)) { ::g_mkdir(path.c_str(), 0775); return true; } return false; } void MixerSnapshotDialog::display_drag_data_received(const Glib::RefPtr& context, int x, int y, const SelectionData& data, guint info, guint time, bool global) { if (data.get_target() != "string") { context->drag_finish(false, false, time); return; } const void* d = data.get_data(); const Gtkmm2ext::DnDTreeView* tree_view = reinterpret_cast*>(d); bool ok = false; if(tree_view) { list paths; TreeView* source; tree_view->get_object_drag_data(paths, &source); if(!paths.empty()) { ensure_directory(global); } for (list::const_iterator i = paths.begin(); i != paths.end(); i++) { string new_path = Glib::build_filename(global ? global_snap_path : local_snap_path, basename((*i).c_str())); ::g_rename((*i).c_str(), new_path.c_str()); } ok = true; } context->drag_finish(ok, false, time); // ToDo: create/delete model rows instead of doing a heavy refill refill(); } bool MixerSnapshotDialog::button_press(GdkEventButton* ev, bool global) { if (ev->button == 3) { TreeViewColumn* col; TreeModel::Path path; int cx; int cy; TreeModel::iterator iter; if(global) { global_display.get_path_at_pos ((int) ev->x, (int) ev->y, path, col, cx, cy); iter = global_model->get_iter(path); } else { local_display.get_path_at_pos ((int) ev->x, (int) ev->y, path, col, cx, cy); iter = local_model->get_iter(path); } if (iter) { popup_context_menu(ev->button, ev->time, iter, global); return true; } }; if (ev->type == GDK_2BUTTON_PRESS) { TreeModel::iterator iter; if(global) { iter = global_display.get_selection()->get_selected(); } else { iter = local_display.get_selection()->get_selected(); } global_display.get_selection()->unselect_all(); local_display.get_selection()->unselect_all(); if(iter) { load_snapshot(iter); return true; } } return false; } void MixerSnapshotDialog::popup_context_menu(int btn, int64_t time, TreeModel::iterator& iter, bool global) { using namespace Menu_Helpers; MenuList& items(menu.items()); items.clear(); add_item_with_sensitivity(items, MenuElem(_("Recall"), sigc::bind(sigc::mem_fun(*this, &MixerSnapshotDialog::load_snapshot), iter)), true); add_item_with_sensitivity(items, MenuElem(_("Rename..."), sigc::bind(sigc::mem_fun(*this, &MixerSnapshotDialog::rename_snapshot), iter)), true); add_item_with_sensitivity(items, MenuElem(_("Remove"), sigc::bind(sigc::mem_fun(*this, &MixerSnapshotDialog::remove_snapshot), iter, global)), true); menu.popup(btn, time); } void MixerSnapshotDialog::load_snapshot(TreeModel::iterator& iter) { MixerSnapshot* snap = (*iter)[_columns.snapshot]; MixerSnapshotSubstitutionDialog* d = new MixerSnapshotSubstitutionDialog(snap); d->show_all(); // d->signal_response().connect(sigc::bind(sigc::mem_fun(*this, &MixerSnapshotDialog::sub_dialog_finished), d)); // snap->recall(); } void MixerSnapshotDialog::rename_snapshot(TreeModel::iterator& iter) { string old_path = (*iter)[_columns.full_path]; string dir_name = Glib::path_get_dirname(old_path); Prompter prompt(true); prompt.set_name("Rename MixerSnapshot Prompter"); prompt.set_title(_("New Snapshot Name:")); prompt.add_button(Stock::SAVE, RESPONSE_ACCEPT); prompt.set_prompt(_("Rename Mixer Snapshot:")); prompt.set_initial_text(basename_nosuffix(old_path)); if (prompt.run() == RESPONSE_ACCEPT) { string new_label; prompt.get_result(new_label); if (new_label.length() > 0) { string new_path = Glib::build_filename(dir_name, new_label + ".xml"); ::g_rename(old_path.c_str(), new_path.c_str()); if (new_label.length() > 45) { new_label = new_label.substr (0, 45); new_label.append("..."); } (*iter)[_columns.name] = new_label; (*iter)[_columns.full_path] = new_path; } } } void MixerSnapshotDialog::remove_snapshot(TreeModel::iterator& iter, bool global) { string path = (*iter)[_columns.full_path]; ::g_remove(path.c_str()); if(global) { global_model->erase(iter); } else { local_model->erase(iter); } } bool MixerSnapshotDialog::bootstrap_display_and_model(Gtkmm2ext::DnDTreeView& display, ScrolledWindow& scroller, Glib::RefPtr model, bool global) { if(!model) { return false; } display.set_model(model); display.append_column(_("Fav"), _columns.favorite); display.append_column(_("Name"), _columns.name); display.append_column(_("# Tracks"), _columns.n_tracks); display.append_column(_("# VCAs"), _columns.n_vcas); display.append_column(_("# Groups"), _columns.n_groups); display.append_column(_("Date"), _columns.date); display.append_column(_("Version"), _columns.version); //newest snaps should be at the top model->set_sort_column(6, SORT_DESCENDING); //flag setting columns /* dumb work around here because we're doing an #ifdef MIXBUS - Basically, append_column() returns the no. of columns *after* appending, we can just put this in a vector and use it later */ vector column_counts { #ifdef MIXBUS display.append_column(_("EQ"), _columns.recall_eq), display.append_column(_("Comp."), _columns.recall_comp), display.append_column(_("Sends"), _columns.recall_sends), #endif display.append_column(_("Pan"), _columns.recall_pan), display.append_column(_("Plugins"), _columns.recall_plugins), display.append_column(_("Groups"), _columns.recall_groups), display.append_column(_("VCAs"), _columns.recall_vcas), }; for(vector::iterator i = column_counts.begin(); i != column_counts.end(); i++) { int index = (*i) - 1; //the actual index at the time of appending CellRendererToggle* cell = dynamic_cast(display.get_column_cell_renderer(index)); string col_title = display.get_column(index)->get_title(); cell->property_activatable() = true; cell->property_radio() = true; cell->signal_toggled().connect(sigc::bind(sigc::mem_fun(*this, &MixerSnapshotDialog::recall_flag_cell_action), global, col_title)); } display.set_headers_visible(true); display.set_headers_clickable(true); display.set_reorderable(false); display.set_rules_hint(true); display.add_object_drag(_columns.full_path.index(), "string"); display.set_drag_column(_columns.name.index()); CellRendererToggle* fav_cell = dynamic_cast(display.get_column_cell_renderer(0)); fav_cell->property_activatable() = true; fav_cell->property_radio() = true; fav_cell->signal_toggled().connect(sigc::bind(sigc::mem_fun(*this, &MixerSnapshotDialog::fav_cell_action), global)); HBox* add_remove = manage(new HBox); Button* btn_add = manage(new Button("New")); Button* btn_new = manage(new Button("New From Session")); add_remove->pack_start(*btn_add, true, true, 50); add_remove->pack_start(*btn_new, true, true, 45); VBox* vbox = manage(new VBox); vbox->set_homogeneous(); vbox->pack_start(*add_remove); vbox->set_size_request(1000, -1); btn_add->signal_clicked().connect(sigc::bind(sigc::mem_fun(*this, &MixerSnapshotDialog::new_snapshot), global)); btn_new->signal_clicked().connect(sigc::bind(sigc::mem_fun(*this, &MixerSnapshotDialog::new_snapshot_from_session), global)); scroller.set_border_width(10); scroller.set_policy(POLICY_AUTOMATIC, POLICY_AUTOMATIC); scroller.add(display); Table* table = manage(new Table(3, 3)); table->set_size_request(-1, 400); table->attach(scroller, 0, 3, 0, 5 ); table->attach(*vbox, 2, 3, 6, 8, FILL|EXPAND, FILL, 5, 5); top_level_view_box.pack_start(*table); ColumnInfo ci[] = { { 0, 0, ALIGN_CENTER, _("Favorite"), _("") }, { 1, 1, ALIGN_LEFT, _("Name"), _("") }, { 2, 2, ALIGN_CENTER, _("# Tracks"), _("") }, { 3, 3, ALIGN_CENTER, _("# VCAs"), _("") }, { 4, 4, ALIGN_CENTER, _("# Groups"), _("") }, { 5, 8, ALIGN_LEFT, _("Date"), _("") }, { 6, 6, ALIGN_LEFT, _("Version"), _("") }, { -1,-1, ALIGN_CENTER, 0, 0 } }; for (int i = 0; ci[i].index >= 0; ++i) { ColumnInfo info = ci[i]; TreeViewColumn* col = display.get_column(info.index); Label* label = manage(new Label (info.label)); label->set_alignment(info.al); set_tooltip(*label, info.tooltip); col->set_widget(*label); label->show(); col->set_sort_column(info.sort_idx); col->set_expand(false); col->set_alignment(info.al); //...and this sets the alignment for the data cells CellRendererText* rend = dynamic_cast(display.get_column_cell_renderer(info.index)); if (rend) { rend->property_xalign() = (info.al == ALIGN_RIGHT ? 1.0 : (info.al == ALIGN_LEFT ? 0.0 : 0.5)); } } return true; } void MixerSnapshotDialog::new_row(Glib::RefPtr model, MixerSnapshot* snap, string path) { string name = basename_nosuffix(path); snap->set_label(name); TreeModel::Children rows = model->children(); for(TreeModel::iterator i = rows.begin(); i != rows.end(); i++) { string row_name = (*i)[_columns.name]; if(row_name == name) { model->erase((*i)); break; } } if (name.length() > 45) { name = name.substr (0, 45); name.append("..."); } TreeModel::Row row = *(model->append()); row[_columns.name] = name; row[_columns.favorite] = snap->get_favorite(); row[_columns.version] = snap->get_last_modified_with(); row[_columns.n_tracks] = snap->get_routes().size(); row[_columns.n_vcas] = snap->get_vcas().size(); row[_columns.n_groups] = snap->get_groups().size(); GStatBuf gsb; g_stat(path.c_str(), &gsb); Glib::DateTime gdt(Glib::DateTime::create_now_local(gsb.st_ctime)); row[_columns.timestamp] = gsb.st_ctime; row[_columns.date] = gdt.format ("%F %H:%M"); row[_columns.full_path] = path; row[_columns.snapshot] = snap; #ifdef MIXBUS row[_columns.recall_eq] = snap->get_recall_eq(); row[_columns.recall_comp] = snap->get_recall_comp(); row[_columns.recall_sends] = snap->get_recall_sends(); #endif row[_columns.recall_pan] = snap->get_recall_pan(); row[_columns.recall_plugins] = snap->get_recall_plugins(); row[_columns.recall_groups] = snap->get_recall_groups(); row[_columns.recall_vcas] = snap->get_recall_vcas(); } void MixerSnapshotDialog::new_snapshot(bool global) { if(!_session) { return; } MixerSnapshot* snap = new MixerSnapshot(_session); Prompter prompt(true); prompt.set_name("New Mixer Snapshot Prompter"); prompt.set_title(_("Mixer Snapshot Name:")); prompt.add_button(Stock::SAVE, RESPONSE_ACCEPT); prompt.set_prompt(_("Set Mixer Snapshot Name")); prompt.set_initial_text(_session->name()); RouteList rl = PublicEditor::instance().get_selection().tracks.routelist(); CheckButton* sel = new CheckButton(_("Selected Tracks Only:")); sel->set_active(!rl.empty()); sel->show(); prompt.get_vbox()->pack_start(*sel); if(prompt.run() == RESPONSE_ACCEPT) { ensure_directory(global); string new_label; prompt.get_result(new_label); if (new_label.length() > 0) { snap->set_label(new_label); string path = Glib::build_filename(global ? global_snap_path : local_snap_path, snap->get_label() + ".xml"); if(!rl.empty() && sel->get_active()) { snap->snap(rl); } else { snap->snap(); } snap->write(path); if(global && !snap->empty()) { new_row(global_model, snap, path); } else { new_row(local_model, snap, path); } } } } void MixerSnapshotDialog::new_snapshot_from_session(bool global) { FileChooserDialog session_selector(_("Open Session"), FILE_CHOOSER_ACTION_OPEN); string session_parent_dir = Glib::path_get_dirname(_session->path()); session_selector.add_button(Stock::CANCEL, RESPONSE_CANCEL); session_selector.add_button(Stock::OPEN, RESPONSE_ACCEPT); session_selector.set_current_folder(session_parent_dir); int response = session_selector.run(); session_selector.hide(); if (response != RESPONSE_ACCEPT) { return; } ensure_directory(global); string session_path = session_selector.get_filename(); MixerSnapshot* snap = new MixerSnapshot(_session, session_path); snap->set_label(basename_nosuffix(session_path)); string path = Glib::build_filename(global ? global_snap_path : local_snap_path, snap->get_label() + ".xml"); if(!snap->empty()) { snap->write(path); if(global) { new_row(global_model, snap, path); } else { new_row(local_model, snap, path); } } else { delete snap; } } void MixerSnapshotDialog::refill_display(bool global) { Glib::RefPtr model; if(global) { model = global_model; } else { model = local_model; } model->clear(); vector files; find_files_matching_pattern(files, global ? global_snap_path : local_snap_path, "*.xml"); if(files.empty()) { return; } for(vector::iterator i = files.begin(); i != files.end(); i++) { string path = *(i); MixerSnapshot* snap = new MixerSnapshot(_session, path); new_row(model, snap, path); } } void MixerSnapshotDialog::refill() { refill_display(true); refill_display(false); } void MixerSnapshotDialog::fav_cell_action(const string& path, bool global) { TreeModel::iterator iter; if(global) { iter = global_model->get_iter(path); } else { iter = local_model->get_iter(path); } if(iter) { MixerSnapshot* snap = (*iter)[_columns.snapshot]; snap->set_favorite(!snap->get_favorite()); (*iter)[_columns.favorite] = snap->get_favorite(); snap->write((*iter)[_columns.full_path]); } } void MixerSnapshotDialog::recall_flag_cell_action(const std::string& path, bool global, string title) { TreeModel::iterator iter; if(global) { iter = global_model->get_iter(path); } else { iter = local_model->get_iter(path); } if(iter) { MixerSnapshot* snap = (*iter)[_columns.snapshot]; #ifdef MIXBUS if(title == "EQ") { snap->set_recall_eq(!snap->get_recall_eq()); (*iter)[_columns.recall_eq] = snap->get_recall_eq(); } if(title == "Comp.") { snap->set_recall_comp(!snap->get_recall_comp()); (*iter)[_columns.recall_comp] = snap->get_recall_comp(); } if(title == "Sends") { snap->set_recall_sends(!snap->get_recall_sends()); (*iter)[_columns.recall_comp] = snap->get_recall_sends(); } #endif if(title == "Pan") { snap->set_recall_pan(!snap->get_recall_pan()); (*iter)[_columns.recall_pan] = snap->get_recall_pan(); } if(title == "Plugins") { snap->set_recall_plugins(!snap->get_recall_plugins()); (*iter)[_columns.recall_plugins] = snap->get_recall_plugins(); } if(title == "Groups") { snap->set_recall_groups(!snap->get_recall_groups()); (*iter)[_columns.recall_groups] = snap->get_recall_groups(); } if(title == "VCAs") { snap->set_recall_vcas(!snap->get_recall_vcas()); (*iter)[_columns.recall_vcas] = snap->get_recall_vcas(); } snap->write((*iter)[_columns.full_path]); } }