Implement "multi-paste" for notes, regions, and automation.
authorDavid Robillard <d@drobilla.net>
Sat, 15 Nov 2014 01:04:09 +0000 (20:04 -0500)
committerDavid Robillard <d@drobilla.net>
Sat, 15 Nov 2014 01:04:19 +0000 (20:04 -0500)
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.

gtk2_ardour/automation_time_axis.cc
gtk2_ardour/automation_time_axis.h
gtk2_ardour/editor.cc
gtk2_ardour/editor.h
gtk2_ardour/editor_ops.cc
gtk2_ardour/midi_region_view.cc
gtk2_ardour/midi_region_view.h
gtk2_ardour/public_editor.h
gtk2_ardour/route_time_axis.cc
gtk2_ardour/route_time_axis.h
gtk2_ardour/time_axis_view.h

index 2c342fb8c9b9c6b085e936bfc0491b33208fa644..e0e9b9428f73e30f2eb3f95f90b8076dba380de2 100644 (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();
index a468c12459a7962d8197be5f4d040f72761719b7..39a211a456af0062bb84c38d86fb6e94ffe7077e 100644 (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);
index d5c9aa6fea239561d6667e4e83a8734fdbf9e35c..a0dcf72db25002436271d8b8cea9997018c1d116 100644 (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)
 {
index 4dc5f80d6f95651fc9bae2b147c3c056b404c0e7..2831fc894fb8cb47ed4062c9972c48177ce9915e 100644 (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);
index 98235e58610388659bff09f5341ca496e0a7bbaf..f6ec077301ff5585e0f8db52d3c5729e027817e4 100644 (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 ();
index b560367c5cb5a60d9128c2af3dff7a8a0aef9394..fc8948e734c30a2ae5e0939e33d1b8e50ba115f8 100644 (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 ();
index 65ca7df7abda498d91a90826e40af970aff5764c..cda4d5802c6b1fb6219560c60fe8a864a57f35b7 100644 (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);
 
index bd861ab085ef1d4aca9571954027b22c06e4a458..4bf03bc72f68a8de37303920d6b531b1d864ee8a 100644 (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;
 
index c4d49e40b94bbf9aae215e6d1de96f515e342f6b..b44728856632a96a8898751ac62de8cfe9f8a570 100644 (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();
index 8e0941d591606c469a15b2bbffe6adedcf2be15f..45b8afd82dc4930313fc5f78f48bb71fcf777dfe 100644 (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*);
index cc7f7a0fe0d689fc1900736af4cc5aa21d4689a7..3dc440b54c5211c9b1a7913a2a21172585ba94f2 100644 (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&) {}