Implement "multi-paste" for notes, regions, and automation.

The idea here is that pasting several times to the same location doesn't make
sense.  Instead, the paste is appended past the last paste, snapped to the
grid.  This make it simple to replicate a given section a number of times,
simply by copying once and pasting several times.

This behaviour only appears when successive pastes are done to the same
location (whatever the edit point is).  When the paste point changes, the
"multi-paste" state is reset.

Boots 'n cats 'n boots 'n cats.
This commit is contained in:
David Robillard 2014-11-14 20:04:09 -05:00
parent b01d1813f8
commit 31acd96384
11 changed files with 80 additions and 17 deletions

View File

@ -637,7 +637,7 @@ AutomationTimeAxisView::add_automation_event (GdkEvent* event, framepos_t when,
* @param nth Index of the AutomationList within the selection to paste from.
*/
bool
AutomationTimeAxisView::paste (framepos_t pos, float times, Selection& selection, size_t nth)
AutomationTimeAxisView::paste (framepos_t pos, unsigned paste_count, float times, Selection& selection, size_t nth)
{
boost::shared_ptr<AutomationLine> line;
@ -651,11 +651,11 @@ AutomationTimeAxisView::paste (framepos_t pos, float times, Selection& selection
return false;
}
return paste_one (*line, pos, times, selection, nth);
return paste_one (*line, pos, paste_count, times, selection, nth);
}
bool
AutomationTimeAxisView::paste_one (AutomationLine& line, framepos_t pos, float times, Selection& selection, size_t nth)
AutomationTimeAxisView::paste_one (AutomationLine& line, framepos_t pos, unsigned paste_count, float times, Selection& selection, size_t nth)
{
AutomationSelection::iterator p;
boost::shared_ptr<AutomationList> alist(line.the_list());
@ -671,6 +671,9 @@ AutomationTimeAxisView::paste_one (AutomationLine& line, framepos_t pos, float t
return false;
}
/* 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 ());
XMLNode &before = alist->get_state();

View File

@ -93,7 +93,7 @@ class AutomationTimeAxisView : public TimeAxisView {
/* editing operations */
void cut_copy_clear (Selection&, Editing::CutCopyOp);
bool paste (ARDOUR::framepos_t, float times, Selection&, size_t nth);
bool paste (ARDOUR::framepos_t, unsigned paste_count, float times, Selection&, size_t nth);
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, float times, Selection&, size_t nth);
bool paste_one (AutomationLine&, ARDOUR::framepos_t, unsigned, float times, Selection&, size_t nth);
void route_going_away ();
void set_automation_state (ARDOUR::AutoState);

View File

@ -312,6 +312,8 @@ Editor::Editor ()
clicked_control_point = 0;
last_update_frame = 0;
pre_press_cursor = 0;
last_paste_pos = 0;
paste_count = 0;
_drags = new DragManager (this);
lock_dialog = 0;
ruler_dialog = 0;
@ -3856,6 +3858,31 @@ Editor::playlist_selector () const
return *_playlist_selector;
}
framecnt_t
Editor::get_paste_offset (framepos_t pos, unsigned paste_count, framecnt_t duration)
{
if (paste_count == 0) {
/* don't bother calculating an offset that will be zero anyway */
return 0;
}
/* 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);
}
return offset;
}
Evoral::MusicalTime
Editor::get_grid_type_as_beats (bool& success, framepos_t position)
{

View File

@ -313,6 +313,7 @@ class Editor : public PublicEditor, public PBD::ScopedConnectionList, public ARD
/* nudge is initiated by transport controls owned by ARDOUR_UI */
framecnt_t get_nudge_distance (framepos_t pos, framecnt_t& next);
framecnt_t get_paste_offset (framepos_t pos, unsigned paste_count, framecnt_t duration);
Evoral::MusicalTime get_grid_type_as_beats (bool& success, framepos_t position);
void nudge_forward (bool next, bool force_playhead);
@ -1109,6 +1110,11 @@ class Editor : public PublicEditor, public PBD::ScopedConnectionList, public ARD
Gtkmm2ext::ActionMap editor_action_map;
Gtkmm2ext::Bindings key_bindings;
/* CUT/COPY/PASTE */
framepos_t last_paste_pos;
unsigned paste_count;
void cut_copy (Editing::CutCopyOp);
bool can_cut_copy () const;
void cut_copy_points (Editing::CutCopyOp);

View File

@ -4364,6 +4364,15 @@ Editor::paste_internal (framepos_t position, float times)
DEBUG_TRACE (DEBUG::CutNPaste, string_compose ("preferred edit position is %1\n", position));
}
if (position == last_paste_pos) {
/* repeated paste in the same position */
++paste_count;
} else {
/* paste in new location, reset repeated paste state */
paste_count = 0;
last_paste_pos = position;
}
TrackViewList ts;
TrackViewList::iterator i;
size_t nth;
@ -4401,7 +4410,7 @@ Editor::paste_internal (framepos_t position, float times)
cb != cut_buffer->midi_notes.end() && r != rs.end(); ++r) {
MidiRegionView* mrv = dynamic_cast<MidiRegionView*> (*r);
if (mrv) {
mrv->paste (position, times, **cb);
mrv->paste (position, paste_count, times, **cb);
++cb;
}
}
@ -4413,7 +4422,7 @@ 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, times, *cut_buffer, nth);
(*i)->paste (position, paste_count, times, *cut_buffer, nth);
}
commit_reversible_command ();

