mirror of
https://github.com/Ardour/ardour.git
synced 2025-12-10 00:34:59 +01:00
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:
parent
b01d1813f8
commit
31acd96384
11 changed files with 80 additions and 17 deletions
|
|
@ -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.
|
* @param nth Index of the AutomationList within the selection to paste from.
|
||||||
*/
|
*/
|
||||||
bool
|
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;
|
boost::shared_ptr<AutomationLine> line;
|
||||||
|
|
||||||
|
|
@ -651,11 +651,11 @@ AutomationTimeAxisView::paste (framepos_t pos, float times, Selection& selection
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return paste_one (*line, pos, times, selection, nth);
|
return paste_one (*line, pos, paste_count, times, selection, nth);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool
|
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;
|
AutomationSelection::iterator p;
|
||||||
boost::shared_ptr<AutomationList> alist(line.the_list());
|
boost::shared_ptr<AutomationList> alist(line.the_list());
|
||||||
|
|
@ -671,6 +671,9 @@ AutomationTimeAxisView::paste_one (AutomationLine& line, framepos_t pos, float t
|
||||||
return false;
|
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 ());
|
double const model_pos = line.time_converter().from (pos - line.time_converter().origin_b ());
|
||||||
|
|
||||||
XMLNode &before = alist->get_state();
|
XMLNode &before = alist->get_state();
|
||||||
|
|
|
||||||
|
|
@ -93,7 +93,7 @@ class AutomationTimeAxisView : public TimeAxisView {
|
||||||
/* editing operations */
|
/* editing operations */
|
||||||
|
|
||||||
void cut_copy_clear (Selection&, Editing::CutCopyOp);
|
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);
|
int set_state (const XMLNode&, int version);
|
||||||
|
|
||||||
|
|
@ -171,7 +171,7 @@ class AutomationTimeAxisView : public TimeAxisView {
|
||||||
void build_display_menu ();
|
void build_display_menu ();
|
||||||
|
|
||||||
void cut_copy_clear_one (AutomationLine&, Selection&, Editing::CutCopyOp);
|
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 route_going_away ();
|
||||||
|
|
||||||
void set_automation_state (ARDOUR::AutoState);
|
void set_automation_state (ARDOUR::AutoState);
|
||||||
|
|
|
||||||
|
|
@ -312,6 +312,8 @@ Editor::Editor ()
|
||||||
clicked_control_point = 0;
|
clicked_control_point = 0;
|
||||||
last_update_frame = 0;
|
last_update_frame = 0;
|
||||||
pre_press_cursor = 0;
|
pre_press_cursor = 0;
|
||||||
|
last_paste_pos = 0;
|
||||||
|
paste_count = 0;
|
||||||
_drags = new DragManager (this);
|
_drags = new DragManager (this);
|
||||||
lock_dialog = 0;
|
lock_dialog = 0;
|
||||||
ruler_dialog = 0;
|
ruler_dialog = 0;
|
||||||
|
|
@ -3856,6 +3858,31 @@ Editor::playlist_selector () const
|
||||||
return *_playlist_selector;
|
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
|
Evoral::MusicalTime
|
||||||
Editor::get_grid_type_as_beats (bool& success, framepos_t position)
|
Editor::get_grid_type_as_beats (bool& success, framepos_t position)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -313,6 +313,7 @@ class Editor : public PublicEditor, public PBD::ScopedConnectionList, public ARD
|
||||||
/* nudge is initiated by transport controls owned by ARDOUR_UI */
|
/* nudge is initiated by transport controls owned by ARDOUR_UI */
|
||||||
|
|
||||||
framecnt_t get_nudge_distance (framepos_t pos, framecnt_t& next);
|
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);
|
Evoral::MusicalTime get_grid_type_as_beats (bool& success, framepos_t position);
|
||||||
|
|
||||||
void nudge_forward (bool next, bool force_playhead);
|
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::ActionMap editor_action_map;
|
||||||
Gtkmm2ext::Bindings key_bindings;
|
Gtkmm2ext::Bindings key_bindings;
|
||||||
|
|
||||||
|
/* CUT/COPY/PASTE */
|
||||||
|
|
||||||
|
framepos_t last_paste_pos;
|
||||||
|
unsigned paste_count;
|
||||||
|
|
||||||
void cut_copy (Editing::CutCopyOp);
|
void cut_copy (Editing::CutCopyOp);
|
||||||
bool can_cut_copy () const;
|
bool can_cut_copy () const;
|
||||||
void cut_copy_points (Editing::CutCopyOp);
|
void cut_copy_points (Editing::CutCopyOp);
|
||||||
|
|
|
||||||
|
|
@ -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));
|
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 ts;
|
||||||
TrackViewList::iterator i;
|
TrackViewList::iterator i;
|
||||||
size_t nth;
|
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) {
|
cb != cut_buffer->midi_notes.end() && r != rs.end(); ++r) {
|
||||||
MidiRegionView* mrv = dynamic_cast<MidiRegionView*> (*r);
|
MidiRegionView* mrv = dynamic_cast<MidiRegionView*> (*r);
|
||||||
if (mrv) {
|
if (mrv) {
|
||||||
mrv->paste (position, times, **cb);
|
mrv->paste (position, paste_count, times, **cb);
|
||||||
++cb;
|
++cb;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -4413,7 +4422,7 @@ Editor::paste_internal (framepos_t position, float times)
|
||||||
begin_reversible_command (Operations::paste);
|
begin_reversible_command (Operations::paste);
|
||||||
|
|
||||||
for (nth = 0, i = ts.begin(); i != ts.end(); ++i, ++nth) {
|
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 ();
|
commit_reversible_command ();
|
||||||
|
|
|
||||||
|
|
@ -3321,25 +3321,37 @@ MidiRegionView::selection_as_cut_buffer () const
|
||||||
|
|
||||||
/** This method handles undo */
|
/** This method handles undo */
|
||||||
void
|
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()) {
|
if (mcb.empty()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
PublicEditor& editor = trackview.editor ();
|
||||||
|
|
||||||
trackview.session()->begin_reversible_command (_("paste"));
|
trackview.session()->begin_reversible_command (_("paste"));
|
||||||
|
|
||||||
start_note_diff_command (_("paste"));
|
start_note_diff_command (_("paste"));
|
||||||
|
|
||||||
const Evoral::MusicalTime pos_beats = absolute_frames_to_source_beats (pos);
|
/* 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 first_time = (*mcb.notes().begin())->time();
|
||||||
const Evoral::MusicalTime last_time = (*mcb.notes().rbegin())->end_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;
|
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",
|
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,
|
first_time,
|
||||||
last_time,
|
last_time,
|
||||||
last_time - first_time, pos, _region->position(),
|
duration, pos, _region->position(),
|
||||||
pos_beats));
|
pos_beats));
|
||||||
|
|
||||||
clear_selection ();
|
clear_selection ();
|
||||||
|
|
|
||||||
|
|
@ -112,7 +112,7 @@ public:
|
||||||
void resolve_note(uint8_t note_num, double end_time);
|
void resolve_note(uint8_t note_num, double end_time);
|
||||||
|
|
||||||
void cut_copy_clear (Editing::CutCopyOp);
|
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);
|
void add_canvas_patch_change (ARDOUR::MidiModel::PatchChangePtr patch, const std::string& displaytext, bool);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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 foreach_time_axis_view (sigc::slot<void,TimeAxisView&>) = 0;
|
||||||
virtual void add_to_idle_resize (TimeAxisView*, int32_t) = 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_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 Evoral::MusicalTime get_grid_type_as_beats (bool& success, framepos_t position) = 0;
|
||||||
virtual void edit_notes (TimeAxisViewItem&) = 0;
|
virtual void edit_notes (TimeAxisViewItem&) = 0;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1534,7 +1534,7 @@ RouteTimeAxisView::cut_copy_clear (Selection& selection, CutCopyOp op)
|
||||||
}
|
}
|
||||||
|
|
||||||
bool
|
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()) {
|
if (!is_track()) {
|
||||||
return false;
|
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));
|
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 ();
|
pl->clear_changes ();
|
||||||
if (Config->get_edit_mode() == Ripple) {
|
if (Config->get_edit_mode() == Ripple) {
|
||||||
std::pair<framepos_t, framepos_t> extent = (*p)->get_extent_with_endspace();
|
std::pair<framepos_t, framepos_t> extent = (*p)->get_extent_with_endspace();
|
||||||
|
|
|
||||||
|
|
@ -99,7 +99,7 @@ public:
|
||||||
|
|
||||||
/* Editing operations */
|
/* Editing operations */
|
||||||
void cut_copy_clear (Selection&, Editing::CutCopyOp);
|
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 ();
|
RegionView* combine_regions ();
|
||||||
void uncombine_regions ();
|
void uncombine_regions ();
|
||||||
void uncombine_region (RegionView*);
|
void uncombine_region (RegionView*);
|
||||||
|
|
|
||||||
|
|
@ -165,7 +165,7 @@ class TimeAxisView : public virtual AxisView
|
||||||
/* editing operations */
|
/* editing operations */
|
||||||
|
|
||||||
virtual void cut_copy_clear (Selection&, Editing::CutCopyOp) {}
|
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_regionviews (RegionSelection&) {}
|
||||||
virtual void set_selected_points (PointSelection&) {}
|
virtual void set_selected_points (PointSelection&) {}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue