From 3aadf2daf0c2e70445a8ca56d5e46bf161598dd1 Mon Sep 17 00:00:00 2001 From: Robin Gareus Date: Tue, 15 Aug 2023 17:20:02 +0200 Subject: [PATCH] Add Section Arranger to Editor sidebar --- gtk2_ardour/editor.cc | 6 + gtk2_ardour/editor.h | 3 + gtk2_ardour/editor_sections.cc | 337 +++++++++++++++++++++++++++++++++ gtk2_ardour/editor_sections.h | 97 ++++++++++ gtk2_ardour/wscript | 1 + 5 files changed, 444 insertions(+) create mode 100644 gtk2_ardour/editor_sections.cc create mode 100644 gtk2_ardour/editor_sections.h diff --git a/gtk2_ardour/editor.cc b/gtk2_ardour/editor.cc index 0de5683c41..3c67284a28 100644 --- a/gtk2_ardour/editor.cc +++ b/gtk2_ardour/editor.cc @@ -122,6 +122,7 @@ #include "editor_regions.h" #include "editor_route_groups.h" #include "editor_routes.h" +#include "editor_sections.h" #include "editor_snapshots.h" #include "editor_sources.h" #include "editor_summary.h" @@ -429,6 +430,7 @@ Editor::Editor () , _route_groups (0) , _routes (0) , _regions (0) + , _sections (0) , _snapshots (0) , _locations (0) , autoscroll_horizontal_allowed (false) @@ -682,6 +684,7 @@ Editor::Editor () _routes = new EditorRoutes (); _regions = new EditorRegions (this); _sources = new EditorSources (this); + _sections = new EditorSections (); _snapshots = new EditorSnapshots (); _locations = new EditorLocations (this); _properties_box = new SelectionPropertiesBox (); @@ -700,6 +703,7 @@ Editor::Editor () add_notebook_page (_("Sources"), _sources->widget ()); add_notebook_page (_("Regions"), _regions->widget ()); add_notebook_page (_("Clips"), _trigger_clip_picker); + add_notebook_page (_("Sections"), _sections->widget ()); add_notebook_page (_("Snapshots"), _snapshots->widget ()); add_notebook_page (_("Track & Bus Groups"), _route_groups->widget ()); add_notebook_page (_("Ranges & Marks"), _locations->widget ()); @@ -915,6 +919,7 @@ Editor::~Editor() delete _group_tabs; delete _regions; delete _snapshots; + delete _sections; delete _locations; delete _properties_box; delete selection; @@ -1346,6 +1351,7 @@ Editor::set_session (Session *t) _regions->set_session (_session); _sources->set_session (_session); _snapshots->set_session (_session); + _sections->set_session (_session); _routes->set_session (_session); _locations->set_session (_session); _properties_box->set_session (_session); diff --git a/gtk2_ardour/editor.h b/gtk2_ardour/editor.h index 2a26e47fdc..3685d838b0 100644 --- a/gtk2_ardour/editor.h +++ b/gtk2_ardour/editor.h @@ -126,6 +126,7 @@ class EditorCursor; class EditorGroupTabs; class EditorLocations; class EditorRegions; +class EditorSections; class EditorSources; class EditorRoutes; class EditorRouteGroups; @@ -1787,6 +1788,7 @@ private: friend class DragManager; friend class EditorRouteGroups; friend class EditorRegions; + friend class EditorSections; friend class EditorSources; /* non-public event handlers */ @@ -2129,6 +2131,7 @@ private: EditorRouteGroups* _route_groups; EditorRoutes* _routes; EditorRegions* _regions; + EditorSections* _sections; EditorSources* _sources; EditorSnapshots* _snapshots; EditorLocations* _locations; diff --git a/gtk2_ardour/editor_sections.cc b/gtk2_ardour/editor_sections.cc new file mode 100644 index 0000000000..d55fed0513 --- /dev/null +++ b/gtk2_ardour/editor_sections.cc @@ -0,0 +1,337 @@ +/* + * Copyright (C) 2023 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 "ardour/session.h" +#include "pbd/unwind.h" + +#include "ardour_ui.h" +#include "editor_sections.h" +#include "gui_thread.h" +#include "public_editor.h" +#include "utils.h" + +#include "pbd/i18n.h" + +using namespace std; +using namespace PBD; +using namespace Gtk; +using namespace ARDOUR; + +EditorSections::EditorSections () + : _ignore_redisplay (false) +{ + _model = ListStore::create (_columns); + _view.set_model (_model); + _view.append_column (_("Name"), _columns.name); + _view.set_headers_visible (true); + _view.get_selection ()->set_mode (Gtk::SELECTION_SINGLE); + + _scroller.add (_view); + _scroller.set_policy (Gtk::POLICY_NEVER, Gtk::POLICY_AUTOMATIC); + + _view.signal_key_release_event ().connect (sigc::mem_fun (*this, &EditorSections::key_release), false); + _view.get_selection ()->signal_changed ().connect (sigc::mem_fun (*this, &EditorSections::selection_changed)); + + /* DnD source */ + std::vector dnd; + dnd.push_back (TargetEntry ("x-ardour/section", Gtk::TARGET_SAME_APP)); + _view.drag_source_set (dnd, Gdk::MODIFIER_MASK, Gdk::ACTION_COPY | Gdk::ACTION_MOVE); + _view.signal_drag_data_get ().connect (sigc::mem_fun (*this, &EditorSections::drag_data_get)); + + /* DnD target */ + _view.drag_dest_set (dnd, DEST_DEFAULT_ALL, Gdk::ACTION_COPY | Gdk::ACTION_MOVE); + _view.signal_drag_begin ().connect (sigc::mem_fun (*this, &EditorSections::drag_begin)); + _view.signal_drag_motion ().connect (sigc::mem_fun (*this, &EditorSections::drag_motion)); + _view.signal_drag_leave ().connect (sigc::mem_fun (*this, &EditorSections::drag_leave)); + _view.signal_drag_data_received ().connect (sigc::mem_fun (*this, &EditorSections::drag_data_received)); +} + +void +EditorSections::set_session (Session* s) +{ + SessionHandlePtr::set_session (s); + + if (_session) { + _session->locations ()->added.connect (_session_connections, invalidator (*this), boost::bind (&EditorSections::redisplay, this), gui_context ()); + _session->locations ()->removed.connect (_session_connections, invalidator (*this), boost::bind (&EditorSections::redisplay, this), gui_context ()); + _session->locations ()->changed.connect (_session_connections, invalidator (*this), boost::bind (&EditorSections::redisplay, this), gui_context ()); + + Location::start_changed.connect (_session_connections, invalidator (*this), boost::bind (&EditorSections::redisplay, this), gui_context ()); + Location::end_changed.connect (_session_connections, invalidator (*this), boost::bind (&EditorSections::redisplay, this), gui_context ()); + Location::flags_changed.connect (_session_connections, invalidator (*this), boost::bind (&EditorSections::redisplay, this), gui_context ()); + Location::name_changed.connect (_session_connections, invalidator (*this), boost::bind (&EditorSections::redisplay, this), gui_context ()); + } + + redisplay (); +} + +void +EditorSections::redisplay () +{ + if (_ignore_redisplay) { + return; + } + _view.set_model (Glib::RefPtr ()); + _model->clear (); + + if (_session == 0) { + return; + } + + timepos_t start; + timepos_t end; + + Locations* loc = _session->locations (); + Location* l = NULL; + + do { + l = loc->next_section (l, start, end); + if (l) { + TreeModel::Row newrow = *(_model->append ()); + newrow[_columns.name] = l->name (); + newrow[_columns.location] = l; + newrow[_columns.start] = start; + newrow[_columns.end] = end; + } + } while (l); + + _view.set_model (_model); +} + +bool +EditorSections::scroll_row_timeout () +{ + int y; + Gdk::Rectangle visible_rect; + Gtk::Adjustment* adj = _scroller.get_vadjustment (); + + gdk_window_get_pointer (_view.get_window ()->gobj(), NULL, &y, NULL); + _view.get_visible_rect (visible_rect); + + y += adj->get_value (); + + int offset = y - (visible_rect.get_y () + 30); + if (offset > 0) { + offset = y - (visible_rect.get_y() + visible_rect.get_height() - 30); + if (offset < 0) { + return true; + } + } + + float value = adj->get_value () + offset; + value = std::max (0, value); + value = std::min (value, adj->get_upper () - adj->get_page_size ()); + adj->set_value (value); + + return true; +} + +void +EditorSections::selection_changed () +{ + TreeView::Selection::ListHandle_Path rows = _view.get_selection ()->get_selected_rows (); + if (rows.empty ()) { + return; + } + Gtk::TreeModel::Row row = *_model->get_iter (*rows.begin ()); + + timepos_t start = row[_columns.start]; + timepos_t end = row[_columns.end]; + + Selection& s (PublicEditor::instance ().get_selection ()); + s.set (start, end); +} + +void +EditorSections::drag_begin (Glib::RefPtr const& context) +{ + TreeView::Selection::ListHandle_Path rows = _view.get_selection ()->get_selected_rows (); + if (!rows.empty ()) { + Glib::RefPtr 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 +EditorSections::drag_data_get (Glib::RefPtr const&, Gtk::SelectionData& data, guint, guint) +{ + if (data.get_target () != "x-ardour/section") { + return; + } + + data.set (data.get_target (), 8, NULL, 0); + TreeView::Selection::ListHandle_Path rows = _view.get_selection ()->get_selected_rows (); + for (auto const& r : rows) { + TreeIter i; + if ((i = _model->get_iter (r))) { + Section s ((*i)[_columns.location], (*i)[_columns.start], (*i)[_columns.end]); + data.set (data.get_target (), sizeof (Section), (guchar const*)&s, sizeof (Section)); + break; + } + } +} + +bool +EditorSections::drag_motion (Glib::RefPtr const& context, int x, int y, guint time) +{ + std::string const& target = _view.drag_dest_find_target (context, _view.drag_dest_get_target_list ()); + + if (target != "x-ardour/section") { + context->drag_status (Gdk::DragAction (0), time); + return false; + } + + int unused, header_height; + _view.convert_bin_window_to_widget_coords (0, 0, unused, header_height); + + if (y < header_height) { + context->drag_status (Gdk::DragAction (0), time); + return false; + } + + TreeModel::Path path; + TreeViewDropPosition pos; + + if (!_view.get_dest_row_at_pos (x, y, path, pos)) { + assert (_model->children ().size () > 0); + pos = Gtk::TREE_VIEW_DROP_AFTER; + path = TreeModel::Path (); + path.push_back (_model->children ().size () - 1); + } + + context->drag_status (context->get_suggested_action (), time); + + _view.set_drag_dest_row (path, pos); + _view.drag_highlight (); + + if (!_scroll_timeout.connected ()) { + _scroll_timeout = Glib::signal_timeout().connect (sigc::mem_fun (*this, &EditorSections::scroll_row_timeout), 150); + } + + return true; +} + +void +EditorSections::drag_leave (Glib::RefPtr const&, guint) +{ + _view.drag_unhighlight (); + _scroll_timeout.disconnect (); +} + +void +EditorSections::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/section") { + return; + } + if (data.get_length () != sizeof (Section)) { + return; + } + + SectionOperation op = CopyPasteSection; + timepos_t to (0); + + if ((context->get_suggested_action () == Gdk::ACTION_MOVE)) { + op = CutPasteSection; + } + + TreeModel::Path path; + TreeViewDropPosition pos; + + if (!_view.get_dest_row_at_pos (x, y, path, pos)) { + /* paste at end */ + TreeModel::Children rows = _model->children (); + assert (!rows.empty ()); + Gtk::TreeModel::Row row = *rows.rbegin (); + to = row[_columns.end]; +#ifndef NDEBUG + cout << "EditorSections::drag_data_received - paste at end\n"; +#endif + } else { + Gtk::TreeModel::iterator i = _model->get_iter (path); + assert (i); + if (pos == Gtk::TREE_VIEW_DROP_AFTER) { +#ifndef NDEBUG + Location* loc = (*i)[_columns.location]; + cout << "EditorSections::drag_data_received - paste after '" << loc->name () << "'\n"; +#endif + to = (*i)[_columns.end]; + } else { +#ifndef NDEBUG + Location* loc = (*i)[_columns.location]; + cout << "EditorSections::drag_data_received - paste before '" << loc->name () << "'\n"; +#endif + to = (*i)[_columns.start]; + } + } + + /* Section is POD, memcpy is fine. + * data is free()ed by ~Gtk::SelectionData */ + Section s; + memcpy (&s, data.get_data (), sizeof (Section)); + + if (op == CutPasteSection && to > s.start) { + /* offset/ripple `to` when using CutPasteSection */ + to = to.earlier (s.start.distance (s.end)); + } + +#ifndef NDEBUG + cout << "cut copy '" << s.location->name () << "' " << s.start << " - " << s.end << " to " << to << "\n"; +#endif + { + PBD::Unwinder uw (_ignore_redisplay, true); + _session->cut_copy_section (s.start, s.end, to, op); + } + redisplay (); +} + +bool +EditorSections::key_release (GdkEventKey* ev) +{ + if (_view.get_selection ()->count_selected_rows () != 1) { + return false; + } + + switch (ev->keyval) { + case GDK_KP_Delete: + /* fallthrough */ + case GDK_Delete: + /* fallthrough */ + case GDK_BackSpace: + break; + default: + return false; + } + + TreeView::Selection::ListHandle_Path rows = _view.get_selection ()->get_selected_rows (); + Gtk::TreeModel::Row row = *_model->get_iter (*rows.begin ()); + + timepos_t start = row[_columns.start]; + timepos_t end = row[_columns.end]; + { + PBD::Unwinder uw (_ignore_redisplay, true); + _session->cut_copy_section (start, end, timepos_t (0), DeleteSection); + } + redisplay (); + return true; +} diff --git a/gtk2_ardour/editor_sections.h b/gtk2_ardour/editor_sections.h new file mode 100644 index 0000000000..4260928571 --- /dev/null +++ b/gtk2_ardour/editor_sections.h @@ -0,0 +1,97 @@ +/* + * Copyright (C) 2023 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. + */ + +#ifndef _gtk_ardour_editor_sections_h_ +#define _gtk_ardour_editor_sections_h_ + +#include "ardour/location.h" +#include "ardour/session_handle.h" + +#include +#include +#include +#include + +class EditorSections : public ARDOUR::SessionHandlePtr, public virtual sigc::trackable +{ +public: + EditorSections (); + + void set_session (ARDOUR::Session*); + + Gtk::Widget& widget () + { + return _scroller; + } + + void redisplay (); + +private: + void drag_data_get (Glib::RefPtr const&, Gtk::SelectionData&, guint, guint); + void drag_begin (Glib::RefPtr const&); + bool drag_motion (Glib::RefPtr const&, int, int, guint); + void drag_data_received (Glib::RefPtr const&, int, int, Gtk::SelectionData const&, guint, guint); + void drag_leave (Glib::RefPtr const&, guint); + bool key_release (GdkEventKey*); + void selection_changed (); + bool scroll_row_timeout (); + + struct Section { + Section () + : location (NULL) + , start (0) + , end (0) + { + } + + Section (ARDOUR::Location const* const l, Temporal::timepos_t const& s, Temporal::timepos_t const& e) + : location (l) + , start (s) + , end (e) + { + } + + ARDOUR::Location const* const location; + Temporal::timepos_t const start; + Temporal::timepos_t const end; + }; + + struct Columns : public Gtk::TreeModel::ColumnRecord { + Columns () + { + add (name); + add (location); + add (start); + add (end); + } + Gtk::TreeModelColumn name; + Gtk::TreeModelColumn location; + Gtk::TreeModelColumn start; + Gtk::TreeModelColumn end; + }; + + Columns _columns; + Glib::RefPtr _model; + Gtk::TreeView _view; + Gtk::ScrolledWindow _scroller; + + bool _ignore_redisplay; + sigc::connection _scroll_timeout; +}; + +#endif diff --git a/gtk2_ardour/wscript b/gtk2_ardour/wscript index 1055c37eaf..cc3cf45594 100644 --- a/gtk2_ardour/wscript +++ b/gtk2_ardour/wscript @@ -105,6 +105,7 @@ gtk2_ardour_sources = [ 'editor_regions.cc', 'editor_routes.cc', 'editor_rulers.cc', + 'editor_sections.cc', 'editor_selection.cc', 'editor_snapshots.cc', 'editor_sources.cc',