View File

@ -3321,25 +3321,37 @@ MidiRegionView::selection_as_cut_buffer () const
/** This method handles undo */
void
MidiRegionView::paste (framepos_t pos, float times, const MidiCutBuffer& mcb)
MidiRegionView::paste (framepos_t pos, unsigned paste_count, float times, const MidiCutBuffer& mcb)
{
if (mcb.empty()) {
return;
}
PublicEditor& editor = trackview.editor ();
trackview.session()->begin_reversible_command (_("paste"));
start_note_diff_command (_("paste"));
const Evoral::MusicalTime pos_beats = absolute_frames_to_source_beats (pos);
const Evoral::MusicalTime first_time = (*mcb.notes().begin())->time();
const Evoral::MusicalTime last_time = (*mcb.notes().rbegin())->end_time();
Evoral::MusicalTime end_point = 0;
/* get snap duration, default to 1 beat if not snapped to anything musical */
bool success = true;
double snap_beats = editor.get_grid_type_as_beats(success, pos);
if (!success) {
snap_beats = 1.0;
}
const Evoral::MusicalTime first_time = (*mcb.notes().begin())->time();
const Evoral::MusicalTime last_time = (*mcb.notes().rbegin())->end_time();
const Evoral::MusicalTime duration = last_time - first_time;
const Evoral::MusicalTime snap_duration = ceil(duration / snap_beats) * snap_beats;
const Evoral::MusicalTime paste_offset = paste_count * snap_duration;
const Evoral::MusicalTime pos_beats = absolute_frames_to_source_beats(pos) + paste_offset;
Evoral::MusicalTime end_point = 0;
DEBUG_TRACE (DEBUG::CutNPaste, string_compose ("Paste data spans from %1 to %2 (%3) ; paste pos beats = %4 (based on %5 - %6)\n",
first_time,
last_time,
last_time - first_time, pos, _region->position(),
duration, pos, _region->position(),
pos_beats));
clear_selection ();

View File

@ -112,7 +112,7 @@ public:
void resolve_note(uint8_t note_num, double end_time);
void cut_copy_clear (Editing::CutCopyOp);
void paste (framepos_t pos, float times, const MidiCutBuffer&);
void paste (framepos_t pos, unsigned paste_count, float times, const MidiCutBuffer&);
void add_canvas_patch_change (ARDOUR::MidiModel::PatchChangePtr patch, const std::string& displaytext, bool);

View File

@ -299,6 +299,7 @@ class PublicEditor : public Gtk::Window, public PBD::StatefulDestructible, publi
virtual void foreach_time_axis_view (sigc::slot<void,TimeAxisView&>) = 0;
virtual void add_to_idle_resize (TimeAxisView*, int32_t) = 0;
virtual framecnt_t get_nudge_distance (framepos_t pos, framecnt_t& next) = 0;
virtual framecnt_t get_paste_offset (framepos_t pos, unsigned paste_count, framecnt_t duration) = 0;
virtual Evoral::MusicalTime get_grid_type_as_beats (bool& success, framepos_t position) = 0;
virtual void edit_notes (TimeAxisViewItem&) = 0;

View File

@ -1534,7 +1534,7 @@ RouteTimeAxisView::cut_copy_clear (Selection& selection, CutCopyOp op)
}
bool
RouteTimeAxisView::paste (framepos_t pos, float times, Selection& selection, size_t nth)
RouteTimeAxisView::paste (framepos_t pos, unsigned paste_count, float times, Selection& selection, size_t nth)
{
if (!is_track()) {
return false;
@ -1556,6 +1556,11 @@ RouteTimeAxisView::paste (framepos_t pos, float times, Selection& selection, siz
DEBUG_TRACE (DEBUG::CutNPaste, string_compose ("modified paste to %1\n", pos));
}
/* add multi-paste offset if applicable */
std::pair<framepos_t, framepos_t> extent = (*p)->get_extent();
const framecnt_t duration = extent.second - extent.first;
pos += _editor.get_paste_offset(pos, paste_count, duration);
pl->clear_changes ();
if (Config->get_edit_mode() == Ripple) {
std::pair<framepos_t, framepos_t> extent = (*p)->get_extent_with_endspace();

View File

@ -99,7 +99,7 @@ public:
/* Editing operations */
void cut_copy_clear (Selection&, Editing::CutCopyOp);
bool paste (ARDOUR::framepos_t, float times, Selection&, size_t nth);
bool paste (ARDOUR::framepos_t, unsigned paste_count, float times, Selection&, size_t nth);
RegionView* combine_regions ();
void uncombine_regions ();
void uncombine_region (RegionView*);

View File

@ -165,7 +165,7 @@ class TimeAxisView : public virtual AxisView
/* editing operations */
virtual void cut_copy_clear (Selection&, Editing::CutCopyOp) {}
virtual bool paste (ARDOUR::framepos_t, float /*times*/, Selection&, size_t /*nth*/) { return false; }
virtual bool paste (ARDOUR::framepos_t, unsigned /*paste_count*/, float /*times*/, Selection&, size_t /*nth*/) { return false; }
virtual void set_selected_regionviews (RegionSelection&) {}
virtual void set_selected_points (PointSelection&) {}