13
0

Support cut/copy/paste of several regions and lines at once.

The idea here is to do the reasonable thing, and copy objects of some
type (e.g. MIDI region, gain line) to tracks with a matching type.  The user
can override this with a track selection, which will be used straight-up.

Lost: ability to copy/paste lines across types, e.g. gain to pan.  This is
often questionable, but sometimes useful, so we will need to implement some
sort of "greedy mode" to make it possible.  Implementation simple, but not sure
what to do.  Perhaps this should only be possible if one automation track is
explicitly (i.e. via track selection) involved, and the types are at least
compatible-ish?
This commit is contained in:
David Robillard 2014-11-16 17:04:27 -05:00
parent 5393982c80
commit 2fa6caad95
15 changed files with 289 additions and 115 deletions

View File

@ -192,6 +192,34 @@ AutomationRegionView::add_automation_event (GdkEvent *, framepos_t when, double
view->session()->set_dirty ();
}
bool
AutomationRegionView::paste (framepos_t pos,
unsigned paste_count,
float times,
boost::shared_ptr<const ARDOUR::AutomationList> slist)
{
AutomationTimeAxisView* const view = automation_view();
boost::shared_ptr<ARDOUR::AutomationList> my_list = _line->the_list();
if (view->session()->transport_rolling() && my_list->automation_write()) {
/* do not paste if this control is in write mode and we're rolling */
return false;
}
/* add multi-paste offset if applicable */
pos += view->editor().get_paste_offset(
pos, paste_count, _line->time_converter().to(slist->length()));
const double model_pos = _line->time_converter().from(pos - _line->time_converter().origin_b());
XMLNode& before = my_list->get_state();
my_list->paste(*slist, model_pos, times);
view->session()->add_command(
new MementoCommand<ARDOUR::AutomationList>(*my_list.get(), &before, &my_list->get_state()));
return true;
}
void
AutomationRegionView::set_height (double h)
{

View File

@ -49,6 +49,11 @@ public:
void init (bool wfd);
bool paste (framepos_t pos,
unsigned paste_count,
float times,
boost::shared_ptr<const ARDOUR::AutomationList> slist);
inline AutomationTimeAxisView* automation_view() const
{ return dynamic_cast<AutomationTimeAxisView*>(&trackview); }

View File

@ -22,10 +22,23 @@
#include <list>
namespace ARDOUR {
class AutomationList;
}
#include "ardour/automation_list.h"
#include "evoral/Parameter.hpp"
class AutomationSelection : public std::list<boost::shared_ptr<ARDOUR::AutomationList> > {};
class AutomationSelection : public std::list<boost::shared_ptr<ARDOUR::AutomationList> > {
public:
const_iterator
get_nth(const Evoral::Parameter& param, size_t nth) const {
size_t count = 0;
for (const_iterator l = begin(); l != end(); ++l) {
if ((*l)->parameter() == param) {
if (count++ == nth) {
return l;
}
}
}
return end();
}
};
#endif /* __ardour_gtk_automation_selection_h__ */

View File

@ -16,8 +16,9 @@
Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
*/
#include <cmath>
#include <cassert>
#include <cmath>
#include <list>
#include <utility>
#include <gtkmm.h>
@ -314,16 +315,16 @@ struct RegionPositionSorter {
};
/** @param pos Position, in session frames.
* @return AutomationLine to paste to for that position, or 0 if there is none appropriate.
*/
boost::shared_ptr<AutomationLine>
AutomationStreamView::paste_line (framepos_t pos)
bool
AutomationStreamView::paste (framepos_t pos,
unsigned paste_count,
float times,
boost::shared_ptr<ARDOUR::AutomationList> alist)
{
/* XXX: not sure how best to pick this; for now, just use the last region which starts before pos */
if (region_views.empty()) {
return boost::shared_ptr<AutomationLine> ();
return false;
}
region_views.sort (RegionPositionSorter ());
@ -345,7 +346,5 @@ AutomationStreamView::paste_line (framepos_t pos)
}
AutomationRegionView* arv = dynamic_cast<AutomationRegionView*> (*prev);
assert (arv);
return arv->line ();
return arv ? arv->paste(pos, paste_count, times, alist) : false;
}

View File

@ -64,7 +64,11 @@ class AutomationStreamView : public StreamView
void set_selected_points (PointSelection &);
std::list<boost::shared_ptr<AutomationLine> > get_lines () const;
boost::shared_ptr<AutomationLine> paste_line (ARDOUR::framepos_t);
bool paste (framepos_t pos,
unsigned paste_count,
float times,
boost::shared_ptr<ARDOUR::AutomationList> list);
private:
void setup_rec_box ();

View File

@ -48,6 +48,7 @@
#include "point_selection.h"
#include "control_point.h"
#include "utils.h"
#include "item_counts.h"
#include "i18n.h"
@ -630,51 +631,43 @@ AutomationTimeAxisView::add_automation_event (GdkEvent* event, framepos_t when,
_session->set_dirty ();
}
/** Paste a selection.
* @param pos Position to paste to (session frames).
* @param times Number of times to paste.
* @param selection Selection to paste.
* @param nth Index of the AutomationList within the selection to paste from.
*/
bool
AutomationTimeAxisView::paste (framepos_t pos, unsigned paste_count, float times, Selection& selection, size_t nth)
AutomationTimeAxisView::paste (framepos_t pos, unsigned paste_count, float times, const Selection& selection, ItemCounts& counts)
{
boost::shared_ptr<AutomationLine> line;
if (_line) {
line = _line;
return paste_one (pos, paste_count, times, selection, counts);
} else if (_view) {
line = _view->paste_line (pos);
AutomationSelection::const_iterator l = selection.lines.get_nth(_parameter, counts.n_lines(_parameter));
if (l != selection.lines.end() && _view->paste (pos, paste_count, times, *l)) {
counts.increase_n_lines(_parameter);
return true;
}
}
if (!line) {
return false;
}
return paste_one (*line, pos, paste_count, times, selection, nth);
return false;
}
bool
AutomationTimeAxisView::paste_one (AutomationLine& line, framepos_t pos, unsigned paste_count, float times, Selection& selection, size_t nth)
AutomationTimeAxisView::paste_one (framepos_t pos, unsigned paste_count, float times, const Selection& selection, ItemCounts& counts)
{
AutomationSelection::iterator p;
boost::shared_ptr<AutomationList> alist(line.the_list());
boost::shared_ptr<AutomationList> alist(_line->the_list());
if (_session->transport_rolling() && alist->automation_write()) {
/* do not paste if this control is in write mode and we're rolling */
return false;
}
for (p = selection.lines.begin(); p != selection.lines.end() && nth; ++p, --nth) {}
/* Get appropriate list from selection. */
AutomationSelection::const_iterator p = selection.lines.get_nth(_parameter, counts.n_lines(_parameter));
if (p == selection.lines.end()) {
return false;
}
counts.increase_n_lines(_parameter);
/* add multi-paste offset if applicable */
pos += _editor.get_paste_offset(pos, paste_count, (*p)->length());
double const model_pos = line.time_converter().from (pos - line.time_converter().origin_b ());
double const model_pos = _line->time_converter().from (pos - _line->time_converter().origin_b ());
XMLNode &before = alist->get_state();
alist->paste (**p, model_pos, times);

View File

@ -51,7 +51,7 @@ class Selection;
class Selectable;
class AutomationStreamView;
class AutomationController;
class ItemCounts;
class AutomationTimeAxisView : public TimeAxisView {
public:
@ -93,7 +93,7 @@ class AutomationTimeAxisView : public TimeAxisView {
/* editing operations */
void cut_copy_clear (Selection&, Editing::CutCopyOp);
bool paste (ARDOUR::framepos_t, unsigned paste_count, float times, Selection&, size_t nth);
bool paste (ARDOUR::framepos_t, unsigned paste_count, float times, const Selection&, ItemCounts&);
int set_state (const XMLNode&, int version);
@ -171,7 +171,7 @@ class AutomationTimeAxisView : public TimeAxisView {
void build_display_menu ();
void cut_copy_clear_one (AutomationLine&, Selection&, Editing::CutCopyOp);
bool paste_one (AutomationLine&, ARDOUR::framepos_t, unsigned, float times, Selection&, size_t nth);
bool paste_one (ARDOUR::framepos_t, unsigned, float times, const Selection&, ItemCounts& counts);
void route_going_away ();
void set_automation_state (ARDOUR::AutoState);

View File

@ -3887,16 +3887,10 @@ Editor::get_paste_offset (framepos_t pos, unsigned paste_count, framecnt_t durat
/* calculate basic unsnapped multi-paste offset */
framecnt_t offset = paste_count * duration;
bool success = true;
double snap_beats = get_grid_type_as_beats(success, pos);
if (success) {
/* we're snapped to something musical, round duration up */
BeatsFramesConverter conv(_session->tempo_map(), pos);
const Evoral::MusicalTime dur_beats = conv.from(duration);
const framecnt_t snap_dur_beats = ceil(dur_beats / snap_beats) * snap_beats;
offset = paste_count * conv.to(snap_dur_beats);
}
/* snap offset so pos + offset is aligned to the grid */
framepos_t offset_pos = pos + offset;
snap_to(offset_pos, RoundUpMaybe);
offset = offset_pos - pos;
return offset;
}

View File

@ -24,6 +24,7 @@
#include <cstdlib>
#include <cmath>
#include <string>
#include <limits>
#include <map>
#include <set>
@ -63,6 +64,7 @@
#include "audio_region_view.h"
#include "audio_streamview.h"
#include "audio_time_axis.h"
#include "automation_region_view.h"
#include "automation_time_axis.h"
#include "control_point.h"
#include "debug.h"
@ -75,6 +77,7 @@
#include "gui_thread.h"
#include "insert_time_dialog.h"
#include "interthread_progress_window.h"
#include "item_counts.h"
#include "keyboard.h"
#include "midi_region_view.h"
#include "mixer_strip.h"
@ -3843,26 +3846,8 @@ Editor::cut_copy (CutCopyOp op)
bool did_edit = false;
if (!selection->points.empty()) {
begin_reversible_command (opname + _(" points"));
did_edit = true;
cut_copy_points (op);
if (op == Cut || op == Delete) {
selection->clear_points ();
}
} else if (!selection->regions.empty() || !selection->points.empty()) {
string thing_name;
if (selection->regions.empty()) {
thing_name = _("points");
} else if (selection->points.empty()) {
thing_name = _("regions");
} else {
thing_name = _("objects");
}
begin_reversible_command (opname + ' ' + thing_name);
if (!selection->regions.empty() || !selection->points.empty()) {
begin_reversible_command (opname + ' ' + _("objects"));
did_edit = true;
if (!selection->regions.empty()) {
@ -3889,7 +3874,7 @@ Editor::cut_copy (CutCopyOp op)
selection->set (start, end);
}
} else if (!selection->time.empty()) {
begin_reversible_command (opname + _(" range"));
begin_reversible_command (opname + ' ' + _("range"));
did_edit = true;
cut_copy_ranges (op);
@ -3912,10 +3897,11 @@ Editor::cut_copy (CutCopyOp op)
}
struct AutomationRecord {
AutomationRecord () : state (0) {}
AutomationRecord (XMLNode* s) : state (s) {}
AutomationRecord () : state (0) , line(NULL) {}
AutomationRecord (XMLNode* s, const AutomationLine* l) : state (s) , line (l) {}
XMLNode* state; ///< state before any operation
const AutomationLine* line; ///< line this came from
boost::shared_ptr<Evoral::ControlList> copy; ///< copied events for the cut buffer
};
@ -3938,12 +3924,13 @@ Editor::cut_copy_points (CutCopyOp op)
/* Go through all selected points, making an AutomationRecord for each distinct AutomationList */
for (PointSelection::iterator i = selection->points.begin(); i != selection->points.end(); ++i) {
boost::shared_ptr<AutomationList> al = (*i)->line().the_list();
const AutomationLine& line = (*i)->line();
const boost::shared_ptr<AutomationList> al = line.the_list();
if (lists.find (al) == lists.end ()) {
/* We haven't seen this list yet, so make a record for it. This includes
taking a copy of its current state, in case this is needed for undo later.
*/
lists[al] = AutomationRecord (&al->get_state ());
lists[al] = AutomationRecord (&al->get_state (), &line);
}
}
@ -3951,8 +3938,12 @@ Editor::cut_copy_points (CutCopyOp op)
/* This operation will involve putting things in the cut buffer, so create an empty
ControlList for each of our source lists to put the cut buffer data in.
*/
framepos_t start = std::numeric_limits<framepos_t>::max();
for (Lists::iterator i = lists.begin(); i != lists.end(); ++i) {
i->second.copy = i->first->create (i->first->parameter ());
/* Calculate earliest start position of any point in selection. */
start = std::min(start, i->second.line->session_position(i->first->begin()));
}
/* Add all selected points to the relevant copy ControlLists */
@ -3962,11 +3953,18 @@ Editor::cut_copy_points (CutCopyOp op)
lists[al].copy->fast_simple_add ((*j)->when, (*j)->value);
}
/* Snap start time backwards, so copy/paste is snap aligned. */
snap_to(start, RoundDownMaybe);
for (Lists::iterator i = lists.begin(); i != lists.end(); ++i) {
/* Correct this copy list so that it starts at time 0 */
double const start = i->second.copy->front()->when;
/* Correct this copy list so that it is relative to the earliest
start time, so relative ordering between points is preserved
when copying from several lists. */
const AutomationLine* line = i->second.line;
const double line_offset = line->time_converter().from(start);
for (AutomationList::iterator j = i->second.copy->begin(); j != i->second.copy->end(); ++j) {
(*j)->when -= start;
(*j)->when -= line_offset;
}
/* And add it to the cut buffer */
@ -4352,14 +4350,8 @@ Editor::paste_internal (framepos_t position, float times)
{
DEBUG_TRACE (DEBUG::CutNPaste, string_compose ("apparent paste position is %1\n", position));
if (internal_editing()) {
if (cut_buffer->midi_notes.empty()) {
return;
}
} else {
if (cut_buffer->empty()) {
return;
}
if (cut_buffer->empty(internal_editing())) {
return;
}
if (position == max_framepos) {
@ -4376,27 +4368,64 @@ Editor::paste_internal (framepos_t position, float times)
last_paste_pos = position;
}
TrackViewList ts;
TrackViewList::iterator i;
size_t nth;
/* get everything in the correct order */
if (_edit_point == Editing::EditAtMouse && entered_track) {
/* With the mouse edit point, paste onto the track under the mouse */
ts.push_back (entered_track);
} else if (_edit_point == Editing::EditAtMouse && entered_regionview) {
/* With the mouse edit point, paste onto the track of the region under the mouse */
ts.push_back (&entered_regionview->get_time_axis_view());
} else if (!selection->tracks.empty()) {
/* Otherwise, if there are some selected tracks, paste to them */
TrackViewList ts;
if (!selection->tracks.empty()) {
/* If there is a track selection, paste into exactly those tracks and
only those tracks. This allows the user to be explicit and override
the below "do the reasonable thing" logic. */
ts = selection->tracks.filter_to_unique_playlists ();
sort_track_selection (ts);
} else if (_last_cut_copy_source_track) {
/* Otherwise paste to the track that the cut/copy came from;
see discussion in mantis #3333.
*/
ts.push_back (_last_cut_copy_source_track);
} else {
/* Figure out which track to base the paste at. */
TimeAxisView* base_track;
if (_edit_point == Editing::EditAtMouse && entered_track) {
/* With the mouse edit point, paste onto the track under the mouse. */
base_track = entered_track;
} else if (_edit_point == Editing::EditAtMouse && entered_regionview) {
/* With the mouse edit point, paste onto the track of the region under the mouse. */
base_track = &entered_regionview->get_time_axis_view();
} else if (_last_cut_copy_source_track) {
/* Paste to the track that the cut/copy came from (see mantis #333). */
base_track = _last_cut_copy_source_track;
}
/* Walk up to parent if necessary, so base track is a route. */
while (base_track->get_parent()) {
base_track = base_track->get_parent();
}
/* Add base track and all tracks below it. The paste logic will select
the appropriate object types from the cut buffer in relative order. */
for (TrackViewList::iterator i = track_views.begin(); i != track_views.end(); ++i) {
if ((*i)->order() >= base_track->order()) {
ts.push_back(*i);
}
}
/* Sort tracks so the nth track of type T will pick the nth object of type T. */
sort_track_selection (ts);
/* Add automation children of each track in order, for pasting several lines. */
for (TrackViewList::iterator i = ts.begin(); i != ts.end();) {
/* Add any automation children for pasting several lines */
RouteTimeAxisView* rtv = dynamic_cast<RouteTimeAxisView*>(*i++);
if (!rtv) {
continue;
}
typedef RouteTimeAxisView::AutomationTracks ATracks;
const ATracks& atracks = rtv->automation_tracks();
for (ATracks::const_iterator a = atracks.begin(); a != atracks.end(); ++a) {
i = ts.insert(i, a->second.get());
++i;
}
}
/* We now have a list of trackviews starting at base_track, including
automation children, in the order shown in the editor, e.g. R1,
R1.A1, R1.A2, R2, R2.A1, ... */
}
if (internal_editing ()) {
@ -4424,8 +4453,9 @@ Editor::paste_internal (framepos_t position, float times)
begin_reversible_command (Operations::paste);
for (nth = 0, i = ts.begin(); i != ts.end(); ++i, ++nth) {
(*i)->paste (position, paste_count, times, *cut_buffer, nth);
ItemCounts counts;
for (TrackViewList::iterator i = ts.begin(); i != ts.end(); ++i) {
(*i)->paste (position, paste_count, times, *cut_buffer, counts);
}
commit_reversible_command ();

78
gtk2_ardour/item_counts.h Normal file
View File

@ -0,0 +1,78 @@
/*
Copyright (C) 2014 Paul Davis
Author: David Robillard
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., 675 Mass Ave, Cambridge, MA 02139, USA.
*/
#ifndef __ardour_item_counts_h__
#define __ardour_item_counts_h__
#include <cstddef>
#include <map>
#include <utility>
#include "ardour/data_type.h"
#include "evoral/Parameter.hpp"
/** A count of various GUI items.
*
* This is used to keep track of 'consumption' of a selection when pasting, but
* may be useful elsewhere.
*/
class ItemCounts
{
public:
size_t n_playlists(ARDOUR::DataType t) const { return get_n(t, _playlists); }
size_t n_regions(ARDOUR::DataType t) const { return get_n(t, _regions); }
size_t n_lines(Evoral::Parameter t) const { return get_n(t, _lines); }
void increase_n_playlists(ARDOUR::DataType t, size_t delta=1) {
increase_n(t, _playlists, delta);
}
void increase_n_regions(ARDOUR::DataType t, size_t delta=1) {
increase_n(t, _regions, delta);
}
void increase_n_lines(Evoral::Parameter t, size_t delta=1) {
increase_n(t, _lines, delta);
}
private:
template<typename Key>
size_t
get_n(const Key& key, const typename std::map<Key, size_t>& counts) const {
typename std::map<Key, size_t>::const_iterator i = counts.find(key);
return (i == counts.end()) ? 0 : i->second;
}
template<typename Key>
void
increase_n(const Key& key, typename std::map<Key, size_t>& counts, size_t delta) {
typename std::map<Key, size_t>::iterator i = counts.find(key);
if (i != counts.end()) {
i->second += delta;
} else {
counts.insert(std::make_pair(key, delta));
}
}
std::map<ARDOUR::DataType, size_t> _playlists;
std::map<ARDOUR::DataType, size_t> _regions;
std::map<Evoral::Parameter, size_t> _lines;
};
#endif /* __ardour_item_counts_h__ */

View File

@ -27,6 +27,20 @@ namespace ARDOUR {
class Playlist;
}
struct PlaylistSelection : std::list<boost::shared_ptr<ARDOUR::Playlist> > {};
struct PlaylistSelection : std::list<boost::shared_ptr<ARDOUR::Playlist> > {
public:
const_iterator
get_nth(ARDOUR::DataType type, size_t nth) const {
size_t count = 0;
for (const_iterator l = begin(); l != end(); ++l) {
if ((*l)->data_type() == type) {
if (count++ == nth) {
return l;
}
}
}
return end();
}
};
#endif /* __ardour_gtk_playlist_selection_h__ */

View File

@ -63,6 +63,7 @@
#include "automation_time_axis.h"
#include "enums.h"
#include "gui_thread.h"
#include "item_counts.h"
#include "keyboard.h"
#include "playlist_selector.h"
#include "point_selection.h"
@ -1534,20 +1535,20 @@ RouteTimeAxisView::cut_copy_clear (Selection& selection, CutCopyOp op)
}
bool
RouteTimeAxisView::paste (framepos_t pos, unsigned paste_count, float times, Selection& selection, size_t nth)
RouteTimeAxisView::paste (framepos_t pos, unsigned paste_count, float times, const Selection& selection, ItemCounts& counts)
{
if (!is_track()) {
return false;
}
boost::shared_ptr<Playlist> pl = playlist ();
PlaylistSelection::iterator p;
for (p = selection.playlists.begin(); p != selection.playlists.end() && nth; ++p, --nth) {}
boost::shared_ptr<Playlist> pl = playlist ();
const ARDOUR::DataType type = pl->data_type();
PlaylistSelection::const_iterator p = selection.playlists.get_nth(type, counts.n_playlists(type));
if (p == selection.playlists.end()) {
return false;
}
counts.increase_n_playlists(type);
DEBUG_TRACE (DEBUG::CutNPaste, string_compose ("paste to %1\n", pos));

View File

@ -70,6 +70,7 @@ class AutomationLine;
class ProcessorAutomationLine;
class TimeSelection;
class RouteGroupMenu;
class ItemCounts;
class RouteTimeAxisView : public RouteUI, public TimeAxisView
{
@ -99,7 +100,7 @@ public:
/* Editing operations */
void cut_copy_clear (Selection&, Editing::CutCopyOp);
bool paste (ARDOUR::framepos_t, unsigned paste_count, float times, Selection&, size_t nth);
bool paste (ARDOUR::framepos_t, unsigned paste_count, float times, const Selection&, ItemCounts&);
RegionView* combine_regions ();
void uncombine_regions ();
void uncombine_region (RegionView*);
@ -125,7 +126,7 @@ public:
virtual void create_automation_child (const Evoral::Parameter& param, bool show) = 0;
typedef std::map<Evoral::Parameter, boost::shared_ptr<AutomationTimeAxisView> > AutomationTracks;
AutomationTracks automation_tracks() { return _automation_tracks; }
const AutomationTracks& automation_tracks() const { return _automation_tracks; }
boost::shared_ptr<AutomationTimeAxisView> automation_child(Evoral::Parameter param);
virtual Gtk::CheckMenuItem* automation_child_menu_item (Evoral::Parameter);

View File

@ -951,7 +951,7 @@ Selection::empty (bool internal_selection)
as a cut buffer.
*/
return object_level_empty && midi_notes.empty();
return object_level_empty && midi_notes.empty() && points.empty();
}
void

View File

@ -78,6 +78,7 @@ class RegionView;
class GhostRegion;
class StreamView;
class ArdourDialog;
class ItemCounts;
/** Abstract base class for time-axis views (horizontal editor 'strips')
*
@ -165,7 +166,20 @@ class TimeAxisView : public virtual AxisView
/* editing operations */
virtual void cut_copy_clear (Selection&, Editing::CutCopyOp) {}
virtual bool paste (ARDOUR::framepos_t, unsigned /*paste_count*/, float /*times*/, Selection&, size_t /*nth*/) { return false; }
/** Paste a selection.
* @param pos Position to paste to (session frames).
* @param paste_count Number of pastes to the same location previously (multi-paste).
* @param times Number of times to paste.
* @param selection Selection to paste.
* @param counts Count of consumed selection items (used to find the
* correct item to paste here, then updated for the next pastee).
*/
virtual bool paste (ARDOUR::framepos_t pos,
unsigned paste_count,
float times,
const Selection& selection,
ItemCounts& counts) { return false; }
virtual void set_selected_regionviews (RegionSelection&) {}
virtual void set_selected_points (PointSelection&) {}