7109 lines
197 KiB
C++
7109 lines
197 KiB
C++
/*
|
||
* Copyright (C) 2009-2012 Carl Hetherington <carl@carlh.net>
|
||
* Copyright (C) 2009-2015 David Robillard <d@drobilla.net>
|
||
* Copyright (C) 2009-2017 Paul Davis <paul@linuxaudiosystems.com>
|
||
* Copyright (C) 2012-2019 Robin Gareus <robin@gareus.org>
|
||
* Copyright (C) 2013-2017 Nick Mainsbridge <mainsbridge@gmail.com>
|
||
* Copyright (C) 2013 Michael Fisher <mfisher31@gmail.com>
|
||
* Copyright (C) 2014-2019 Ben Loftis <ben@harrisonconsoles.com>
|
||
* Copyright (C) 2014 Colin Fletcher <colin.m.fletcher@googlemail.com>
|
||
* Copyright (C) 2015-2017 Tim Mayberry <mojofunk@gmail.com>
|
||
*
|
||
* 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.
|
||
*/
|
||
|
||
#ifdef WAF_BUILD
|
||
#include "gtk2ardour-config.h"
|
||
#endif
|
||
|
||
#include <stdint.h>
|
||
#include <algorithm>
|
||
|
||
#include "pbd/memento_command.h"
|
||
#include "pbd/basename.h"
|
||
#include "pbd/stateful_diff_command.h"
|
||
|
||
#include <gtkmm/stock.h>
|
||
|
||
#include "gtkmm2ext/utils.h"
|
||
|
||
#include "ardour/audioengine.h"
|
||
#include "ardour/audioregion.h"
|
||
#include "ardour/audio_track.h"
|
||
#include "ardour/dB.h"
|
||
#include "ardour/midi_region.h"
|
||
#include "ardour/midi_track.h"
|
||
#include "ardour/operations.h"
|
||
#include "ardour/profile.h"
|
||
#include "ardour/region_factory.h"
|
||
#include "ardour/session.h"
|
||
#include "ardour/session_playlists.h"
|
||
|
||
#include "canvas/canvas.h"
|
||
#include "canvas/scroll_group.h"
|
||
|
||
#include "editor.h"
|
||
#include "pbd/i18n.h"
|
||
#include "bbt_marker_dialog.h"
|
||
#include "keyboard.h"
|
||
#include "audio_region_view.h"
|
||
#include "automation_region_view.h"
|
||
#include "midi_region_view.h"
|
||
#include "ardour_ui.h"
|
||
#include "gui_thread.h"
|
||
#include "control_point.h"
|
||
#include "region_gain_line.h"
|
||
#include "editor_drag.h"
|
||
#include "audio_time_axis.h"
|
||
#include "midi_time_axis.h"
|
||
#include "selection.h"
|
||
#include "midi_selection.h"
|
||
#include "automation_time_axis.h"
|
||
#include "debug.h"
|
||
#include "editor_cursors.h"
|
||
#include "mouse_cursors.h"
|
||
#include "note_base.h"
|
||
#include "patch_change.h"
|
||
#include "ui_config.h"
|
||
#include "verbose_cursor.h"
|
||
#include "video_timeline.h"
|
||
|
||
using namespace std;
|
||
using namespace ARDOUR;
|
||
using namespace PBD;
|
||
using namespace Gtk;
|
||
using namespace Gtkmm2ext;
|
||
using namespace Editing;
|
||
using namespace ArdourCanvas;
|
||
using namespace Temporal;
|
||
|
||
using Gtkmm2ext::Keyboard;
|
||
|
||
double ControlPointDrag::_zero_gain_fraction = -1.0;
|
||
|
||
DragManager::DragManager (Editor* e)
|
||
: _editor (e)
|
||
, _ending (false)
|
||
, _current_pointer_x (0.0)
|
||
, _current_pointer_y (0.0)
|
||
, _current_pointer_time (timepos_t::from_superclock (0)) /* avoid early use of superclock_ticks_per_second */
|
||
, _old_follow_playhead (false)
|
||
{
|
||
}
|
||
|
||
DragManager::~DragManager ()
|
||
{
|
||
abort ();
|
||
}
|
||
|
||
/** Call abort for each active drag */
|
||
void
|
||
DragManager::abort ()
|
||
{
|
||
_ending = true;
|
||
|
||
for (list<Drag*>::const_iterator i = _drags.begin(); i != _drags.end(); ++i) {
|
||
(*i)->abort ();
|
||
delete *i;
|
||
}
|
||
|
||
if (!_drags.empty ()) {
|
||
_editor->set_follow_playhead (_old_follow_playhead, false);
|
||
}
|
||
|
||
_drags.clear ();
|
||
_editor->abort_reversible_command();
|
||
|
||
_ending = false;
|
||
}
|
||
|
||
void
|
||
DragManager::add (Drag* d)
|
||
{
|
||
d->set_manager (this);
|
||
_drags.push_back (d);
|
||
}
|
||
|
||
void
|
||
DragManager::set (Drag* d, GdkEvent* e, Gdk::Cursor* c)
|
||
{
|
||
d->set_manager (this);
|
||
_drags.push_back (d);
|
||
start_grab (e, c);
|
||
}
|
||
|
||
bool
|
||
DragManager::preview_video () const {
|
||
for (list<Drag*>::const_iterator i = _drags.begin(); i != _drags.end(); ++i) {
|
||
if ((*i)->preview_video ()) {
|
||
return true;
|
||
}
|
||
}
|
||
return false;
|
||
}
|
||
|
||
void
|
||
DragManager::start_grab (GdkEvent* e, Gdk::Cursor* c)
|
||
{
|
||
/* Prevent follow playhead during the drag to be nice to the user */
|
||
_old_follow_playhead = _editor->follow_playhead ();
|
||
_editor->set_follow_playhead (false);
|
||
|
||
_current_pointer_time = timepos_t (_editor->canvas_event_sample (e, &_current_pointer_x, &_current_pointer_y));
|
||
|
||
for (list<Drag*>::const_iterator i = _drags.begin(); i != _drags.end(); ++i) {
|
||
if ((*i)->grab_button() < 0) {
|
||
(*i)->start_grab (e, c);
|
||
}
|
||
}
|
||
}
|
||
|
||
/** Call end_grab for each active drag.
|
||
* @return true if any drag reported movement having occurred.
|
||
*/
|
||
bool
|
||
DragManager::end_grab (GdkEvent* e)
|
||
{
|
||
_ending = true;
|
||
|
||
bool r = false;
|
||
|
||
for (list<Drag*>::iterator i = _drags.begin(); i != _drags.end(); ) {
|
||
list<Drag*>::iterator tmp = i;
|
||
|
||
if ((*i)->grab_button() == (int) e->button.button) {
|
||
bool const t = (*i)->end_grab (e);
|
||
if (t) {
|
||
r = true;
|
||
}
|
||
delete *i;
|
||
tmp = _drags.erase (i);
|
||
} else {
|
||
++tmp;
|
||
}
|
||
|
||
i = tmp;
|
||
}
|
||
|
||
_ending = false;
|
||
|
||
if (_drags.empty()) {
|
||
_editor->set_follow_playhead (_old_follow_playhead, false);
|
||
}
|
||
|
||
return r;
|
||
}
|
||
|
||
void
|
||
DragManager::mark_double_click ()
|
||
{
|
||
for (list<Drag*>::const_iterator i = _drags.begin(); i != _drags.end(); ++i) {
|
||
(*i)->set_double_click (true);
|
||
}
|
||
}
|
||
|
||
bool
|
||
DragManager::motion_handler (GdkEvent* e, bool from_autoscroll)
|
||
{
|
||
bool r = false;
|
||
|
||
/* calling this implies that we expect the event to have canvas
|
||
* coordinates
|
||
*
|
||
* Can we guarantee that this is true?
|
||
*/
|
||
|
||
_current_pointer_time = timepos_t (_editor->canvas_event_sample (e, &_current_pointer_x, &_current_pointer_y));
|
||
|
||
for (list<Drag*>::iterator i = _drags.begin(); i != _drags.end(); ++i) {
|
||
bool const t = (*i)->motion_handler (e, from_autoscroll);
|
||
/* run all handlers; return true if at least one of them
|
||
returns true (indicating that the event has been handled).
|
||
*/
|
||
if (t) {
|
||
r = true;
|
||
}
|
||
|
||
}
|
||
|
||
return r;
|
||
}
|
||
|
||
bool
|
||
DragManager::have_item (ArdourCanvas::Item* i) const
|
||
{
|
||
list<Drag*>::const_iterator j = _drags.begin ();
|
||
while (j != _drags.end() && (*j)->item () != i) {
|
||
++j;
|
||
}
|
||
|
||
return j != _drags.end ();
|
||
}
|
||
|
||
Drag::Drag (Editor* e, ArdourCanvas::Item* i, Temporal::TimeDomain td, bool trackview_only)
|
||
: _editor (e)
|
||
, _drags (0)
|
||
, _item (i)
|
||
, _pointer_offset (0)
|
||
, _video_offset (0)
|
||
, _preview_video (false)
|
||
, _x_constrained (false)
|
||
, _y_constrained (false)
|
||
, _was_rolling (false)
|
||
, _earliest_time_limit (0)
|
||
, _trackview_only (trackview_only)
|
||
, _move_threshold_passed (false)
|
||
, _starting_point_passed (false)
|
||
, _initially_vertical (false)
|
||
, _was_double_click (false)
|
||
, _grab_x (0.0)
|
||
, _grab_y (0.0)
|
||
, _last_pointer_x (0.0)
|
||
, _last_pointer_y (0.0)
|
||
, _time_domain (td)
|
||
, _snap_delta (0)
|
||
, _constraint_pressed (false)
|
||
, _grab_button (-1)
|
||
{
|
||
}
|
||
|
||
timepos_t
|
||
Drag::pixel_to_time (double x) const
|
||
{
|
||
samplepos_t p = _editor->pixel_to_sample (x);
|
||
|
||
if (_time_domain == Temporal::AudioTime) {
|
||
return timepos_t (p);
|
||
}
|
||
|
||
return timepos_t (timepos_t (p).beats ());
|
||
}
|
||
|
||
void
|
||
Drag::swap_grab (ArdourCanvas::Item* new_item, Gdk::Cursor* cursor, uint32_t /*time*/)
|
||
{
|
||
_item->ungrab ();
|
||
_item = new_item;
|
||
|
||
if (!_cursor_ctx) {
|
||
_cursor_ctx = CursorContext::create (*_editor, cursor);
|
||
} else {
|
||
_cursor_ctx->change (cursor);
|
||
}
|
||
|
||
_item->grab ();
|
||
}
|
||
|
||
void
|
||
Drag::start_grab (GdkEvent* event, Gdk::Cursor *cursor)
|
||
{
|
||
/* we set up x/y dragging constraints on first move */
|
||
_constraint_pressed = ArdourKeyboard::indicates_constraint (event->button.state);
|
||
|
||
const samplepos_t pos = _editor->canvas_event_sample (event, &_grab_x, &_grab_y);
|
||
|
||
if (_time_domain == Temporal::AudioTime) {
|
||
_raw_grab_time = timepos_t (pos);
|
||
} else {
|
||
_raw_grab_time = timepos_t (timepos_t (pos).beats());
|
||
}
|
||
|
||
_grab_button = event->button.button;
|
||
|
||
setup_pointer_offset ();
|
||
setup_video_offset ();
|
||
if (! UIConfiguration::instance ().get_preview_video_frame_on_drag ()) {
|
||
_preview_video = false;
|
||
}
|
||
|
||
_grab_time = adjusted_time (_raw_grab_time, event);
|
||
_last_pointer_time = _grab_time;
|
||
_last_pointer_x = _grab_x;
|
||
|
||
if (_trackview_only) {
|
||
_grab_y = _grab_y - _editor->get_trackview_group()->canvas_origin().y;
|
||
}
|
||
|
||
_last_pointer_y = _grab_y;
|
||
|
||
_item->grab ();
|
||
|
||
if (!_editor->cursors()->is_invalid (cursor)) {
|
||
/* CAIROCANVAS need a variant here that passes *cursor */
|
||
_cursor_ctx = CursorContext::create (*_editor, cursor);
|
||
}
|
||
|
||
if (_editor->session() && _editor->session()->transport_rolling()) {
|
||
_was_rolling = true;
|
||
} else {
|
||
_was_rolling = false;
|
||
}
|
||
|
||
// if ( UIConfiguration::instance().get_snap_to_region_start() || UIConfiguration::instance().get_snap_to_region_end() || UIConfiguration::instance().get_snap_to_region_sync() ) {
|
||
// _editor->build_region_boundary_cache ();
|
||
// }
|
||
}
|
||
|
||
/** Call to end a drag `successfully'. Ungrabs item and calls
|
||
* subclass' finished() method.
|
||
*
|
||
* @param event GDK event, or 0.
|
||
* @return true if some movement occurred, otherwise false.
|
||
*/
|
||
bool
|
||
Drag::end_grab (GdkEvent* event)
|
||
{
|
||
_editor->stop_canvas_autoscroll ();
|
||
|
||
_item->ungrab ();
|
||
|
||
finished (event, _starting_point_passed);
|
||
|
||
_editor->verbose_cursor()->hide ();
|
||
_cursor_ctx.reset();
|
||
|
||
return _starting_point_passed;
|
||
}
|
||
|
||
timepos_t
|
||
Drag::adjusted_time (timepos_t const & f, GdkEvent const * event, bool snap) const
|
||
{
|
||
timepos_t pos (f.time_domain()); /* zero */
|
||
|
||
if (f > _pointer_offset) {
|
||
pos = f;
|
||
pos.shift_earlier (_pointer_offset);
|
||
}
|
||
|
||
if (snap) {
|
||
_editor->snap_to_with_modifier (pos, event);
|
||
}
|
||
|
||
return pos;
|
||
}
|
||
|
||
timepos_t
|
||
Drag::adjusted_current_time (GdkEvent const * event, bool snap) const
|
||
{
|
||
return adjusted_time (_drags->current_pointer_time (), event, snap);
|
||
}
|
||
|
||
timecnt_t
|
||
Drag::snap_delta (guint state) const
|
||
{
|
||
if (ArdourKeyboard::indicates_snap_delta (state)) {
|
||
return _snap_delta;
|
||
}
|
||
|
||
return timecnt_t (_editor->default_time_domain());
|
||
}
|
||
|
||
double
|
||
Drag::current_pointer_x() const
|
||
{
|
||
return _drags->current_pointer_x ();
|
||
}
|
||
|
||
double
|
||
Drag::current_pointer_y () const
|
||
{
|
||
if (!_trackview_only) {
|
||
return _drags->current_pointer_y ();
|
||
}
|
||
|
||
return _drags->current_pointer_y () - _editor->get_trackview_group()->canvas_origin().y;
|
||
}
|
||
|
||
void
|
||
Drag::setup_snap_delta (timepos_t const & pos)
|
||
{
|
||
timepos_t snap (pos);
|
||
_editor->snap_to (snap, Temporal::RoundNearest, ARDOUR::SnapToAny_Visual, true);
|
||
_snap_delta = pos.distance (snap);
|
||
}
|
||
|
||
bool
|
||
Drag::motion_handler (GdkEvent* event, bool from_autoscroll)
|
||
{
|
||
/* check to see if we have moved in any way that matters since the last motion event */
|
||
if (_move_threshold_passed &&
|
||
(!x_movement_matters() || _last_pointer_x == current_pointer_x ()) &&
|
||
(!y_movement_matters() || _last_pointer_y == current_pointer_y ()) ) {
|
||
return false;
|
||
}
|
||
|
||
pair<timecnt_t, int> const threshold = move_threshold ();
|
||
|
||
bool const old_move_threshold_passed = _move_threshold_passed;
|
||
|
||
if (!_move_threshold_passed) {
|
||
|
||
bool const xp = (_raw_grab_time.distance (_drags->current_pointer_time ()).abs() >= threshold.first);
|
||
bool const yp = (::fabs ((current_pointer_y () - _grab_y)) >= threshold.second);
|
||
|
||
_move_threshold_passed = ((xp && x_movement_matters()) || (yp && y_movement_matters()));
|
||
}
|
||
|
||
if (active (_editor->mouse_mode) && _move_threshold_passed) {
|
||
|
||
if (event->motion.state & Gdk::BUTTON1_MASK || event->motion.state & Gdk::BUTTON2_MASK) {
|
||
|
||
if (old_move_threshold_passed != _move_threshold_passed) {
|
||
|
||
/* just changed */
|
||
|
||
if (fabs (current_pointer_y() - _grab_y) > fabs (current_pointer_x() - _grab_x)) {
|
||
_initially_vertical = true;
|
||
} else {
|
||
_initially_vertical = false;
|
||
}
|
||
/** check constraints for this drag.
|
||
* Note that the current convention is to use "contains" for
|
||
* key modifiers during motion and "equals" when initiating a drag.
|
||
* In this case we haven't moved yet, so "equals" applies here.
|
||
*/
|
||
if (Config->get_edit_mode() != Lock) {
|
||
if (event->motion.state & Gdk::BUTTON2_MASK) {
|
||
// if dragging with button2, the motion is x constrained, with constraint modifier it is y constrained
|
||
if (_constraint_pressed) {
|
||
_x_constrained = false;
|
||
_y_constrained = true;
|
||
} else {
|
||
_x_constrained = true;
|
||
_y_constrained = false;
|
||
}
|
||
} else if (_constraint_pressed) {
|
||
// if dragging normally, the motion is constrained to the first direction of movement.
|
||
if (_initially_vertical) {
|
||
_x_constrained = true;
|
||
_y_constrained = false;
|
||
} else {
|
||
_x_constrained = false;
|
||
_y_constrained = true;
|
||
}
|
||
}
|
||
} else {
|
||
if (event->button.state & Gdk::BUTTON2_MASK) {
|
||
_x_constrained = false;
|
||
} else {
|
||
_x_constrained = true;
|
||
}
|
||
_y_constrained = false;
|
||
}
|
||
}
|
||
|
||
if (!from_autoscroll) {
|
||
_editor->maybe_autoscroll (true, allow_vertical_autoscroll (), false);
|
||
}
|
||
|
||
if (!_editor->autoscroll_active() || from_autoscroll) {
|
||
|
||
|
||
bool first_move = (_move_threshold_passed != old_move_threshold_passed) || from_autoscroll;
|
||
|
||
motion (event, first_move && !_starting_point_passed);
|
||
|
||
if (first_move && !_starting_point_passed) {
|
||
_starting_point_passed = true;
|
||
}
|
||
|
||
_last_pointer_x = _drags->current_pointer_x ();
|
||
_last_pointer_y = current_pointer_y ();
|
||
_last_pointer_time = adjusted_current_time (event, false);
|
||
}
|
||
|
||
return true;
|
||
}
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
/** Call to abort a drag. Ungrabs item and calls subclass's aborted () */
|
||
void
|
||
Drag::abort ()
|
||
{
|
||
if (_item) {
|
||
_item->ungrab ();
|
||
}
|
||
|
||
aborted (_move_threshold_passed);
|
||
|
||
_editor->stop_canvas_autoscroll ();
|
||
_editor->verbose_cursor()->hide ();
|
||
}
|
||
|
||
void
|
||
Drag::show_verbose_cursor_time (timepos_t const & pos)
|
||
{
|
||
_editor->verbose_cursor()->set_time (pos.samples());
|
||
_editor->verbose_cursor()->show ();
|
||
}
|
||
|
||
void
|
||
Drag::show_verbose_cursor_duration (timepos_t const & start, timepos_t const & end, double /*xoffset*/)
|
||
{
|
||
_editor->verbose_cursor()->set_duration (start.samples(), end.samples());
|
||
_editor->verbose_cursor()->show ();
|
||
}
|
||
|
||
void
|
||
Drag::show_verbose_cursor_text (string const & text)
|
||
{
|
||
_editor->verbose_cursor()->set (text);
|
||
_editor->verbose_cursor()->show ();
|
||
}
|
||
|
||
void
|
||
Drag::show_view_preview (timepos_t const & pos)
|
||
{
|
||
if (_preview_video) {
|
||
ARDOUR_UI::instance()->video_timeline->manual_seek_video_monitor (pos.samples());
|
||
}
|
||
}
|
||
|
||
boost::shared_ptr<Region>
|
||
Drag::add_midi_region (MidiTimeAxisView* view, bool commit)
|
||
{
|
||
if (_editor->session()) {
|
||
const timepos_t pos (grab_time());
|
||
/* not that the frame rate used here can be affected by pull up/down which
|
||
might be wrong.
|
||
*/
|
||
|
||
timepos_t beats = timepos_t (grab_time().beats());
|
||
cerr << "Adding region based on grab @ " << grab_time() << " = " << beats << " aka " << beats.beats() << endl;
|
||
timecnt_t len = pos.distance (max (timepos_t::zero (Temporal::BeatTime), timepos_t (pos.beats() + Beats (1, 0))));
|
||
return view->add_region (beats, len, commit);
|
||
}
|
||
|
||
return boost::shared_ptr<Region>();
|
||
}
|
||
|
||
struct TimeAxisViewStripableSorter {
|
||
bool operator() (TimeAxisView* tav_a, TimeAxisView* tav_b) {
|
||
boost::shared_ptr<ARDOUR::Stripable> const& a = tav_a->stripable ();
|
||
boost::shared_ptr<ARDOUR::Stripable> const& b = tav_b->stripable ();
|
||
return ARDOUR::Stripable::Sorter () (a, b);
|
||
}
|
||
};
|
||
|
||
RegionDrag::RegionDrag (Editor* e, ArdourCanvas::Item* i, RegionView* p, list<RegionView*> const & v, Temporal::TimeDomain td)
|
||
: Drag (e, i, td)
|
||
, _primary (p)
|
||
, _ntracks (0)
|
||
{
|
||
_editor->visible_order_range (&_visible_y_low, &_visible_y_high);
|
||
|
||
/* Make a list of tracks to refer to during the drag; we include hidden tracks,
|
||
as some of the regions we are dragging may be on such tracks.
|
||
*/
|
||
|
||
TrackViewList track_views = _editor->track_views;
|
||
track_views.sort (TimeAxisViewStripableSorter ());
|
||
|
||
for (TrackViewList::iterator i = track_views.begin(); i != track_views.end(); ++i) {
|
||
_time_axis_views.push_back (*i);
|
||
|
||
TimeAxisView::Children children_list = (*i)->get_child_list ();
|
||
for (TimeAxisView::Children::iterator j = children_list.begin(); j != children_list.end(); ++j) {
|
||
_time_axis_views.push_back (j->get());
|
||
}
|
||
}
|
||
|
||
/* the list of views can be empty at this point if this is a region list-insert drag
|
||
*/
|
||
|
||
for (list<RegionView*>::const_iterator i = v.begin(); i != v.end(); ++i) {
|
||
_views.push_back (DraggingView (*i, this, &(*i)->get_time_axis_view()));
|
||
}
|
||
|
||
RegionView::RegionViewGoingAway.connect (death_connection, invalidator (*this), boost::bind (&RegionDrag::region_going_away, this, _1), gui_context());
|
||
}
|
||
|
||
void
|
||
RegionDrag::region_going_away (RegionView* v)
|
||
{
|
||
list<DraggingView>::iterator i = _views.begin ();
|
||
while (i != _views.end() && i->view != v) {
|
||
++i;
|
||
}
|
||
|
||
if (i != _views.end()) {
|
||
_views.erase (i);
|
||
}
|
||
}
|
||
|
||
/** Given a TimeAxisView, return the index of it into the _time_axis_views vector,
|
||
* or -1 if it is not found.
|
||
*/
|
||
int
|
||
RegionDrag::find_time_axis_view (TimeAxisView* t) const
|
||
{
|
||
int i = 0;
|
||
int const N = _time_axis_views.size ();
|
||
while (i < N && _time_axis_views[i] != t) {
|
||
++i;
|
||
}
|
||
|
||
if (i == N) {
|
||
return -1;
|
||
}
|
||
|
||
return i;
|
||
}
|
||
|
||
void
|
||
RegionDrag::setup_video_offset ()
|
||
{
|
||
if (_views.empty ()) {
|
||
_preview_video = true;
|
||
return;
|
||
}
|
||
timepos_t first_sync = _views.begin()->view->region()->sync_position ();
|
||
for (list<DraggingView>::const_iterator i = _views.begin(); i != _views.end(); ++i) {
|
||
first_sync = std::min (first_sync, i->view->region()->sync_position ());
|
||
}
|
||
_video_offset = raw_grab_time().distance (first_sync + _pointer_offset);
|
||
_preview_video = true;
|
||
}
|
||
|
||
void
|
||
RegionDrag::add_stateful_diff_commands_for_playlists (PlaylistSet const & playlists)
|
||
{
|
||
for (PlaylistSet::const_iterator i = playlists.begin(); i != playlists.end(); ++i) {
|
||
StatefulDiffCommand* c = new StatefulDiffCommand (*i);
|
||
if (!c->empty()) {
|
||
_editor->session()->add_command (c);
|
||
} else {
|
||
delete c;
|
||
}
|
||
}
|
||
}
|
||
|
||
|
||
RegionSlipContentsDrag::RegionSlipContentsDrag (Editor* e, ArdourCanvas::Item* i, RegionView* p, list<RegionView*> const & v, TimeDomain td)
|
||
: RegionDrag (e, i, p, v, td)
|
||
{
|
||
DEBUG_TRACE (DEBUG::Drags, "New RegionSlipContentsDrag\n");
|
||
}
|
||
|
||
void
|
||
RegionSlipContentsDrag::start_grab (GdkEvent* event, Gdk::Cursor* cursor)
|
||
{
|
||
Drag::start_grab (event, _editor->cursors()->trimmer);
|
||
}
|
||
|
||
void
|
||
RegionSlipContentsDrag::motion (GdkEvent* event, bool first_move)
|
||
{
|
||
if (first_move) {
|
||
|
||
/*prepare reversible cmd*/
|
||
_editor->begin_reversible_command (_("Slip Contents"));
|
||
for (list<DraggingView>::iterator i = _views.begin(); i != _views.end(); ++i) {
|
||
RegionView* rv = i->view;
|
||
rv->region()->clear_changes ();
|
||
|
||
rv->drag_start (); //this allows the region to draw itself 'transparently' while we drag it
|
||
}
|
||
|
||
} else {
|
||
for (list<DraggingView>::iterator i = _views.begin(); i != _views.end(); ++i) {
|
||
RegionView* rv = i->view;
|
||
timecnt_t slippage = adjusted_current_time(event, false).distance (last_pointer_time());
|
||
rv->move_contents (slippage);
|
||
}
|
||
show_verbose_cursor_time (_primary->region()->start ());
|
||
}
|
||
}
|
||
|
||
void
|
||
RegionSlipContentsDrag::finished (GdkEvent *, bool movement_occurred)
|
||
{
|
||
if (movement_occurred) {
|
||
/*finish reversible cmd*/
|
||
for (list<DraggingView>::iterator i = _views.begin(); i != _views.end(); ++i) {
|
||
RegionView* rv = i->view;
|
||
_editor->session()->add_command (new StatefulDiffCommand (rv->region()));
|
||
|
||
rv->drag_end ();
|
||
}
|
||
_editor->commit_reversible_command ();
|
||
}
|
||
}
|
||
|
||
void
|
||
RegionSlipContentsDrag::aborted (bool movement_occurred)
|
||
{
|
||
/* ToDo: revert to the original region properties */
|
||
_editor->abort_reversible_command ();
|
||
}
|
||
|
||
|
||
RegionBrushDrag::RegionBrushDrag (Editor* e, ArdourCanvas::Item* i, RegionView* p, list<RegionView*> const & v, TimeDomain td)
|
||
: RegionDrag (e, i, p, v, td)
|
||
{
|
||
DEBUG_TRACE (DEBUG::Drags, "New RegionBrushDrag\n");
|
||
_y_constrained = true;
|
||
}
|
||
|
||
void
|
||
RegionBrushDrag::start_grab (GdkEvent* event, Gdk::Cursor* cursor)
|
||
{
|
||
Drag::start_grab (event, _editor->cursors()->trimmer);
|
||
}
|
||
|
||
void
|
||
RegionBrushDrag::motion (GdkEvent* event, bool first_move)
|
||
{
|
||
if (first_move) {
|
||
_editor->begin_reversible_command (_("Region brush drag"));
|
||
_already_pasted.insert(_primary->region()->position());
|
||
} else {
|
||
timepos_t snapped (adjusted_current_time (event, false));
|
||
_editor->snap_to (snapped, RoundDownAlways, SnapToGrid_Scaled, false);
|
||
if(_already_pasted.find(snapped) == _already_pasted.end()) {
|
||
_editor->mouse_brush_insert_region (_primary, snapped);
|
||
_already_pasted.insert(snapped);
|
||
}
|
||
}
|
||
}
|
||
|
||
void
|
||
RegionBrushDrag::finished (GdkEvent *, bool movement_occurred)
|
||
{
|
||
if (!movement_occurred) {
|
||
return;
|
||
}
|
||
|
||
PlaylistSet modified_playlists;
|
||
modified_playlists.insert(_primary->region()->playlist());
|
||
add_stateful_diff_commands_for_playlists(modified_playlists);
|
||
_editor->commit_reversible_command ();
|
||
_already_pasted.clear();
|
||
}
|
||
|
||
void
|
||
RegionBrushDrag::aborted (bool movement_occurred)
|
||
{
|
||
_already_pasted.clear();
|
||
|
||
/* ToDo: revert to the original playlist properties */
|
||
_editor->abort_reversible_command ();
|
||
}
|
||
|
||
RegionMotionDrag::RegionMotionDrag (Editor* e, ArdourCanvas::Item* i, RegionView* p, list<RegionView*> const & v, TimeDomain td)
|
||
: RegionDrag (e, i, p, v, td)
|
||
, _ignore_video_lock (false)
|
||
, _total_x_delta (0)
|
||
, _last_pointer_time_axis_view (0)
|
||
, _last_pointer_layer (0)
|
||
, _ndropzone (0)
|
||
, _pdropzone (0)
|
||
, _ddropzone (0)
|
||
{
|
||
DEBUG_TRACE (DEBUG::Drags, "New RegionMotionDrag\n");
|
||
}
|
||
|
||
void
|
||
RegionMotionDrag::start_grab (GdkEvent* event, Gdk::Cursor* cursor)
|
||
{
|
||
Drag::start_grab (event, cursor);
|
||
setup_snap_delta (_last_position);
|
||
|
||
show_verbose_cursor_time (_last_position);
|
||
show_view_preview (_last_position + _video_offset);
|
||
|
||
/* this conditional is required because drag-n-drop'ed regions end up
|
||
* here, and at this point they are not attached to a playlist.
|
||
*/
|
||
|
||
if (_editor->should_ripple() && _primary && _primary->region() && _primary->region()->playlist()) {
|
||
_earliest_time_limit = _primary->region()->playlist()->find_prev_region_start (_primary->region()->position());
|
||
}
|
||
|
||
pair<TimeAxisView*, double> const tv = _editor->trackview_by_y_position (current_pointer_y ());
|
||
if (tv.first) {
|
||
_last_pointer_time_axis_view = find_time_axis_view (tv.first);
|
||
assert(_last_pointer_time_axis_view >= 0);
|
||
_last_pointer_layer = tv.first->layer_display() == Overlaid ? 0 : tv.second;
|
||
}
|
||
|
||
if (Keyboard::modifier_state_equals (event->button.state, Keyboard::ModifierMask (Keyboard::TertiaryModifier))) {
|
||
_ignore_video_lock = true;
|
||
}
|
||
|
||
if (_editor->should_ripple()) {
|
||
/* we do not drag across tracks when rippling or brushing */
|
||
_y_constrained = true;
|
||
}
|
||
}
|
||
|
||
double
|
||
RegionMotionDrag::compute_x_delta (GdkEvent const * event, Temporal::timepos_t & pending_region_position)
|
||
{
|
||
/* compute the amount of pointer motion in samples, and where
|
||
the region would be if we moved it by that much.
|
||
*/
|
||
if (_x_constrained) {
|
||
pending_region_position = _last_position;
|
||
return 0.0;
|
||
}
|
||
|
||
pending_region_position = adjusted_time (_drags->current_pointer_time (), event, false);
|
||
|
||
timecnt_t sync_offset;
|
||
int32_t sync_dir;
|
||
|
||
sync_offset = _primary->region()->sync_offset (sync_dir);
|
||
|
||
/* we don't handle a sync point that lies before zero.
|
||
*/
|
||
if (sync_dir >= 0 || (sync_dir < 0 && pending_region_position >= sync_offset)) {
|
||
|
||
timecnt_t const sd = snap_delta (event->button.state);
|
||
timepos_t sync_snap;
|
||
if (sync_dir > 0) {
|
||
sync_snap = pending_region_position + sync_offset + sd;
|
||
} else {
|
||
sync_snap = pending_region_position.earlier (sync_offset) + sd;
|
||
}
|
||
_editor->snap_to_with_modifier (sync_snap, event);
|
||
if (sync_offset.is_zero () && sd.is_zero ()) {
|
||
pending_region_position = sync_snap;
|
||
} else {
|
||
pending_region_position = _primary->region()->adjust_to_sync (sync_snap).earlier (sd);
|
||
}
|
||
} else {
|
||
pending_region_position = _last_position;
|
||
}
|
||
|
||
if (pending_region_position > timepos_t::max (time_domain()).earlier (_primary->region()->length())) {
|
||
pending_region_position = _last_position;
|
||
}
|
||
|
||
if (!_earliest_time_limit.is_zero () && pending_region_position <= _earliest_time_limit) {
|
||
pending_region_position = _earliest_time_limit;
|
||
return 0.0;
|
||
}
|
||
|
||
double dx = 0;
|
||
|
||
bool const x_move_allowed = !_x_constrained;
|
||
|
||
if ((pending_region_position != _last_position) && x_move_allowed) {
|
||
|
||
/* x movement since last time (in pixels) */
|
||
dx = _editor->duration_to_pixels_unrounded (_last_position.distance (pending_region_position));
|
||
|
||
/* total x movement */
|
||
timecnt_t total_dx = timecnt_t (pixel_to_time (_total_x_delta + dx), grab_time());
|
||
|
||
for (list<DraggingView>::const_iterator i = _views.begin(); i != _views.end(); ++i) {
|
||
const timepos_t off = i->view->region()->position() + total_dx;
|
||
if (off.is_negative()) {
|
||
dx = dx - _editor->time_to_pixel_unrounded (off);
|
||
pending_region_position = pending_region_position.earlier (timecnt_t (off, timepos_t (time_domain())));
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
|
||
_editor->set_snapped_cursor_position (pending_region_position);
|
||
|
||
return dx;
|
||
}
|
||
|
||
int
|
||
RegionDrag::apply_track_delta (const int start, const int delta, const int skip, const bool distance_only) const
|
||
{
|
||
if (delta == 0) {
|
||
return start;
|
||
}
|
||
|
||
const int tavsize = _time_axis_views.size();
|
||
const int dt = delta > 0 ? +1 : -1;
|
||
int current = start;
|
||
int target = start + delta - skip;
|
||
|
||
assert (current < 0 || current >= tavsize || !_time_axis_views[current]->hidden());
|
||
assert (skip == 0 || (skip < 0 && delta < 0) || (skip > 0 && delta > 0));
|
||
|
||
while (current >= 0 && current != target) {
|
||
current += dt;
|
||
if (current < 0 && dt < 0) {
|
||
break;
|
||
}
|
||
if (current >= tavsize && dt > 0) {
|
||
break;
|
||
}
|
||
if (current < 0 || current >= tavsize) {
|
||
continue;
|
||
}
|
||
|
||
RouteTimeAxisView const * rtav = dynamic_cast<RouteTimeAxisView const *> (_time_axis_views[current]);
|
||
if (_time_axis_views[current]->hidden() || !rtav || !rtav->is_track()) {
|
||
target += dt;
|
||
}
|
||
|
||
if (distance_only && current == start + delta) {
|
||
break;
|
||
}
|
||
}
|
||
return target;
|
||
}
|
||
|
||
bool
|
||
RegionMotionDrag::y_movement_allowed (int delta_track, double delta_layer, int skip_invisible) const
|
||
{
|
||
if (_y_constrained) {
|
||
return false;
|
||
}
|
||
|
||
const int tavsize = _time_axis_views.size();
|
||
for (list<DraggingView>::const_iterator i = _views.begin(); i != _views.end(); ++i) {
|
||
int n = apply_track_delta (i->time_axis_view, delta_track, skip_invisible);
|
||
assert (n < 0 || n >= tavsize || !_time_axis_views[n]->hidden());
|
||
|
||
if (i->time_axis_view < 0 || i->time_axis_view >= tavsize) {
|
||
/* already in the drop zone */
|
||
if (delta_track >= 0) {
|
||
/* downward motion - OK if others are still not in the dropzone */
|
||
continue;
|
||
}
|
||
|
||
}
|
||
|
||
if (n < 0) {
|
||
/* off the top */
|
||
return false;
|
||
} else if (n >= tavsize) {
|
||
/* downward motion into drop zone. That's fine. */
|
||
continue;
|
||
}
|
||
|
||
RouteTimeAxisView const * to = dynamic_cast<RouteTimeAxisView const *> (_time_axis_views[n]);
|
||
if (to == 0 || to->hidden() || !to->is_track() || to->track()->data_type() != i->view->region()->data_type()) {
|
||
/* not a track, or the wrong type */
|
||
return false;
|
||
}
|
||
|
||
double const l = i->layer + delta_layer;
|
||
|
||
/* Note that we allow layer to be up to 0.5 below zero, as this is used by `Expanded'
|
||
mode to allow the user to place a region below another on layer 0.
|
||
*/
|
||
if (delta_track == 0 && (l < -0.5 || l >= int (to->view()->layers()))) {
|
||
/* Off the top or bottom layer; note that we only refuse if the track hasn't changed.
|
||
If it has, the layers will be munged later anyway, so it's ok.
|
||
*/
|
||
return false;
|
||
}
|
||
}
|
||
|
||
/* all regions being dragged are ok with this change */
|
||
return true;
|
||
}
|
||
|
||
struct DraggingViewSorter {
|
||
bool operator() (const DraggingView& a, const DraggingView& b) {
|
||
return a.time_axis_view < b.time_axis_view;
|
||
}
|
||
};
|
||
|
||
void
|
||
RegionMotionDrag::collect_ripple_views ()
|
||
{
|
||
RegionSelection copy;
|
||
TrackViewList tracklist;
|
||
|
||
/* find all regions that we *might* ripple */
|
||
_editor->get_regionviews_at_or_after (_primary->region()->position(), copy);
|
||
|
||
/* if we aren't in ripple-all, find which tracks we will be rippling, based on the current region selection */
|
||
if (!_editor->should_ripple_all()) {
|
||
for (RegionSelection::iterator r = _editor->selection->regions.begin(); r != _editor->selection->regions.end(); ++r) {
|
||
tracklist.push_back (&(*r)->get_time_axis_view ());
|
||
}
|
||
}
|
||
|
||
for (RegionSelection::reverse_iterator i = copy.rbegin(); i != copy.rend(); ++i) {
|
||
TimeAxisView *tav = &(*i)->get_time_axis_view();
|
||
if (_editor->should_ripple_all() || tracklist.contains(tav)) {
|
||
if (!_editor->selection->regions.contains (*i)) {
|
||
_views.push_back (DraggingView (*i, this, &(*i)->get_time_axis_view()));
|
||
}
|
||
}
|
||
}
|
||
|
||
if (_editor->should_ripple_all()) {
|
||
_editor->get_markers_to_ripple (_primary->region()->playlist(), _primary->region()->position(), ripple_markers);
|
||
}
|
||
}
|
||
|
||
void
|
||
RegionMotionDrag::motion (GdkEvent* event, bool first_move)
|
||
{
|
||
double delta_layer = 0;
|
||
int delta_time_axis_view = 0;
|
||
int current_pointer_time_axis_view = -1;
|
||
|
||
assert (!_views.empty ());
|
||
|
||
/* Note: time axis views in this method are often expressed as an index into the _time_axis_views vector */
|
||
|
||
/* Find the TimeAxisView that the pointer is now over */
|
||
const double cur_y = current_pointer_y ();
|
||
pair<TimeAxisView*, double> const r = _editor->trackview_by_y_position (cur_y);
|
||
TimeAxisView* tv = r.first;
|
||
|
||
if (!tv && cur_y < 0) {
|
||
/* above trackview area, autoscroll hasn't moved us since last time, nothing to do */
|
||
return;
|
||
}
|
||
|
||
/* find drop-zone y-position */
|
||
Coord last_track_bottom_edge;
|
||
last_track_bottom_edge = 0;
|
||
for (std::vector<TimeAxisView*>::reverse_iterator t = _time_axis_views.rbegin(); t != _time_axis_views.rend(); ++t) {
|
||
if (!(*t)->hidden()) {
|
||
last_track_bottom_edge = (*t)->canvas_display()->canvas_origin ().y + (*t)->effective_height();
|
||
break;
|
||
}
|
||
}
|
||
|
||
if (tv && tv->view()) {
|
||
/* the mouse is over a track */
|
||
double layer = r.second;
|
||
|
||
if (first_move && tv->view()->layer_display() == Stacked) {
|
||
tv->view()->set_layer_display (Expanded);
|
||
}
|
||
|
||
/* Here's the current pointer position in terms of time axis view and layer */
|
||
current_pointer_time_axis_view = find_time_axis_view (tv);
|
||
assert(current_pointer_time_axis_view >= 0);
|
||
|
||
double const current_pointer_layer = tv->layer_display() == Overlaid ? 0 : layer;
|
||
|
||
/* Work out the change in y */
|
||
|
||
RouteTimeAxisView* rtv = dynamic_cast<RouteTimeAxisView*> (tv);
|
||
if (!rtv || !rtv->is_track()) {
|
||
/* ignore non-tracks early on. we can't move any regions on them */
|
||
} else if (_last_pointer_time_axis_view < 0) {
|
||
/* Was in the drop-zone, now over a track.
|
||
* Hence it must be an upward move (from the bottom)
|
||
*
|
||
* track_index is still -1, so delta must be set to
|
||
* move up the correct number of tracks from the bottom.
|
||
*
|
||
* This is necessary because steps may be skipped if
|
||
* the bottom-most track is not a valid target and/or
|
||
* if there are hidden tracks at the bottom.
|
||
* Hence the initial offset (_ddropzone) as well as the
|
||
* last valid pointer position (_pdropzone) need to be
|
||
* taken into account.
|
||
*/
|
||
delta_time_axis_view = current_pointer_time_axis_view - _time_axis_views.size () + _ddropzone - _pdropzone;
|
||
} else {
|
||
delta_time_axis_view = current_pointer_time_axis_view - _last_pointer_time_axis_view;
|
||
}
|
||
|
||
/* TODO needs adjustment per DraggingView,
|
||
*
|
||
* e.g. select one region on the top-layer of a track
|
||
* and one region which is at the bottom-layer of another track
|
||
* drag both.
|
||
*
|
||
* Indicated drop-zones and layering is wrong.
|
||
* and may infer additional layers on the target-track
|
||
* (depending how many layers the original track had).
|
||
*
|
||
* Or select two regions (different layers) on a same track,
|
||
* move across a non-layer track.. -> layering info is lost.
|
||
* on drop either of the regions may be on top.
|
||
*
|
||
* Proposed solution: screw it :) well,
|
||
* don't use delta_layer, use an absolute value
|
||
* 1) remember DraggingView's layer as float 0..1 [current layer / all layers of source]
|
||
* 2) calculate current mouse pos, y-pos inside track divided by height of mouse-over track.
|
||
* 3) iterate over all DraggingView, find the one that is over the track with most layers
|
||
* 4) proportionally scale layer to layers available on target
|
||
*/
|
||
delta_layer = current_pointer_layer - _last_pointer_layer;
|
||
|
||
}
|
||
/* for automation lanes, there is a TimeAxisView but no ->view()
|
||
* if (!tv) -> dropzone
|
||
*/
|
||
else if (!tv && cur_y >= 0 && _last_pointer_time_axis_view >= 0) {
|
||
/* Moving into the drop-zone.. */
|
||
delta_time_axis_view = _time_axis_views.size () - _last_pointer_time_axis_view;
|
||
/* delta_time_axis_view may not be sufficient to move into the DZ
|
||
* the mouse may enter it, but it may not be a valid move due to
|
||
* constraints.
|
||
*
|
||
* -> remember the delta needed to move into the dropzone
|
||
*/
|
||
_ddropzone = delta_time_axis_view;
|
||
/* ..but subtract hidden tracks (or routes) at the bottom.
|
||
* we silently move mover them
|
||
*/
|
||
_ddropzone -= apply_track_delta(_last_pointer_time_axis_view, delta_time_axis_view, 0, true)
|
||
- _time_axis_views.size();
|
||
}
|
||
else if (!tv && cur_y >= 0 && _last_pointer_time_axis_view < 0) {
|
||
/* move around inside the zone.
|
||
* This allows to move further down until all regions are in the zone.
|
||
*/
|
||
const double ptr_y = cur_y + _editor->get_trackview_group()->canvas_origin().y;
|
||
assert(ptr_y >= last_track_bottom_edge);
|
||
assert(_ddropzone > 0);
|
||
|
||
/* calculate mouse position in 'tracks' below last track. */
|
||
const double dzi_h = TimeAxisView::preset_height (HeightNormal);
|
||
uint32_t dzpos = _ddropzone + floor((1 + ptr_y - last_track_bottom_edge) / dzi_h);
|
||
|
||
if (dzpos > _pdropzone && _ndropzone < _ntracks) {
|
||
// move further down
|
||
delta_time_axis_view = dzpos - _pdropzone;
|
||
} else if (dzpos < _pdropzone && _ndropzone > 0) {
|
||
// move up inside the DZ
|
||
delta_time_axis_view = dzpos - _pdropzone;
|
||
}
|
||
}
|
||
|
||
/* Work out the change in x */
|
||
timepos_t pending_region_position;
|
||
double const x_delta = compute_x_delta (event, pending_region_position);
|
||
|
||
Temporal::Beats const last_pos_qn = _last_position.beats();
|
||
Temporal::Beats const qn_delta = pending_region_position.beats() - last_pos_qn;
|
||
|
||
_last_position = pending_region_position;
|
||
|
||
/* calculate hidden tracks in current y-axis delta */
|
||
int delta_skip = 0;
|
||
if (_last_pointer_time_axis_view < 0 && _pdropzone > 0) {
|
||
/* The mouse is more than one track below the dropzone.
|
||
* distance calculation is not needed (and would not work, either
|
||
* because the dropzone is "packed").
|
||
*
|
||
* Except when [partially] moving regions out of dropzone in a large step.
|
||
* (the mouse may or may not remain in the DZ)
|
||
* Hidden tracks at the bottom of the TAV need to be skipped.
|
||
*
|
||
* This also handles the case if the mouse entered the DZ
|
||
* in a large step (exessive delta), either due to fast-movement,
|
||
* autoscroll, laggy UI. _ddropzone copensates for that (see "move into dz" above)
|
||
*/
|
||
if (delta_time_axis_view < 0 && (int)_ddropzone - delta_time_axis_view >= (int)_pdropzone) {
|
||
const int dt = delta_time_axis_view + (int)_pdropzone - (int)_ddropzone;
|
||
assert(dt <= 0);
|
||
delta_skip = apply_track_delta(_time_axis_views.size(), dt, 0, true)
|
||
-_time_axis_views.size() - dt;
|
||
}
|
||
}
|
||
else if (_last_pointer_time_axis_view < 0) {
|
||
/* Moving out of the zone. Check for hidden tracks at the bottom. */
|
||
delta_skip = apply_track_delta(_time_axis_views.size(), delta_time_axis_view, 0, true)
|
||
-_time_axis_views.size() - delta_time_axis_view;
|
||
} else {
|
||
/* calculate hidden tracks that are skipped by the pointer movement */
|
||
delta_skip = apply_track_delta(_last_pointer_time_axis_view, delta_time_axis_view, 0, true)
|
||
- _last_pointer_time_axis_view
|
||
- delta_time_axis_view;
|
||
}
|
||
|
||
/* Verify change in y */
|
||
if (!y_movement_allowed (delta_time_axis_view, delta_layer, delta_skip)) {
|
||
/* this y movement is not allowed, so do no y movement this time */
|
||
delta_time_axis_view = 0;
|
||
delta_layer = 0;
|
||
delta_skip = 0;
|
||
}
|
||
|
||
if (x_delta == 0 && (tv && tv->view() && delta_time_axis_view == 0) && delta_layer == 0 && !first_move) {
|
||
/* haven't reached next snap point, and we're not switching
|
||
trackviews nor layers. nothing to do.
|
||
*/
|
||
return;
|
||
}
|
||
|
||
typedef map<boost::shared_ptr<Playlist>, double> PlaylistDropzoneMap;
|
||
PlaylistDropzoneMap playlist_dropzone_map;
|
||
_ndropzone = 0; // number of elements currently in the dropzone
|
||
|
||
if (first_move) {
|
||
/* sort views by time_axis.
|
||
* This retains track order in the dropzone, regardless
|
||
* of actual selection order
|
||
*/
|
||
_views.sort (DraggingViewSorter());
|
||
|
||
/* count number of distinct tracks of all regions
|
||
* being dragged, used for dropzone.
|
||
*/
|
||
int prev_track = -1;
|
||
for (list<DraggingView>::const_iterator i = _views.begin(); i != _views.end(); ++i) {
|
||
if ((int) i->time_axis_view != prev_track) {
|
||
prev_track = i->time_axis_view;
|
||
++_ntracks;
|
||
}
|
||
}
|
||
#ifndef NDEBUG
|
||
int spread =
|
||
_views.back().time_axis_view -
|
||
_views.front().time_axis_view;
|
||
|
||
spread -= apply_track_delta (_views.front().time_axis_view, spread, 0, true)
|
||
- _views.back().time_axis_view;
|
||
|
||
printf("Dragging region(s) from %d different track(s), max dist: %d\n", _ntracks, spread);
|
||
#endif
|
||
}
|
||
|
||
if (x_delta) {
|
||
for (vector<ArdourMarker*>::iterator m = ripple_markers.begin(); m != ripple_markers.end(); ++m) {
|
||
(*m)->the_item().move (Duple (x_delta, 0.));
|
||
}
|
||
}
|
||
|
||
for (list<DraggingView>::iterator i = _views.begin(); i != _views.end(); ++i) {
|
||
|
||
RegionView* rv = i->view;
|
||
double y_delta;
|
||
|
||
y_delta = 0;
|
||
|
||
if (rv->region()->locked() || (rv->region()->video_locked() && !_ignore_video_lock)) {
|
||
continue;
|
||
}
|
||
|
||
if (first_move) {
|
||
rv->drag_start ();
|
||
|
||
/* reparent the regionview into a group above all
|
||
* others
|
||
*/
|
||
|
||
ArdourCanvas::Item* rvg = rv->get_canvas_group();
|
||
Duple rv_canvas_offset = rvg->parent()->canvas_origin ();
|
||
Duple dmg_canvas_offset = _editor->_drag_motion_group->canvas_origin ();
|
||
rv->get_canvas_group()->reparent (_editor->_drag_motion_group);
|
||
/* move the item so that it continues to appear at the
|
||
same location now that its parent has changed.
|
||
*/
|
||
rvg->move (rv_canvas_offset - dmg_canvas_offset);
|
||
}
|
||
|
||
/* If we have moved tracks, we'll fudge the layer delta so that the
|
||
region gets moved back onto layer 0 on its new track; this avoids
|
||
confusion when dragging regions from non-zero layers onto different
|
||
tracks.
|
||
*/
|
||
double this_delta_layer = delta_layer;
|
||
if (delta_time_axis_view != 0) {
|
||
this_delta_layer = - i->layer;
|
||
}
|
||
|
||
int this_delta_time_axis_view = apply_track_delta(i->time_axis_view, delta_time_axis_view, delta_skip) - i->time_axis_view;
|
||
|
||
int track_index = i->time_axis_view + this_delta_time_axis_view;
|
||
assert(track_index >= 0);
|
||
|
||
if (track_index < 0 || track_index >= (int) _time_axis_views.size()) {
|
||
/* Track is in the Dropzone */
|
||
|
||
i->time_axis_view = track_index;
|
||
assert(i->time_axis_view >= (int) _time_axis_views.size());
|
||
if (cur_y >= 0) {
|
||
|
||
double yposition = 0;
|
||
PlaylistDropzoneMap::iterator pdz = playlist_dropzone_map.find (i->view->region()->playlist());
|
||
rv->set_height (TimeAxisView::preset_height (HeightNormal));
|
||
++_ndropzone;
|
||
|
||
/* store index of each new playlist as a negative count, starting at -1 */
|
||
|
||
if (pdz == playlist_dropzone_map.end()) {
|
||
/* compute where this new track (which doesn't exist yet) will live
|
||
on the y-axis.
|
||
*/
|
||
yposition = last_track_bottom_edge; /* where to place the top edge of the regionview */
|
||
|
||
/* How high is this region view ? */
|
||
|
||
boost::optional<ArdourCanvas::Rect> obbox = rv->get_canvas_group()->bounding_box ();
|
||
ArdourCanvas::Rect bbox;
|
||
|
||
if (obbox) {
|
||
bbox = obbox.get ();
|
||
}
|
||
|
||
last_track_bottom_edge += bbox.height();
|
||
|
||
playlist_dropzone_map.insert (make_pair (i->view->region()->playlist(), yposition));
|
||
|
||
} else {
|
||
yposition = pdz->second;
|
||
}
|
||
|
||
/* values are zero or negative, hence the use of min() */
|
||
y_delta = yposition - rv->get_canvas_group()->canvas_origin().y;
|
||
}
|
||
|
||
MidiRegionView* mrv = dynamic_cast<MidiRegionView*>(rv);
|
||
if (mrv) {
|
||
mrv->apply_note_range (60, 71, true);
|
||
}
|
||
} else {
|
||
|
||
/* The TimeAxisView that this region is now over */
|
||
TimeAxisView* current_tv = _time_axis_views[track_index];
|
||
|
||
/* Ensure it is moved from stacked -> expanded if appropriate */
|
||
if (current_tv->view()->layer_display() == Stacked) {
|
||
current_tv->view()->set_layer_display (Expanded);
|
||
}
|
||
|
||
/* We're only allowed to go -ve in layer on Expanded views */
|
||
if (current_tv->view()->layer_display() != Expanded && (i->layer + this_delta_layer) < 0) {
|
||
this_delta_layer = - i->layer;
|
||
}
|
||
|
||
/* Set height */
|
||
rv->set_height (current_tv->view()->child_height ());
|
||
|
||
/* Update show/hidden status as the region view may have come from a hidden track,
|
||
or have moved to one.
|
||
*/
|
||
if (current_tv->hidden ()) {
|
||
rv->get_canvas_group()->hide ();
|
||
} else {
|
||
rv->get_canvas_group()->show ();
|
||
}
|
||
|
||
/* Update the DraggingView */
|
||
i->time_axis_view = track_index;
|
||
i->layer += this_delta_layer;
|
||
|
||
Duple track_origin;
|
||
|
||
/* Get the y coordinate of the top of the track that this region is now over */
|
||
track_origin = current_tv->canvas_display()->item_to_canvas (track_origin);
|
||
|
||
/* And adjust for the layer that it should be on */
|
||
StreamView* cv = current_tv->view ();
|
||
switch (cv->layer_display ()) {
|
||
case Overlaid:
|
||
break;
|
||
case Stacked:
|
||
track_origin.y += (cv->layers() - i->layer - 1) * cv->child_height ();
|
||
break;
|
||
case Expanded:
|
||
track_origin.y += (cv->layers() - i->layer - 0.5) * 2 * cv->child_height ();
|
||
break;
|
||
}
|
||
|
||
/* need to get the parent of the regionview
|
||
* canvas group and get its position in
|
||
* equivalent coordinate space as the trackview
|
||
* we are now dragging over.
|
||
*/
|
||
|
||
y_delta = track_origin.y - rv->get_canvas_group()->canvas_origin().y;
|
||
|
||
MidiRegionView* mrv = dynamic_cast<MidiRegionView*>(rv);
|
||
if (mrv) {
|
||
MidiStreamView* msv;
|
||
if ((msv = dynamic_cast <MidiStreamView*> (current_tv->view())) != 0) {
|
||
mrv->apply_note_range (msv->lowest_note(), msv->highest_note(), true);
|
||
}
|
||
}
|
||
}
|
||
|
||
/* Now move the region view */
|
||
if (rv->region()->position_time_domain() == Temporal::BeatTime) {
|
||
Temporal::Beats const last_qn = rv->get_position().beats();
|
||
rv->set_position (timepos_t (last_qn + qn_delta), 0);
|
||
rv->move (0, y_delta);
|
||
} else {
|
||
rv->move (x_delta, y_delta);
|
||
}
|
||
} /* foreach region */
|
||
|
||
_total_x_delta += x_delta;
|
||
|
||
if (x_delta != 0) {
|
||
show_verbose_cursor_time (_last_position);
|
||
show_view_preview (_last_position + _video_offset);
|
||
}
|
||
|
||
/* keep track of pointer movement */
|
||
if (tv) {
|
||
/* the pointer is currently over a time axis view */
|
||
|
||
if (_last_pointer_time_axis_view < 0) {
|
||
/* last motion event was not over a time axis view
|
||
* or last y-movement out of the dropzone was not valid
|
||
*/
|
||
int dtz = 0;
|
||
if (delta_time_axis_view < 0) {
|
||
/* in the drop zone, moving up */
|
||
|
||
/* _pdropzone is the last known pointer y-axis position inside the DZ.
|
||
* We do not use negative _last_pointer_time_axis_view because
|
||
* the dropzone is "packed" (the actual track offset is ignored)
|
||
*
|
||
* As opposed to the actual number
|
||
* of elements in the dropzone (_ndropzone)
|
||
* _pdropzone is not constrained. This is necessary
|
||
* to allow moving multiple regions with y-distance
|
||
* into the DZ.
|
||
*
|
||
* There can be 0 elements in the dropzone,
|
||
* even though the drag-pointer is inside the DZ.
|
||
*
|
||
* example:
|
||
* [ Audio-track, Midi-track, Audio-track, DZ ]
|
||
* move regions from both audio tracks at the same time into the
|
||
* DZ by grabbing the region in the bottom track.
|
||
*/
|
||
assert(current_pointer_time_axis_view >= 0);
|
||
dtz = std::min((int)_pdropzone, (int)_ddropzone - delta_time_axis_view);
|
||
_pdropzone -= dtz;
|
||
}
|
||
|
||
/* only move out of the zone if the movement is OK */
|
||
if (_pdropzone == 0 && delta_time_axis_view != 0) {
|
||
assert(delta_time_axis_view < 0);
|
||
_last_pointer_time_axis_view = current_pointer_time_axis_view;
|
||
/* if all logic and maths are correct, there is no need to assign the 'current' pointer.
|
||
* the current position can be calculated as follows:
|
||
*/
|
||
// a well placed oofus attack can still throw this off.
|
||
// likely auto-scroll related, printf() debugging may tell, commented out for now.
|
||
//assert (current_pointer_time_axis_view == _time_axis_views.size() - dtz + _ddropzone + delta_time_axis_view);
|
||
}
|
||
} else {
|
||
/* last motion event was also over a time axis view */
|
||
_last_pointer_time_axis_view += delta_time_axis_view;
|
||
assert(_last_pointer_time_axis_view >= 0);
|
||
}
|
||
|
||
} else {
|
||
|
||
/* the pointer is not over a time axis view */
|
||
assert ((delta_time_axis_view > 0) || (((int)_pdropzone) >= (delta_skip - delta_time_axis_view)));
|
||
_pdropzone += delta_time_axis_view - delta_skip;
|
||
_last_pointer_time_axis_view = -1; // <0 : we're in the zone, value does not matter.
|
||
}
|
||
|
||
_last_pointer_layer += delta_layer;
|
||
}
|
||
|
||
void
|
||
RegionMoveDrag::motion (GdkEvent* event, bool first_move)
|
||
{
|
||
if (first_move && _editor->should_ripple() && !_copy) {
|
||
collect_ripple_views ();
|
||
}
|
||
|
||
if (_copy && first_move) {
|
||
if (_x_constrained) {
|
||
_editor->begin_reversible_command (Operations::fixed_time_region_copy);
|
||
} else {
|
||
_editor->begin_reversible_command (Operations::region_copy);
|
||
}
|
||
|
||
/* duplicate the regionview(s) and region(s) */
|
||
|
||
list<DraggingView> new_regionviews;
|
||
|
||
for (list<DraggingView>::const_iterator i = _views.begin(); i != _views.end(); ++i) {
|
||
|
||
RegionView* rv = i->view;
|
||
AudioRegionView* arv = dynamic_cast<AudioRegionView*>(rv);
|
||
MidiRegionView* mrv = dynamic_cast<MidiRegionView*>(rv);
|
||
|
||
const boost::shared_ptr<const Region> original = rv->region();
|
||
boost::shared_ptr<Region> region_copy;
|
||
|
||
region_copy = RegionFactory::create (original, true);
|
||
|
||
/* need to set this so that the drop zone code can work. This doesn't
|
||
actually put the region into the playlist, but just sets a weak pointer
|
||
to it.
|
||
*/
|
||
region_copy->set_playlist (original->playlist());
|
||
|
||
RegionView* nrv;
|
||
if (arv) {
|
||
boost::shared_ptr<AudioRegion> audioregion_copy
|
||
= boost::dynamic_pointer_cast<AudioRegion>(region_copy);
|
||
|
||
nrv = new AudioRegionView (*arv, audioregion_copy);
|
||
} else if (mrv) {
|
||
boost::shared_ptr<MidiRegion> midiregion_copy
|
||
= boost::dynamic_pointer_cast<MidiRegion>(region_copy);
|
||
nrv = new MidiRegionView (*mrv, midiregion_copy);
|
||
} else {
|
||
continue;
|
||
}
|
||
|
||
nrv->get_canvas_group()->show ();
|
||
new_regionviews.push_back (DraggingView (nrv, this, i->initial_time_axis_view));
|
||
|
||
/* swap _primary to the copy */
|
||
|
||
if (rv == _primary) {
|
||
_primary = nrv;
|
||
}
|
||
|
||
/* ..and deselect the one we copied */
|
||
|
||
rv->set_selected (false);
|
||
}
|
||
|
||
if (!new_regionviews.empty()) {
|
||
|
||
/* reflect the fact that we are dragging the copies */
|
||
|
||
_views = new_regionviews;
|
||
|
||
swap_grab (new_regionviews.front().view->get_canvas_group (), 0, event ? event->motion.time : 0);
|
||
}
|
||
|
||
} else if (!_copy && first_move) {
|
||
if (_x_constrained) {
|
||
_editor->begin_reversible_command (_("fixed time region drag"));
|
||
} else {
|
||
_editor->begin_reversible_command (Operations::region_drag);
|
||
}
|
||
}
|
||
RegionMotionDrag::motion (event, first_move);
|
||
}
|
||
|
||
void
|
||
RegionMotionDrag::finished (GdkEvent *, bool)
|
||
{
|
||
for (vector<TimeAxisView*>::iterator i = _time_axis_views.begin(); i != _time_axis_views.end(); ++i) {
|
||
if (!(*i)->view()) {
|
||
continue;
|
||
}
|
||
|
||
if ((*i)->view()->layer_display() == Expanded) {
|
||
(*i)->view()->set_layer_display (Stacked);
|
||
}
|
||
}
|
||
}
|
||
|
||
void
|
||
RegionMoveDrag::finished (GdkEvent* ev, bool movement_occurred)
|
||
{
|
||
RegionMotionDrag::finished (ev, movement_occurred);
|
||
|
||
if (!movement_occurred) {
|
||
|
||
/* just a click */
|
||
|
||
if (was_double_click() && !_views.empty()) {
|
||
DraggingView dv = _views.front();
|
||
_editor->edit_region (dv.view);
|
||
}
|
||
|
||
return;
|
||
}
|
||
|
||
assert (!_views.empty ());
|
||
|
||
/* We might have hidden region views so that they weren't visible during the drag
|
||
(when they have been reparented). Now everything can be shown again, as region
|
||
views are back in their track parent groups.
|
||
*/
|
||
for (list<DraggingView>::iterator i = _views.begin(); i != _views.end(); ++i) {
|
||
i->view->get_canvas_group()->show ();
|
||
}
|
||
|
||
bool const changed_position = (_last_position != _primary->region()->position());
|
||
bool changed_tracks;
|
||
|
||
if (_views.front().time_axis_view >= (int) _time_axis_views.size()) {
|
||
/* in the drop zone */
|
||
changed_tracks = true;
|
||
} else {
|
||
|
||
if (_views.front().time_axis_view < 0) {
|
||
#warning paul fix this code
|
||
/* XXX this test is nonsensical. See 0aef128207 and
|
||
#8672 for the origin of this and related code
|
||
*/
|
||
if (&_views.front().view->get_time_axis_view()) {
|
||
changed_tracks = true;
|
||
} else {
|
||
changed_tracks = false;
|
||
}
|
||
} else {
|
||
changed_tracks = (_time_axis_views[_views.front().time_axis_view] != &_views.front().view->get_time_axis_view());
|
||
}
|
||
}
|
||
|
||
if (_copy) {
|
||
|
||
finished_copy (
|
||
changed_position,
|
||
changed_tracks,
|
||
_last_position,
|
||
ev->button.state
|
||
);
|
||
|
||
} else {
|
||
|
||
finished_no_copy (
|
||
changed_position,
|
||
changed_tracks,
|
||
_last_position,
|
||
ev->button.state
|
||
);
|
||
|
||
}
|
||
}
|
||
|
||
RouteTimeAxisView*
|
||
RegionMoveDrag::create_destination_time_axis (boost::shared_ptr<Region> region, TimeAxisView* original)
|
||
{
|
||
if (!ARDOUR_UI_UTILS::engine_is_running ()) {
|
||
return NULL;
|
||
}
|
||
|
||
/* Add a new track of the correct type, and return the RouteTimeAxisView that is created to display the
|
||
new track.
|
||
*/
|
||
TimeAxisView* tav = 0;
|
||
try {
|
||
if (boost::dynamic_pointer_cast<AudioRegion> (region)) {
|
||
list<boost::shared_ptr<AudioTrack> > audio_tracks;
|
||
uint32_t output_chan = region->sources().size();
|
||
if ((Config->get_output_auto_connect() & AutoConnectMaster) && _editor->session()->master_out()) {
|
||
output_chan = _editor->session()->master_out()->n_inputs().n_audio();
|
||
}
|
||
audio_tracks = _editor->session()->new_audio_track (region->sources().size(), output_chan, 0, 1, region->name(), PresentationInfo::max_order);
|
||
tav =_editor->time_axis_view_from_stripable (audio_tracks.front());
|
||
} else {
|
||
ChanCount one_midi_port (DataType::MIDI, 1);
|
||
list<boost::shared_ptr<MidiTrack> > midi_tracks;
|
||
midi_tracks = _editor->session()->new_midi_track (one_midi_port, one_midi_port,
|
||
Config->get_strict_io () || Profile->get_mixbus (),
|
||
boost::shared_ptr<ARDOUR::PluginInfo>(),
|
||
(ARDOUR::Plugin::PresetRecord*) 0,
|
||
(ARDOUR::RouteGroup*) 0, 1, region->name(), PresentationInfo::max_order, Normal, true);
|
||
tav = _editor->time_axis_view_from_stripable (midi_tracks.front());
|
||
}
|
||
|
||
if (tav) {
|
||
tav->set_height (original->current_height());
|
||
}
|
||
} catch (...) {
|
||
error << _("Could not create new track after region placed in the drop zone") << endmsg;
|
||
}
|
||
|
||
return dynamic_cast<RouteTimeAxisView*> (tav);
|
||
}
|
||
|
||
void
|
||
RegionMoveDrag::clear_draggingview_list ()
|
||
{
|
||
for (list<DraggingView>::const_iterator i = _views.begin(); i != _views.end();) {
|
||
list<DraggingView>::const_iterator next = i;
|
||
++next;
|
||
delete i->view;
|
||
i = next;
|
||
}
|
||
_views.clear();
|
||
}
|
||
|
||
void
|
||
RegionMoveDrag::finished_copy (bool const changed_position, bool const changed_tracks, timepos_t const & last_position, int32_t const ev_state)
|
||
{
|
||
RegionSelection new_views;
|
||
PlaylistSet modified_playlists;
|
||
RouteTimeAxisView* new_time_axis_view = 0;
|
||
|
||
timecnt_t const drag_delta = _last_position.distance (_primary->region()->position());
|
||
RegionList ripple_exclude;
|
||
|
||
/*x_contrained on the same track: this will just make a duplicate region in the same place: abort the operation */
|
||
if (_x_constrained && !changed_tracks) {
|
||
clear_draggingview_list();
|
||
_editor->abort_reversible_command ();
|
||
return;
|
||
}
|
||
|
||
typedef map<boost::shared_ptr<Playlist>, RouteTimeAxisView*> PlaylistMapping;
|
||
PlaylistMapping playlist_mapping;
|
||
|
||
/* determine boundaries of dragged regions, across all playlists */
|
||
timepos_t extent_min = timepos_t::max(_primary->region()->position().time_domain());
|
||
timepos_t extent_max;
|
||
|
||
/* insert the regions into their (potentially) new (or existing) playlists */
|
||
for (list<DraggingView>::const_iterator i = _views.begin(); i != _views.end(); ++i) {
|
||
|
||
RouteTimeAxisView* dest_rtv = 0;
|
||
|
||
if (i->view->region()->locked() || (i->view->region()->video_locked() && !_ignore_video_lock)) {
|
||
continue;
|
||
}
|
||
|
||
timepos_t where;
|
||
|
||
if (changed_position && !_x_constrained) {
|
||
where = timepos_t (i->view->region()->position().earlier (drag_delta));
|
||
} else {
|
||
where = timepos_t (i->view->region()->position());
|
||
}
|
||
|
||
/* compute full extent of regions that we're going to insert */
|
||
|
||
if (where < extent_min) {
|
||
extent_min = where;
|
||
}
|
||
|
||
if (where + i->view->region()->length() > extent_max) {
|
||
extent_max = where + i->view->region()->length();
|
||
}
|
||
|
||
if (i->time_axis_view < 0 || i->time_axis_view >= (int)_time_axis_views.size()) {
|
||
/* dragged to drop zone */
|
||
|
||
PlaylistMapping::iterator pm;
|
||
|
||
if ((pm = playlist_mapping.find (i->view->region()->playlist())) == playlist_mapping.end()) {
|
||
/* first region from this original playlist: create a new track */
|
||
new_time_axis_view = create_destination_time_axis (i->view->region(), i->initial_time_axis_view);
|
||
if(!new_time_axis_view) {
|
||
Drag::abort();
|
||
return;
|
||
}
|
||
playlist_mapping.insert (make_pair (i->view->region()->playlist(), new_time_axis_view));
|
||
dest_rtv = new_time_axis_view;
|
||
} else {
|
||
/* we already created a new track for regions from this playlist, use it */
|
||
dest_rtv = pm->second;
|
||
}
|
||
} else {
|
||
/* destination time axis view is the one we dragged to */
|
||
dest_rtv = dynamic_cast<RouteTimeAxisView*> (_time_axis_views[i->time_axis_view]);
|
||
}
|
||
|
||
if (dest_rtv != 0) {
|
||
RegionView* new_view;
|
||
if (i->view == _primary && !_x_constrained) {
|
||
new_view = insert_region_into_playlist (i->view->region(), dest_rtv, i->layer, last_position,
|
||
modified_playlists, true);
|
||
} else {
|
||
if (i->view->region()->position_time_domain() == Temporal::AudioTime) {
|
||
new_view = insert_region_into_playlist (i->view->region(), dest_rtv, i->layer, where,
|
||
modified_playlists);
|
||
} else {
|
||
new_view = insert_region_into_playlist (i->view->region(), dest_rtv, i->layer, where,
|
||
modified_playlists, true);
|
||
}
|
||
}
|
||
|
||
if (new_view != 0) {
|
||
new_views.push_back (new_view);
|
||
ripple_exclude.push_back (new_view->region());
|
||
}
|
||
}
|
||
}
|
||
|
||
/* in the past this was done in the main iterator loop; no need */
|
||
clear_draggingview_list();
|
||
|
||
for (PlaylistSet::iterator p = modified_playlists.begin(); p != modified_playlists.end(); ++p) {
|
||
if (_editor->should_ripple()) {
|
||
(*p)->ripple (extent_min, extent_min.distance (extent_max), &ripple_exclude);
|
||
}
|
||
(*p)->rdiff_and_add_command (_editor->session());
|
||
}
|
||
|
||
/* Ripple marks & ranges if appropriate */
|
||
|
||
if (_editor->should_ripple_all()) {
|
||
_editor->ripple_marks (_primary->region()->playlist(), extent_min, extent_min.distance (extent_max));
|
||
}
|
||
|
||
/* If we've created new regions either by copying or moving
|
||
to a new track, we want to replace the old selection with the new ones
|
||
*/
|
||
|
||
if (new_views.size() > 0) {
|
||
_editor->selection->set (new_views);
|
||
}
|
||
|
||
_editor->commit_reversible_command ();
|
||
}
|
||
|
||
void
|
||
RegionMoveDrag::finished_no_copy (
|
||
bool const changed_position,
|
||
bool const changed_tracks,
|
||
timepos_t const & last_position,
|
||
int32_t const ev_state
|
||
)
|
||
{
|
||
RegionSelection new_views;
|
||
PlaylistSet modified_playlists;
|
||
PlaylistSet frozen_playlists;
|
||
set<RouteTimeAxisView*> views_to_update;
|
||
RouteTimeAxisView* new_time_axis_view = 0;
|
||
|
||
timecnt_t const drag_delta = last_position.distance (_primary->region()->position());
|
||
RegionList ripple_exclude;
|
||
|
||
typedef map<boost::shared_ptr<Playlist>, RouteTimeAxisView*> PlaylistMapping;
|
||
PlaylistMapping playlist_mapping;
|
||
|
||
/* determine earliest position affected by the drag, across all playlists */
|
||
timepos_t extent_min = timepos_t::max (Temporal::AudioTime); /* NUTEMPO fix domain */
|
||
|
||
std::set<boost::shared_ptr<const Region> > uniq;
|
||
for (list<DraggingView>::const_iterator i = _views.begin(); i != _views.end(); ) {
|
||
|
||
RegionView* rv = i->view;
|
||
RouteTimeAxisView* dest_rtv = 0;
|
||
|
||
if (rv->region()->locked() || (rv->region()->video_locked() && !_ignore_video_lock)) {
|
||
++i;
|
||
continue;
|
||
}
|
||
|
||
if (uniq.find (rv->region()) != uniq.end()) {
|
||
/* prevent duplicate moves when selecting regions from shared playlists */
|
||
++i;
|
||
continue;
|
||
}
|
||
uniq.insert(rv->region());
|
||
|
||
if (i->time_axis_view < 0 || i->time_axis_view >= (int)_time_axis_views.size()) {
|
||
/* dragged to drop zone */
|
||
|
||
PlaylistMapping::iterator pm;
|
||
|
||
if ((pm = playlist_mapping.find (i->view->region()->playlist())) == playlist_mapping.end()) {
|
||
/* first region from this original playlist: create a new track */
|
||
new_time_axis_view = create_destination_time_axis (i->view->region(), i->initial_time_axis_view);
|
||
if(!new_time_axis_view) { // New track creation failed
|
||
Drag::abort();
|
||
return;
|
||
}
|
||
playlist_mapping.insert (make_pair (i->view->region()->playlist(), new_time_axis_view));
|
||
dest_rtv = new_time_axis_view;
|
||
} else {
|
||
/* we already created a new track for regions from this playlist, use it */
|
||
dest_rtv = pm->second;
|
||
}
|
||
|
||
} else {
|
||
/* destination time axis view is the one we dragged to */
|
||
dest_rtv = dynamic_cast<RouteTimeAxisView*> (_time_axis_views[i->time_axis_view]);
|
||
}
|
||
|
||
assert (dest_rtv);
|
||
|
||
double const dest_layer = i->layer;
|
||
|
||
views_to_update.insert (dest_rtv);
|
||
|
||
timepos_t where;
|
||
|
||
if (changed_position && !_x_constrained) {
|
||
where = rv->region()->position().earlier (drag_delta);
|
||
} else {
|
||
where = rv->region()->position();
|
||
}
|
||
|
||
/* compute full extent of regions that we're going to insert */
|
||
|
||
if (rv->region()->position() < extent_min) {
|
||
extent_min = rv->region()->position ();
|
||
}
|
||
|
||
if (changed_tracks) {
|
||
|
||
/* insert into new playlist */
|
||
RegionView* new_view;
|
||
if (rv == _primary && !_x_constrained) {
|
||
new_view = insert_region_into_playlist (
|
||
RegionFactory::create (rv->region (), true), dest_rtv, dest_layer, last_position,modified_playlists, true);
|
||
} else {
|
||
if (rv->region()->position_time_domain() == Temporal::AudioTime) {
|
||
|
||
new_view = insert_region_into_playlist (
|
||
RegionFactory::create (rv->region (), true), dest_rtv, dest_layer, where, modified_playlists);
|
||
} else {
|
||
new_view = insert_region_into_playlist (
|
||
RegionFactory::create (rv->region (), true), dest_rtv, dest_layer, where, modified_playlists, true);
|
||
}
|
||
}
|
||
|
||
if (new_view == 0) {
|
||
++i;
|
||
continue;
|
||
}
|
||
|
||
new_views.push_back (new_view);
|
||
|
||
/* remove from old playlist */
|
||
|
||
/* the region that used to be in the old playlist is not
|
||
moved to the new one - we use a copy of it. as a result,
|
||
any existing editor for the region should no longer be
|
||
visible.
|
||
*/
|
||
rv->hide_region_editor();
|
||
|
||
|
||
remove_region_from_playlist (rv->region(), i->initial_playlist, modified_playlists);
|
||
|
||
} else {
|
||
|
||
boost::shared_ptr<Playlist> playlist = dest_rtv->playlist();
|
||
|
||
/* this movement may result in a crossfade being modified, or a layering change,
|
||
so we need to get undo data from the playlist as well as the region.
|
||
*/
|
||
|
||
pair<PlaylistSet::iterator, bool> r = modified_playlists.insert (playlist);
|
||
if (r.second) {
|
||
playlist->clear_changes ();
|
||
}
|
||
|
||
rv->region()->clear_changes ();
|
||
|
||
/*
|
||
motion on the same track. plonk the previously reparented region
|
||
back to its original canvas group (its streamview).
|
||
No need to do anything for copies as they are fake regions which will be deleted.
|
||
*/
|
||
|
||
rv->get_canvas_group()->reparent (dest_rtv->view()->canvas_item());
|
||
rv->get_canvas_group()->set_y_position (i->initial_y);
|
||
rv->drag_end ();
|
||
|
||
/* just change the model */
|
||
if (dest_rtv->view()->layer_display() == Stacked || dest_rtv->view()->layer_display() == Expanded) {
|
||
playlist->set_layer (rv->region(), dest_layer);
|
||
}
|
||
|
||
/* freeze playlist to avoid lots of relayering in the case of a multi-region drag */
|
||
|
||
r = frozen_playlists.insert (playlist);
|
||
|
||
if (r.second) {
|
||
playlist->freeze ();
|
||
}
|
||
|
||
rv->region()->set_position (where);
|
||
|
||
_editor->session()->add_command (new StatefulDiffCommand (rv->region()));
|
||
}
|
||
|
||
// ripple_exclude.push_back (i->view->region());
|
||
|
||
if (changed_tracks) {
|
||
|
||
/* OK, this is where it gets tricky. If the playlist was being used by >1 tracks, and the region
|
||
was selected in all of them, then removing it from a playlist will have removed all
|
||
trace of it from _views (i.e. there were N regions selected, we removed 1,
|
||
but since its the same playlist for N tracks, all N tracks updated themselves, removed the
|
||
corresponding regionview, and _views is now empty).
|
||
|
||
This could have invalidated any and all iterators into _views.
|
||
|
||
The heuristic we use here is: if the region selection is empty, break out of the loop
|
||
here. if the region selection is not empty, then restart the loop because we know that
|
||
we must have removed at least the region(view) we've just been working on as well as any
|
||
that we processed on previous iterations.
|
||
|
||
EXCEPT .... if we are doing a copy drag, then _views hasn't been modified and
|
||
we can just iterate.
|
||
*/
|
||
|
||
|
||
if (_views.empty()) {
|
||
break;
|
||
} else {
|
||
i = _views.begin();
|
||
}
|
||
|
||
} else {
|
||
++i;
|
||
}
|
||
}
|
||
|
||
for (set<boost::shared_ptr<Playlist> >::iterator p = frozen_playlists.begin(); p != frozen_playlists.end(); ++p) {
|
||
(*p)->thaw();
|
||
}
|
||
|
||
if (_editor->should_ripple_all()) {
|
||
_editor->ripple_marks (_primary->region()->playlist(), extent_min, -drag_delta);
|
||
}
|
||
|
||
/* If we've created new regions either by copying or moving
|
||
to a new track, we want to replace the old selection with the new ones
|
||
*/
|
||
|
||
if (new_views.size() > 0) {
|
||
_editor->selection->set (new_views);
|
||
}
|
||
|
||
/* write commands for the accumulated diffs for all our modified playlists */
|
||
add_stateful_diff_commands_for_playlists (modified_playlists);
|
||
|
||
_editor->commit_reversible_command ();
|
||
|
||
/* We have futzed with the layering of canvas items on our streamviews.
|
||
If any region changed layer, this will have resulted in the stream
|
||
views being asked to set up their region views, and all will be well.
|
||
If not, we might now have badly-ordered region views. Ask the StreamViews
|
||
involved to sort themselves out, just in case.
|
||
*/
|
||
|
||
for (set<RouteTimeAxisView*>::iterator i = views_to_update.begin(); i != views_to_update.end(); ++i) {
|
||
(*i)->view()->playlist_layered ((*i)->track ());
|
||
}
|
||
}
|
||
|
||
/** Remove a region from a playlist, clearing the diff history of the playlist first if necessary.
|
||
* @param region Region to remove.
|
||
* @param playlist playlist To remove from.
|
||
* @param modified_playlists The playlist will be added to this if it is not there already; used to ensure
|
||
* that clear_changes () is only called once per playlist.
|
||
*/
|
||
void
|
||
RegionMoveDrag::remove_region_from_playlist (
|
||
boost::shared_ptr<Region> region,
|
||
boost::shared_ptr<Playlist> playlist,
|
||
PlaylistSet& modified_playlists
|
||
)
|
||
{
|
||
pair<set<boost::shared_ptr<Playlist> >::iterator, bool> r = modified_playlists.insert (playlist);
|
||
|
||
if (r.second) {
|
||
playlist->clear_changes ();
|
||
}
|
||
|
||
/* XX NEED TO RIPPLE */
|
||
|
||
playlist->remove_region (region);
|
||
}
|
||
|
||
|
||
/** Insert a region into a playlist, handling the recovery of the resulting new RegionView, and
|
||
* clearing the playlist's diff history first if necessary.
|
||
* @param region Region to insert.
|
||
* @param dest_rtv Destination RouteTimeAxisView.
|
||
* @param dest_layer Destination layer.
|
||
* @param where Destination position.
|
||
* @param modified_playlists The playlist will be added to this if it is not there already; used to ensure
|
||
* that clear_changes () is only called once per playlist.
|
||
* @return New RegionView, or 0 if no insert was performed.
|
||
*/
|
||
RegionView *
|
||
RegionMoveDrag::insert_region_into_playlist (
|
||
boost::shared_ptr<Region> region,
|
||
RouteTimeAxisView* dest_rtv,
|
||
layer_t dest_layer,
|
||
timepos_t const & where,
|
||
PlaylistSet& modified_playlists,
|
||
bool for_music
|
||
)
|
||
{
|
||
boost::shared_ptr<Playlist> dest_playlist = dest_rtv->playlist ();
|
||
if (!dest_playlist) {
|
||
return 0;
|
||
}
|
||
|
||
/* arrange to collect the new region view that will be created as a result of our playlist insertion */
|
||
_new_region_view = 0;
|
||
sigc::connection c = dest_rtv->view()->RegionViewAdded.connect (sigc::mem_fun (*this, &RegionMoveDrag::collect_new_region_view));
|
||
|
||
/* clear history for the playlist we are about to insert to, provided we haven't already done so */
|
||
pair<PlaylistSet::iterator, bool> r = modified_playlists.insert (dest_playlist);
|
||
|
||
if (r.second) {
|
||
dest_playlist->clear_changes ();
|
||
dest_playlist->clear_owned_changes ();
|
||
/* cannot freeze because we need the new region announcements */
|
||
}
|
||
|
||
dest_playlist->add_region (region, where, 1.0, for_music);
|
||
|
||
if (dest_rtv->view()->layer_display() == Stacked || dest_rtv->view()->layer_display() == Expanded) {
|
||
dest_playlist->set_layer (region, dest_layer);
|
||
}
|
||
|
||
c.disconnect ();
|
||
|
||
assert (_new_region_view);
|
||
|
||
return _new_region_view;
|
||
}
|
||
|
||
void
|
||
RegionMoveDrag::collect_new_region_view (RegionView* rv)
|
||
{
|
||
_new_region_view = rv;
|
||
}
|
||
|
||
void
|
||
RegionMoveDrag::aborted (bool movement_occurred)
|
||
{
|
||
if (_copy) {
|
||
clear_draggingview_list();
|
||
} else {
|
||
RegionMotionDrag::aborted (movement_occurred);
|
||
}
|
||
}
|
||
|
||
void
|
||
RegionMotionDrag::aborted (bool)
|
||
{
|
||
for (vector<TimeAxisView*>::iterator i = _time_axis_views.begin(); i != _time_axis_views.end(); ++i) {
|
||
|
||
StreamView* sview = (*i)->view();
|
||
|
||
if (sview) {
|
||
if (sview->layer_display() == Expanded) {
|
||
sview->set_layer_display (Stacked);
|
||
}
|
||
}
|
||
}
|
||
|
||
for (list<DraggingView>::const_iterator i = _views.begin(); i != _views.end(); ++i) {
|
||
RegionView* rv = i->view;
|
||
TimeAxisView* tv = &(rv->get_time_axis_view ());
|
||
RouteTimeAxisView* rtv = dynamic_cast<RouteTimeAxisView*> (tv);
|
||
assert (rtv);
|
||
rv->get_canvas_group()->reparent (rtv->view()->canvas_item());
|
||
rv->get_canvas_group()->set_y_position (0);
|
||
rv->drag_end ();
|
||
rv->move (-_total_x_delta, 0);
|
||
rv->set_height (rtv->view()->child_height ());
|
||
}
|
||
|
||
for (vector<ArdourMarker*>::iterator m = ripple_markers.begin(); m != ripple_markers.end(); ++m) {
|
||
(*m)->the_item().move (Duple (-_total_x_delta, 0.));
|
||
}
|
||
}
|
||
|
||
/** @param b true to brush, otherwise false.
|
||
* @param c true to make copies of the regions being moved, otherwise false.
|
||
*/
|
||
RegionMoveDrag::RegionMoveDrag (Editor* e, ArdourCanvas::Item* i, RegionView* p, list<RegionView*> const & v, bool c, TimeDomain td)
|
||
: RegionMotionDrag (e, i, p, v, td)
|
||
, _copy (c)
|
||
, _new_region_view (0)
|
||
{
|
||
DEBUG_TRACE (DEBUG::Drags, "New RegionMoveDrag\n");
|
||
|
||
_last_position = _primary->region()->position();
|
||
}
|
||
|
||
void
|
||
RegionMoveDrag::setup_pointer_offset ()
|
||
{
|
||
_pointer_offset = timecnt_t (_last_position.distance (raw_grab_time()), _last_position);
|
||
}
|
||
|
||
RegionInsertDrag::RegionInsertDrag (Editor* e, boost::shared_ptr<Region> r, RouteTimeAxisView* v, timepos_t const & pos, Temporal::TimeDomain td)
|
||
: RegionMotionDrag (e, 0, 0, list<RegionView*> (), td)
|
||
{
|
||
DEBUG_TRACE (DEBUG::Drags, "New RegionInsertDrag\n");
|
||
|
||
assert ((boost::dynamic_pointer_cast<AudioRegion> (r) && dynamic_cast<AudioTimeAxisView*> (v)) ||
|
||
(boost::dynamic_pointer_cast<MidiRegion> (r) && dynamic_cast<MidiTimeAxisView*> (v)));
|
||
|
||
_primary = v->view()->create_region_view (r, false, false);
|
||
|
||
_primary->get_canvas_group()->show ();
|
||
_primary->set_position (pos, 0);
|
||
_views.push_back (DraggingView (_primary, this, v));
|
||
|
||
_last_position = pos;
|
||
|
||
_item = _primary->get_canvas_group ();
|
||
}
|
||
|
||
void
|
||
RegionInsertDrag::finished (GdkEvent * event, bool)
|
||
{
|
||
int pos = _views.front().time_axis_view;
|
||
assert(pos >= 0 && pos < (int)_time_axis_views.size());
|
||
|
||
RouteTimeAxisView* dest_rtv = dynamic_cast<RouteTimeAxisView*> (_time_axis_views[pos]);
|
||
|
||
_primary->get_canvas_group()->reparent (dest_rtv->view()->canvas_item());
|
||
_primary->get_canvas_group()->set_y_position (0);
|
||
|
||
boost::shared_ptr<Playlist> playlist = dest_rtv->playlist();
|
||
|
||
_editor->begin_reversible_command (Operations::insert_region);
|
||
playlist->clear_changes ();
|
||
playlist->clear_owned_changes ();
|
||
_editor->snap_to_with_modifier (_last_position, event);
|
||
|
||
playlist->add_region (_primary->region (), _last_position, 1.0, false);
|
||
|
||
if (_editor->should_ripple()) {
|
||
playlist->ripple (_last_position, _primary->region()->length(), _primary->region());
|
||
} else {
|
||
playlist->rdiff_and_add_command (_editor->session());
|
||
}
|
||
|
||
_editor->commit_reversible_command ();
|
||
|
||
delete _primary;
|
||
_primary = 0;
|
||
_views.clear ();
|
||
}
|
||
|
||
void
|
||
RegionInsertDrag::aborted (bool)
|
||
{
|
||
delete _primary;
|
||
_primary = 0;
|
||
_views.clear ();
|
||
}
|
||
|
||
RegionCreateDrag::RegionCreateDrag (Editor* e, ArdourCanvas::Item* i, TimeAxisView* v)
|
||
: Drag (e, i, e->default_time_domain()),
|
||
_view (dynamic_cast<MidiTimeAxisView*> (v))
|
||
{
|
||
DEBUG_TRACE (DEBUG::Drags, "New RegionCreateDrag\n");
|
||
|
||
assert (_view);
|
||
}
|
||
|
||
void
|
||
RegionCreateDrag::motion (GdkEvent* event, bool first_move)
|
||
{
|
||
if (first_move) {
|
||
_editor->begin_reversible_command (_("create region"));
|
||
_region = add_midi_region (_view, false);
|
||
_view->playlist()->freeze ();
|
||
} else {
|
||
|
||
if (_region) {
|
||
timepos_t const pos (adjusted_current_time (event).beats());
|
||
|
||
if (pos <= grab_time()) {
|
||
_region->set_initial_position (pos);
|
||
}
|
||
|
||
if (pos != grab_time()) {
|
||
/* Force MIDI regions to use Beats ... for now */
|
||
timecnt_t const len (grab_time().distance (pos).abs().beats());
|
||
_region->set_length (len);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
void
|
||
RegionCreateDrag::finished (GdkEvent* event, bool movement_occurred)
|
||
{
|
||
if (!movement_occurred) {
|
||
add_midi_region (_view, true);
|
||
} else {
|
||
_view->playlist()->thaw ();
|
||
_editor->commit_reversible_command();
|
||
}
|
||
}
|
||
|
||
void
|
||
RegionCreateDrag::aborted (bool)
|
||
{
|
||
if (_region) {
|
||
_view->playlist()->thaw ();
|
||
}
|
||
|
||
/* XXX */
|
||
}
|
||
|
||
NoteResizeDrag::NoteResizeDrag (Editor* e, ArdourCanvas::Item* i)
|
||
: Drag (e, i, Temporal::BeatTime)
|
||
, region (0)
|
||
, relative (false)
|
||
, at_front (true)
|
||
, _was_selected (false)
|
||
, _snap_delta (0)
|
||
{
|
||
DEBUG_TRACE (DEBUG::Drags, "New NoteResizeDrag\n");
|
||
}
|
||
|
||
void
|
||
NoteResizeDrag::start_grab (GdkEvent* event, Gdk::Cursor* /*ignored*/)
|
||
{
|
||
Gdk::Cursor* cursor;
|
||
NoteBase* cnote = reinterpret_cast<NoteBase*> (_item->get_data ("notebase"));
|
||
assert (cnote);
|
||
float x_fraction = cnote->mouse_x_fraction ();
|
||
|
||
if (x_fraction > 0.0 && x_fraction < 0.25) {
|
||
cursor = _editor->cursors()->left_side_trim;
|
||
at_front = true;
|
||
} else {
|
||
cursor = _editor->cursors()->right_side_trim;
|
||
at_front = false;
|
||
}
|
||
|
||
Drag::start_grab (event, cursor);
|
||
|
||
region = &cnote->region_view();
|
||
|
||
double temp;
|
||
temp = region->snap_to_pixel (cnote->x0 (), true);
|
||
_snap_delta = temp - cnote->x0 ();
|
||
|
||
_item->grab ();
|
||
|
||
if (event->motion.state & ArdourKeyboard::note_size_relative_modifier ()) {
|
||
relative = false;
|
||
} else {
|
||
relative = true;
|
||
}
|
||
MidiRegionSelection ms = _editor->get_selection().midi_regions();
|
||
|
||
if (ms.size() > 1) {
|
||
/* has to be relative, may make no sense otherwise */
|
||
relative = true;
|
||
}
|
||
|
||
if (!(_was_selected = cnote->selected())) {
|
||
|
||
const bool extend = Keyboard::modifier_state_equals (event->button.state, Keyboard::TertiaryModifier);
|
||
const bool add = Keyboard::modifier_state_equals (event->button.state, Keyboard::PrimaryModifier);
|
||
|
||
region->note_selected (cnote, add, extend);
|
||
}
|
||
}
|
||
|
||
void
|
||
NoteResizeDrag::motion (GdkEvent* event, bool first_move)
|
||
{
|
||
MidiRegionSelection ms = _editor->get_selection().midi_regions();
|
||
if (first_move) {
|
||
_editor->begin_reversible_command (_("resize notes"));
|
||
|
||
for (MidiRegionSelection::iterator r = ms.begin(); r != ms.end(); ) {
|
||
MidiRegionSelection::iterator next;
|
||
next = r;
|
||
++next;
|
||
MidiRegionView* mrv = dynamic_cast<MidiRegionView*>(*r);
|
||
if (mrv) {
|
||
mrv->begin_resizing (at_front);
|
||
}
|
||
r = next;
|
||
}
|
||
}
|
||
|
||
for (MidiRegionSelection::iterator r = ms.begin(); r != ms.end(); ++r) {
|
||
NoteBase* nb = reinterpret_cast<NoteBase*> (_item->get_data ("notebase"));
|
||
assert (nb);
|
||
MidiRegionView* mrv = dynamic_cast<MidiRegionView*>(*r);
|
||
if (mrv) {
|
||
double sd = 0.0;
|
||
bool snap = true;
|
||
bool apply_snap_delta = ArdourKeyboard::indicates_snap_delta (event->button.state);
|
||
|
||
if (ArdourKeyboard::indicates_snap (event->button.state)) {
|
||
if (_editor->snap_mode () != SnapOff) {
|
||
snap = false;
|
||
}
|
||
} else {
|
||
if (_editor->snap_mode () == SnapOff) {
|
||
snap = false;
|
||
/* inverted logic here - we;re in snapoff but we've pressed the snap delta modifier */
|
||
if (apply_snap_delta) {
|
||
snap = true;
|
||
}
|
||
}
|
||
}
|
||
|
||
if (apply_snap_delta) {
|
||
sd = _snap_delta;
|
||
}
|
||
|
||
mrv->update_resizing (nb, at_front, _drags->current_pointer_x() - grab_x(), relative, sd, snap);
|
||
}
|
||
}
|
||
}
|
||
|
||
void
|
||
NoteResizeDrag::finished (GdkEvent* event, bool movement_occurred)
|
||
{
|
||
if (!movement_occurred) {
|
||
/* no motion - select note */
|
||
NoteBase* cnote = reinterpret_cast<NoteBase*> (_item->get_data ("notebase"));
|
||
if (_editor->current_mouse_mode() == Editing::MouseContent ||
|
||
_editor->current_mouse_mode() == Editing::MouseDraw) {
|
||
|
||
bool changed = false;
|
||
|
||
if (_was_selected) {
|
||
bool add = Keyboard::modifier_state_equals (event->button.state, Keyboard::PrimaryModifier);
|
||
if (add) {
|
||
region->note_deselected (cnote);
|
||
changed = true;
|
||
} else {
|
||
/* handled during button press */
|
||
}
|
||
} else {
|
||
/* handled during button press */
|
||
}
|
||
|
||
if (changed) {
|
||
_editor->begin_reversible_selection_op(X_("Resize Select Note Release"));
|
||
_editor->commit_reversible_selection_op();
|
||
}
|
||
}
|
||
|
||
return;
|
||
}
|
||
|
||
MidiRegionSelection ms = _editor->get_selection().midi_regions();
|
||
for (MidiRegionSelection::iterator r = ms.begin(); r != ms.end(); ++r) {
|
||
NoteBase* nb = reinterpret_cast<NoteBase*> (_item->get_data ("notebase"));
|
||
assert (nb);
|
||
MidiRegionView* mrv = dynamic_cast<MidiRegionView*>(*r);
|
||
double sd = 0.0;
|
||
bool snap = true;
|
||
bool apply_snap_delta = ArdourKeyboard::indicates_snap_delta (event->button.state);
|
||
if (mrv) {
|
||
if (ArdourKeyboard::indicates_snap (event->button.state)) {
|
||
if (_editor->snap_mode () != SnapOff) {
|
||
snap = false;
|
||
}
|
||
} else {
|
||
if (_editor->snap_mode () == SnapOff) {
|
||
snap = false;
|
||
/* inverted logic here - we;re in snapoff but we've pressed the snap delta modifier */
|
||
if (apply_snap_delta) {
|
||
snap = true;
|
||
}
|
||
}
|
||
}
|
||
|
||
if (apply_snap_delta) {
|
||
sd = _snap_delta;
|
||
}
|
||
|
||
mrv->finish_resizing (nb, at_front, _drags->current_pointer_x() - grab_x(), relative, sd, snap);
|
||
}
|
||
}
|
||
|
||
_editor->commit_reversible_command ();
|
||
}
|
||
|
||
void
|
||
NoteResizeDrag::aborted (bool)
|
||
{
|
||
MidiRegionSelection ms = _editor->get_selection().midi_regions();
|
||
for (MidiRegionSelection::iterator r = ms.begin(); r != ms.end(); ++r) {
|
||
MidiRegionView* mrv = dynamic_cast<MidiRegionView*>(*r);
|
||
if (mrv) {
|
||
mrv->abort_resizing ();
|
||
}
|
||
}
|
||
}
|
||
|
||
AVDraggingView::AVDraggingView (RegionView* v)
|
||
: view (v)
|
||
{
|
||
initial_position = v->region()->position_sample();
|
||
}
|
||
|
||
VideoTimeLineDrag::VideoTimeLineDrag (Editor* e, ArdourCanvas::Item* i)
|
||
: Drag (e, i, e->default_time_domain())
|
||
{
|
||
DEBUG_TRACE (DEBUG::Drags, "New VideoTimeLineDrag\n");
|
||
|
||
RegionSelection rs;
|
||
TrackViewList empty;
|
||
empty.clear();
|
||
_editor->get_regions_after(rs, timepos_t::zero (Temporal::AudioTime), empty);
|
||
std::list<RegionView*> views = rs.by_layer();
|
||
|
||
_stuck = false;
|
||
for (list<RegionView*>::iterator i = views.begin(); i != views.end(); ++i) {
|
||
RegionView* rv = (*i);
|
||
if (!rv->region()->video_locked()) {
|
||
continue;
|
||
}
|
||
if (rv->region()->locked()) {
|
||
_stuck = true;
|
||
}
|
||
_views.push_back (AVDraggingView (rv));
|
||
}
|
||
}
|
||
|
||
void
|
||
VideoTimeLineDrag::start_grab (GdkEvent* event, Gdk::Cursor*)
|
||
{
|
||
Drag::start_grab (event);
|
||
if (_editor->session() == 0) {
|
||
return;
|
||
}
|
||
|
||
if (Keyboard::modifier_state_equals (event->button.state, Keyboard::ModifierMask (Keyboard::TertiaryModifier))) {
|
||
_stuck = false;
|
||
_views.clear();
|
||
}
|
||
|
||
if (_stuck) {
|
||
show_verbose_cursor_text (_("One or more Audio Regions\nare both Locked and\nLocked to Video.\nThe video cannot be moved."));
|
||
return;
|
||
}
|
||
|
||
_startdrag_video_offset=ARDOUR_UI::instance()->video_timeline->get_offset();
|
||
_max_backwards_drag = (
|
||
ARDOUR_UI::instance()->video_timeline->get_duration()
|
||
+ ARDOUR_UI::instance()->video_timeline->get_offset()
|
||
- ceil(ARDOUR_UI::instance()->video_timeline->get_apv())
|
||
);
|
||
|
||
for (list<AVDraggingView>::iterator i = _views.begin(); i != _views.end(); ++i) {
|
||
if (i->initial_position < _max_backwards_drag || _max_backwards_drag < 0) {
|
||
_max_backwards_drag = ARDOUR_UI::instance()->video_timeline->quantify_samples_to_apv (i->initial_position);
|
||
}
|
||
}
|
||
DEBUG_TRACE (DEBUG::Drags, string_compose("VideoTimeLineDrag: max backwards-drag: %1\n", _max_backwards_drag));
|
||
|
||
char buf[128];
|
||
Timecode::Time timecode;
|
||
_editor->session()->sample_to_timecode(abs(_startdrag_video_offset), timecode, true /* use_offset */, false /* use_subframes */ );
|
||
snprintf (buf, sizeof (buf), "Video Start:\n%c%02" PRId32 ":%02" PRId32 ":%02" PRId32 ":%02" PRId32, (_startdrag_video_offset<0?'-':' '), timecode.hours, timecode.minutes, timecode.seconds, timecode.frames);
|
||
show_verbose_cursor_text (buf);
|
||
}
|
||
|
||
void
|
||
VideoTimeLineDrag::motion (GdkEvent* event, bool first_move)
|
||
{
|
||
if (_editor->session() == 0) {
|
||
return;
|
||
}
|
||
if (ARDOUR_UI::instance()->video_timeline->is_offset_locked()) {
|
||
return;
|
||
}
|
||
if (_stuck) {
|
||
show_verbose_cursor_text (_("One or more Audio Regions\nare both Locked and\nLocked to Video.\nThe video cannot be moved."));
|
||
return;
|
||
}
|
||
|
||
samplecnt_t dt = adjusted_current_time (event).samples() - raw_grab_time().samples() + _pointer_offset.samples();
|
||
dt = ARDOUR_UI::instance()->video_timeline->quantify_samples_to_apv(_startdrag_video_offset+dt) - _startdrag_video_offset;
|
||
|
||
if (_max_backwards_drag >= 0 && dt <= - _max_backwards_drag) {
|
||
dt = - _max_backwards_drag;
|
||
}
|
||
|
||
ARDOUR_UI::instance()->video_timeline->set_offset(_startdrag_video_offset+dt);
|
||
ARDOUR_UI::instance()->flush_videotimeline_cache(true);
|
||
|
||
for (list<AVDraggingView>::iterator i = _views.begin(); i != _views.end(); ++i) {
|
||
RegionView* rv = i->view;
|
||
DEBUG_TRACE (DEBUG::Drags, string_compose("SHIFT REGION at %1 by %2\n", i->initial_position, dt));
|
||
if (first_move) {
|
||
rv->drag_start ();
|
||
rv->region()->clear_changes ();
|
||
rv->region()->suspend_property_changes();
|
||
}
|
||
rv->region()->set_position(timepos_t (i->initial_position + dt));
|
||
rv->region_changed (ARDOUR::Properties::length);
|
||
}
|
||
|
||
const samplepos_t offset = ARDOUR_UI::instance()->video_timeline->get_offset();
|
||
Timecode::Time timecode;
|
||
Timecode::Time timediff;
|
||
char buf[128];
|
||
_editor->session()->sample_to_timecode(abs(offset), timecode, true /* use_offset */, false /* use_subframes */ );
|
||
_editor->session()->sample_to_timecode(abs(dt), timediff, false /* use_offset */, false /* use_subframes */ );
|
||
snprintf (buf, sizeof (buf),
|
||
"%s\n%c%02" PRId32 ":%02" PRId32 ":%02" PRId32 ":%02" PRId32
|
||
"\n%s\n%c%02" PRId32 ":%02" PRId32 ":%02" PRId32 ":%02" PRId32
|
||
, _("Video Start:"),
|
||
(offset<0?'-':' '), timecode.hours, timecode.minutes, timecode.seconds, timecode.frames
|
||
, _("Diff:"),
|
||
(dt<0?'-':' '), timediff.hours, timediff.minutes, timediff.seconds, timediff.frames
|
||
);
|
||
show_verbose_cursor_text (buf);
|
||
}
|
||
|
||
void
|
||
VideoTimeLineDrag::finished (GdkEvent * /*event*/, bool movement_occurred)
|
||
{
|
||
if (ARDOUR_UI::instance()->video_timeline->is_offset_locked()) {
|
||
return;
|
||
}
|
||
if (_stuck) {
|
||
return;
|
||
}
|
||
|
||
if (!movement_occurred || ! _editor->session()) {
|
||
return;
|
||
}
|
||
|
||
ARDOUR_UI::instance()->flush_videotimeline_cache(true);
|
||
|
||
_editor->begin_reversible_command (_("Move Video"));
|
||
|
||
XMLNode &before = ARDOUR_UI::instance()->video_timeline->get_state();
|
||
ARDOUR_UI::instance()->video_timeline->save_undo();
|
||
XMLNode &after = ARDOUR_UI::instance()->video_timeline->get_state();
|
||
_editor->session()->add_command(new MementoCommand<VideoTimeLine>(*(ARDOUR_UI::instance()->video_timeline), &before, &after));
|
||
|
||
for (list<AVDraggingView>::iterator i = _views.begin(); i != _views.end(); ++i) {
|
||
i->view->drag_end();
|
||
i->view->region()->resume_property_changes ();
|
||
|
||
_editor->session()->add_command (new StatefulDiffCommand (i->view->region()));
|
||
}
|
||
|
||
_editor->session()->maybe_update_session_range(
|
||
timepos_t (std::max(ARDOUR_UI::instance()->video_timeline->get_offset(), (sampleoffset_t) 0)),
|
||
timepos_t (std::max(ARDOUR_UI::instance()->video_timeline->get_offset() + ARDOUR_UI::instance()->video_timeline->get_duration(), (sampleoffset_t) 0))
|
||
);
|
||
|
||
|
||
_editor->commit_reversible_command ();
|
||
}
|
||
|
||
void
|
||
VideoTimeLineDrag::aborted (bool)
|
||
{
|
||
if (ARDOUR_UI::instance()->video_timeline->is_offset_locked()) {
|
||
return;
|
||
}
|
||
ARDOUR_UI::instance()->video_timeline->set_offset(_startdrag_video_offset);
|
||
ARDOUR_UI::instance()->flush_videotimeline_cache(true);
|
||
|
||
for (list<AVDraggingView>::iterator i = _views.begin(); i != _views.end(); ++i) {
|
||
i->view->region()->resume_property_changes ();
|
||
i->view->region()->set_position(timepos_t (i->initial_position));
|
||
}
|
||
}
|
||
|
||
TrimDrag::TrimDrag (Editor* e, ArdourCanvas::Item* i, RegionView* p, list<RegionView*> const & v, Temporal::TimeDomain td, bool preserve_fade_anchor)
|
||
: RegionDrag (e, i, p, v, td)
|
||
, _operation (StartTrim)
|
||
, _preserve_fade_anchor (preserve_fade_anchor)
|
||
, _jump_position_when_done (false)
|
||
{
|
||
DEBUG_TRACE (DEBUG::Drags, "New TrimDrag\n");
|
||
}
|
||
|
||
void
|
||
TrimDrag::start_grab (GdkEvent* event, Gdk::Cursor*)
|
||
{
|
||
timepos_t const region_start = _primary->region()->position();
|
||
timepos_t const region_end = _primary->region()->end();
|
||
timecnt_t const region_length = _primary->region()->length();
|
||
|
||
timepos_t const pf = adjusted_current_time (event);
|
||
setup_snap_delta (region_start);
|
||
|
||
/* These will get overridden for a point trim.*/
|
||
if (pf < (region_start + region_length.scale (ratio_t (1, 2)))) {
|
||
/* closer to front */
|
||
_operation = StartTrim;
|
||
if (Keyboard::modifier_state_equals (event->button.state, ArdourKeyboard::trim_anchored_modifier ())) {
|
||
Drag::start_grab (event, _editor->cursors()->anchored_left_side_trim);
|
||
} else {
|
||
Drag::start_grab (event, _editor->cursors()->left_side_trim);
|
||
}
|
||
} else {
|
||
/* closer to end */
|
||
_operation = EndTrim;
|
||
if (Keyboard::modifier_state_equals (event->button.state, ArdourKeyboard::trim_anchored_modifier ())) {
|
||
Drag::start_grab (event, _editor->cursors()->anchored_right_side_trim);
|
||
} else {
|
||
Drag::start_grab (event, _editor->cursors()->right_side_trim);
|
||
}
|
||
}
|
||
|
||
/* jump trim disabled for now
|
||
if (Keyboard::modifier_state_equals (event->button.state, Keyboard::trim_jump_modifier ())) {
|
||
_jump_position_when_done = true;
|
||
}
|
||
*/
|
||
|
||
switch (_operation) {
|
||
case StartTrim:
|
||
show_verbose_cursor_time (region_start);
|
||
break;
|
||
case EndTrim:
|
||
show_verbose_cursor_duration (region_start, region_end);
|
||
break;
|
||
}
|
||
show_view_preview (_operation == StartTrim ? region_start : region_end);
|
||
|
||
for (list<DraggingView>::const_iterator i = _views.begin(); i != _views.end(); ++i) {
|
||
i->view->region()->suspend_property_changes ();
|
||
}
|
||
}
|
||
|
||
void
|
||
TrimDrag::motion (GdkEvent* event, bool first_move)
|
||
{
|
||
RegionView* rv = _primary;
|
||
|
||
pair<set<boost::shared_ptr<Playlist> >::iterator,bool> insert_result;
|
||
timecnt_t delta;
|
||
timepos_t adj_time = adjusted_time (_drags->current_pointer_time () + snap_delta (event->button.state), event, true);
|
||
timecnt_t dt = raw_grab_time().distance (adj_time) + _pointer_offset - snap_delta (event->button.state);
|
||
|
||
if (first_move) {
|
||
|
||
string trim_type;
|
||
|
||
switch (_operation) {
|
||
case StartTrim:
|
||
trim_type = "Region start trim";
|
||
break;
|
||
case EndTrim:
|
||
trim_type = "Region end trim";
|
||
break;
|
||
default:
|
||
assert(0);
|
||
break;
|
||
}
|
||
|
||
_editor->begin_reversible_command (trim_type);
|
||
|
||
for (list<DraggingView>::const_iterator i = _views.begin(); i != _views.end(); ++i) {
|
||
RegionView* rv = i->view;
|
||
rv->region()->playlist()->clear_owned_changes ();
|
||
|
||
if (_operation == StartTrim) {
|
||
rv->trim_front_starting ();
|
||
}
|
||
|
||
AudioRegionView* const arv = dynamic_cast<AudioRegionView*> (rv);
|
||
|
||
if (arv) {
|
||
arv->temporarily_hide_envelope ();
|
||
arv->drag_start ();
|
||
}
|
||
|
||
boost::shared_ptr<Playlist> pl = rv->region()->playlist();
|
||
insert_result = _editor->motion_frozen_playlists.insert (pl);
|
||
|
||
if (insert_result.second) {
|
||
pl->freeze();
|
||
}
|
||
|
||
MidiRegionView* const mrv = dynamic_cast<MidiRegionView*> (rv);
|
||
/* a MRV start trim may change the source length. ensure we cover all playlists here */
|
||
if (mrv && _operation == StartTrim) {
|
||
vector<boost::shared_ptr<Playlist> > all_playlists;
|
||
_editor->session()->playlists()->get (all_playlists);
|
||
for (vector<boost::shared_ptr<Playlist> >::iterator x = all_playlists.begin(); x != all_playlists.end(); ++x) {
|
||
|
||
if ((*x)->uses_source (rv->region()->source(0))) {
|
||
insert_result = _editor->motion_frozen_playlists.insert (*x);
|
||
if (insert_result.second) {
|
||
(*x)->clear_owned_changes ();
|
||
(*x)->freeze();
|
||
}
|
||
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
bool non_overlap_trim = false;
|
||
|
||
if (event && Keyboard::modifier_state_contains (event->button.state, ArdourKeyboard::trim_overlap_modifier ())) {
|
||
non_overlap_trim = true;
|
||
}
|
||
|
||
/* contstrain trim to fade length */
|
||
if (_preserve_fade_anchor) {
|
||
|
||
/* fades are audio and always use AudioTime domain */
|
||
|
||
samplecnt_t dts = dt.samples();
|
||
|
||
switch (_operation) {
|
||
case StartTrim:
|
||
for (list<DraggingView>::const_iterator i = _views.begin(); i != _views.end(); ++i) {
|
||
AudioRegionView* arv = dynamic_cast<AudioRegionView*> (i->view);
|
||
if (!arv) continue;
|
||
boost::shared_ptr<AudioRegion> ar (arv->audio_region());
|
||
if (ar->locked()) continue;
|
||
samplecnt_t len = ar->fade_in()->back()->when.samples();
|
||
if (len < dts) dts = min(dts, len);
|
||
}
|
||
break;
|
||
case EndTrim:
|
||
for (list<DraggingView>::const_iterator i = _views.begin(); i != _views.end(); ++i) {
|
||
AudioRegionView* arv = dynamic_cast<AudioRegionView*> (i->view);
|
||
if (!arv) continue;
|
||
boost::shared_ptr<AudioRegion> ar (arv->audio_region());
|
||
if (ar->locked()) continue;
|
||
samplecnt_t len = ar->fade_out()->back()->when.samples();
|
||
if (len < -dts) dts = max(dts, -len);
|
||
}
|
||
break;
|
||
}
|
||
}
|
||
|
||
switch (_operation) {
|
||
case StartTrim:
|
||
for (list<DraggingView>::iterator i = _views.begin(); i != _views.end(); ++i) {
|
||
bool changed = i->view->trim_front (timepos_t (i->initial_position) + dt, non_overlap_trim);
|
||
|
||
|
||
if (changed && _preserve_fade_anchor) {
|
||
AudioRegionView* arv = dynamic_cast<AudioRegionView*> (i->view);
|
||
if (arv) {
|
||
boost::shared_ptr<AudioRegion> ar (arv->audio_region());
|
||
samplecnt_t len = ar->fade_in()->back()->when.samples();
|
||
samplecnt_t diff = ar->first_sample() - i->initial_position.samples();
|
||
samplepos_t new_length = len - diff;
|
||
i->anchored_fade_length = min (ar->length_samples(), new_length);
|
||
//i->anchored_fade_length = ar->verify_xfade_bounds (new_length, true /*START*/ );
|
||
arv->reset_fade_in_shape_width (ar, i->anchored_fade_length, true);
|
||
}
|
||
}
|
||
}
|
||
break;
|
||
|
||
case EndTrim:
|
||
for (list<DraggingView>::iterator i = _views.begin(); i != _views.end(); ++i) {
|
||
bool changed = i->view->trim_end (timepos_t (i->initial_end) + dt, non_overlap_trim);
|
||
|
||
if (changed && _preserve_fade_anchor) {
|
||
AudioRegionView* arv = dynamic_cast<AudioRegionView*> (i->view);
|
||
if (arv) {
|
||
boost::shared_ptr<AudioRegion> ar (arv->audio_region());
|
||
samplecnt_t len = ar->fade_out()->back()->when.samples();
|
||
samplecnt_t diff = 1 + ar->last_sample() - i->initial_end.samples();
|
||
samplepos_t new_length = len + diff;
|
||
i->anchored_fade_length = min (ar->length_samples(), new_length);
|
||
//i->anchored_fade_length = ar->verify_xfade_bounds (new_length, false /*END*/ );
|
||
arv->reset_fade_out_shape_width (ar, i->anchored_fade_length, true);
|
||
}
|
||
}
|
||
}
|
||
break;
|
||
|
||
}
|
||
|
||
switch (_operation) {
|
||
case StartTrim:
|
||
show_verbose_cursor_time (rv->region()->position());
|
||
break;
|
||
case EndTrim:
|
||
show_verbose_cursor_duration (rv->region()->position(), rv->region()->end());
|
||
break;
|
||
}
|
||
show_view_preview ((_operation == StartTrim ? rv->region()->position() : rv->region()->end()));
|
||
}
|
||
|
||
void
|
||
TrimDrag::finished (GdkEvent* event, bool movement_occurred)
|
||
{
|
||
if (movement_occurred) {
|
||
motion (event, false);
|
||
|
||
if (_operation == StartTrim) {
|
||
for (list<DraggingView>::const_iterator i = _views.begin(); i != _views.end(); ++i) {
|
||
{
|
||
/* This must happen before the region's StatefulDiffCommand is created, as it may
|
||
`correct' (ahem) the region's _start from being negative to being zero. It
|
||
needs to be zero in the undo record.
|
||
*/
|
||
i->view->trim_front_ending ();
|
||
}
|
||
if (_preserve_fade_anchor && i->anchored_fade_length) {
|
||
AudioRegionView* arv = dynamic_cast<AudioRegionView*> (i->view);
|
||
if (arv) {
|
||
boost::shared_ptr<AudioRegion> ar (arv->audio_region());
|
||
arv->reset_fade_in_shape_width (ar, i->anchored_fade_length);
|
||
ar->set_fade_in_length(i->anchored_fade_length);
|
||
ar->set_fade_in_active(true);
|
||
}
|
||
}
|
||
if (_jump_position_when_done) {
|
||
i->view->region()->set_position (timepos_t (i->initial_position));
|
||
}
|
||
}
|
||
} else if (_operation == EndTrim) {
|
||
for (list<DraggingView>::const_iterator i = _views.begin(); i != _views.end(); ++i) {
|
||
if (_preserve_fade_anchor && i->anchored_fade_length) {
|
||
AudioRegionView* arv = dynamic_cast<AudioRegionView*> (i->view);
|
||
if (arv) {
|
||
boost::shared_ptr<AudioRegion> ar (arv->audio_region());
|
||
arv->reset_fade_out_shape_width (ar, i->anchored_fade_length);
|
||
ar->set_fade_out_length(i->anchored_fade_length);
|
||
ar->set_fade_out_active(true);
|
||
}
|
||
}
|
||
if (_jump_position_when_done) {
|
||
i->view->region()->set_position (timepos_t (i->initial_end).earlier (i->view->region()->length()));
|
||
}
|
||
}
|
||
}
|
||
|
||
if (!_editor->selection->selected (_primary)) {
|
||
_primary->thaw_after_trim ();
|
||
} else {
|
||
for (list<DraggingView>::const_iterator i = _views.begin(); i != _views.end(); ++i) {
|
||
i->view->thaw_after_trim ();
|
||
}
|
||
}
|
||
|
||
for (set<boost::shared_ptr<Playlist> >::iterator p = _editor->motion_frozen_playlists.begin(); p != _editor->motion_frozen_playlists.end(); ++p) {
|
||
/* Trimming one region may affect others on the playlist, so we need
|
||
to get undo Commands from the whole playlist rather than just the
|
||
region. Use motion_frozen_playlists (a set) to make sure we don't
|
||
diff a given playlist more than once.
|
||
*/
|
||
|
||
vector<Command*> cmds;
|
||
(*p)->rdiff (cmds);
|
||
_editor->session()->add_commands (cmds);
|
||
(*p)->thaw ();
|
||
}
|
||
|
||
_editor->motion_frozen_playlists.clear ();
|
||
_editor->commit_reversible_command();
|
||
|
||
} else {
|
||
/* no mouse movement */
|
||
if (adjusted_current_time (event) != adjusted_time (_drags->current_pointer_time(), event, false)) {
|
||
_editor->point_trim (event, adjusted_current_time (event));
|
||
}
|
||
}
|
||
|
||
for (list<DraggingView>::const_iterator i = _views.begin(); i != _views.end(); ++i) {
|
||
i->view->region()->resume_property_changes ();
|
||
}
|
||
}
|
||
|
||
void
|
||
TrimDrag::aborted (bool movement_occurred)
|
||
{
|
||
/* Our motion method is changing model state, so use the Undo system
|
||
to cancel. Perhaps not ideal, as this will leave an Undo point
|
||
behind which may be slightly odd from the user's point of view.
|
||
*/
|
||
|
||
GdkEvent ev;
|
||
memset (&ev, 0, sizeof (GdkEvent));
|
||
finished (&ev, movement_occurred);
|
||
|
||
if (movement_occurred) {
|
||
_editor->session()->undo (1);
|
||
}
|
||
|
||
for (list<DraggingView>::const_iterator i = _views.begin(); i != _views.end(); ++i) {
|
||
i->view->region()->resume_property_changes ();
|
||
}
|
||
}
|
||
|
||
void
|
||
TrimDrag::setup_pointer_offset ()
|
||
{
|
||
list<DraggingView>::iterator i = _views.begin ();
|
||
while (i != _views.end() && i->view != _primary) {
|
||
++i;
|
||
}
|
||
|
||
if (i == _views.end()) {
|
||
return;
|
||
}
|
||
|
||
|
||
switch (_operation) {
|
||
case StartTrim:
|
||
_pointer_offset = i->initial_position.distance (raw_grab_time());
|
||
break;
|
||
case EndTrim:
|
||
_pointer_offset = i->initial_end.distance (raw_grab_time());
|
||
break;
|
||
}
|
||
}
|
||
|
||
MeterMarkerDrag::MeterMarkerDrag (Editor* e, ArdourCanvas::Item* i, bool c)
|
||
: Drag (e, i, Temporal::BeatTime)
|
||
, _marker (reinterpret_cast<MeterMarker*> (_item->get_data ("marker")))
|
||
, _old_grid_type (e->grid_type())
|
||
, _old_snap_mode (e->snap_mode())
|
||
, before_state (0)
|
||
{
|
||
DEBUG_TRACE (DEBUG::Drags, "New MeterMarkerDrag\n");
|
||
assert (_marker);
|
||
_movable = !TempoMap::use()->is_initial (_marker->meter());
|
||
}
|
||
|
||
void
|
||
MeterMarkerDrag::start_grab (GdkEvent* event, Gdk::Cursor* cursor)
|
||
{
|
||
Drag::start_grab (event, cursor);
|
||
show_verbose_cursor_time (adjusted_current_time (event));
|
||
|
||
/* setup thread-local tempo map ptr as a writable copy, and keep a
|
||
* local reference
|
||
*/
|
||
|
||
map = _editor->begin_tempo_map_edit ();
|
||
initial_sclock = _marker->meter().sclock();
|
||
}
|
||
|
||
void
|
||
MeterMarkerDrag::setup_pointer_offset ()
|
||
{
|
||
_pointer_offset = _marker->meter().time().distance (raw_grab_time());
|
||
}
|
||
|
||
void
|
||
MeterMarkerDrag::motion (GdkEvent* event, bool first_move)
|
||
{
|
||
if (first_move) {
|
||
// create a dummy marker to catch events, then hide it.
|
||
|
||
char name[64];
|
||
snprintf (name, sizeof(name), "%d/%d", _marker->meter().divisions_per_bar(), _marker->meter().note_value ());
|
||
|
||
_marker = new MeterMarker (
|
||
*_editor,
|
||
*_editor->meter_group,
|
||
UIConfiguration::instance().color ("meter marker"),
|
||
name,
|
||
_marker->meter()
|
||
);
|
||
|
||
/* use the new marker for the grab */
|
||
swap_grab (&_marker->the_item(), 0, GDK_CURRENT_TIME);
|
||
_marker->hide();
|
||
|
||
/* get current state */
|
||
before_state = &map->get_state();
|
||
_editor->begin_reversible_command (_("move meter mark"));
|
||
|
||
/* only snap to bars. */
|
||
|
||
_editor->set_grid_to (GridTypeBar);
|
||
_editor->set_snap_mode (SnapMagnetic);
|
||
}
|
||
|
||
if (!_movable) {
|
||
return;
|
||
}
|
||
|
||
timepos_t pos;
|
||
const bool leftward_earlier = (current_pointer_x() < last_pointer_x());
|
||
|
||
/* not useful to try to snap to a grid we're about to change */
|
||
pos = adjusted_current_time (event, false);
|
||
|
||
if (map->move_meter (_marker->meter(), pos, leftward_earlier, false)) {
|
||
/* it was moved */
|
||
_editor->mid_tempo_change (Editor::MeterChanged);
|
||
show_verbose_cursor_time (timepos_t (_marker->meter().beats()));
|
||
_editor->set_snapped_cursor_position (timepos_t (_marker->meter().sample(_editor->session()->sample_rate())));
|
||
}
|
||
}
|
||
|
||
void
|
||
MeterMarkerDrag::finished (GdkEvent* event, bool movement_occurred)
|
||
{
|
||
if (!movement_occurred) {
|
||
/* reset thread local tempo map to the original state */
|
||
_editor->abort_tempo_map_edit ();
|
||
if (was_double_click()) {
|
||
_editor->edit_meter_marker (*_marker);
|
||
}
|
||
return;
|
||
}
|
||
|
||
/* reinstate old snap setting */
|
||
_editor->set_grid_to (_old_grid_type);
|
||
_editor->set_snap_mode (_old_snap_mode);
|
||
|
||
_editor->commit_tempo_map_edit (map);
|
||
XMLNode &after = map->get_state();
|
||
|
||
_editor->session()->add_command (new Temporal::TempoCommand (_("move time signature"), before_state, &after));
|
||
_editor->commit_reversible_command ();
|
||
|
||
// delete the dummy marker we used for visual representation while moving.
|
||
// a new visual marker will show up automatically.
|
||
delete _marker;
|
||
|
||
}
|
||
|
||
void
|
||
MeterMarkerDrag::aborted (bool moved)
|
||
{
|
||
/* reset thread local tempo map to the original state */
|
||
TempoMap::abort_update ();
|
||
|
||
_marker->set_position (_marker->meter().time());
|
||
|
||
if (moved) {
|
||
/* reinstate old snap setting */
|
||
_editor->set_grid_to (_old_grid_type);
|
||
_editor->set_snap_mode (_old_snap_mode);
|
||
|
||
// delete the dummy marker we used for visual representation while moving.
|
||
// a new visual marker will show up automatically.
|
||
delete _marker;
|
||
}
|
||
|
||
}
|
||
|
||
TempoCurveDrag::TempoCurveDrag (Editor* e, ArdourCanvas::Item* i)
|
||
: Drag (e, i, Temporal::BeatTime)
|
||
{
|
||
}
|
||
|
||
void
|
||
TempoCurveDrag::start_grab (GdkEvent* event, Gdk::Cursor* cursor)
|
||
{
|
||
Drag::start_grab (event, cursor);
|
||
/* setup thread-local tempo map ptr as a writable copy */
|
||
map = _editor->begin_tempo_map_edit ();
|
||
TempoCurve* tc = reinterpret_cast<TempoCurve*> (_item->get_data (X_("tempo curve")));
|
||
if (!tc) {
|
||
point = const_cast<TempoPoint*> (&map->tempo_at (raw_grab_time()));
|
||
} else {
|
||
point = const_cast<TempoPoint*> (&tc->tempo());
|
||
}
|
||
initial_bpm = point->note_types_per_minute();
|
||
}
|
||
|
||
void
|
||
TempoCurveDrag::motion (GdkEvent* event, bool first_move)
|
||
{
|
||
if (first_move) {
|
||
/* get current state */
|
||
_before_state = &map->get_state();
|
||
_editor->begin_reversible_command (_("change tempo"));
|
||
}
|
||
|
||
double new_bpm = std::max (1.5, initial_bpm - ((current_pointer_x() - grab_x()) / 5.0));
|
||
stringstream strs;
|
||
Temporal::Tempo new_tempo (new_bpm, point->note_type());
|
||
map->change_tempo (*point, new_tempo);
|
||
strs << "Tempo: " << fixed << setprecision(3) << new_bpm;
|
||
show_verbose_cursor_text (strs.str());
|
||
|
||
_editor->mid_tempo_change (Editor::TempoChanged);
|
||
}
|
||
|
||
|
||
void
|
||
TempoCurveDrag::finished (GdkEvent* event, bool movement_occurred)
|
||
{
|
||
if (!movement_occurred) {
|
||
/* reset the per-thread tempo map ptr back to the current
|
||
* official version
|
||
*/
|
||
|
||
_editor->abort_tempo_map_edit ();
|
||
|
||
if (was_double_click()) {
|
||
// XXX would be nice to do this
|
||
// _editor->edit_tempo_marker (*_marker);
|
||
}
|
||
|
||
return;
|
||
}
|
||
|
||
/* push the current state of our writable map copy */
|
||
|
||
_editor->commit_tempo_map_edit (map);
|
||
XMLNode &after = map->get_state();
|
||
|
||
_editor->session()->add_command (new Temporal::TempoCommand (_("change tempo"), _before_state, &after));
|
||
_editor->commit_reversible_command ();
|
||
}
|
||
|
||
void
|
||
TempoCurveDrag::aborted (bool moved)
|
||
{
|
||
/* reset the per-thread tempo map ptr back to the current
|
||
* official version
|
||
*/
|
||
|
||
_editor->abort_tempo_map_edit ();
|
||
}
|
||
|
||
TempoMarkerDrag::TempoMarkerDrag (Editor* e, ArdourCanvas::Item* i)
|
||
: Drag (e, i, Temporal::BeatTime)
|
||
, _grab_bpm (120.0, 4.0)
|
||
, _before_state (0)
|
||
{
|
||
DEBUG_TRACE (DEBUG::Drags, "New TempoMarkerDrag\n");
|
||
|
||
_marker = reinterpret_cast<TempoMarker*> (_item->get_data ("marker"));
|
||
_real_section = &_marker->tempo();
|
||
_movable = !TempoMap::use()->is_initial (_marker->tempo());
|
||
_grab_bpm = Tempo (_real_section->note_types_per_minute(), _real_section->note_type(), _real_section->end_note_types_per_minute());
|
||
_grab_qn = _real_section->beats();
|
||
assert (_marker);
|
||
}
|
||
|
||
void
|
||
TempoMarkerDrag::start_grab (GdkEvent* event, Gdk::Cursor* cursor)
|
||
{
|
||
Drag::start_grab (event, cursor);
|
||
if (!_real_section->active()) {
|
||
show_verbose_cursor_text (_("inactive"));
|
||
} else {
|
||
show_verbose_cursor_time (adjusted_current_time (event));
|
||
|
||
/* setup thread-local tempo map ptr as a writable copy */
|
||
|
||
map = _editor->begin_tempo_map_edit ();
|
||
}
|
||
}
|
||
|
||
void
|
||
TempoMarkerDrag::setup_pointer_offset ()
|
||
{
|
||
_pointer_offset = _marker->tempo().time().distance (raw_grab_time());
|
||
}
|
||
|
||
void
|
||
TempoMarkerDrag::motion (GdkEvent* event, bool first_move)
|
||
{
|
||
if (!_marker->tempo().active()) {
|
||
return;
|
||
}
|
||
|
||
if (first_move) {
|
||
/* get current state */
|
||
_before_state = &map->get_state();
|
||
_editor->begin_reversible_command (_("move tempo mark"));
|
||
}
|
||
|
||
if (ArdourKeyboard::modifier_state_equals (event->button.state, Keyboard::PrimaryModifier)) {
|
||
|
||
double new_bpm = std::max (1.5, _grab_bpm.note_types_per_minute() - ((current_pointer_x() - grab_x()) / 5.0));
|
||
stringstream strs;
|
||
Temporal::Tempo new_tempo (new_bpm, _marker->tempo().note_type());
|
||
map->change_tempo (const_cast<TempoPoint&>(_marker->tempo()), new_tempo);
|
||
strs << "Tempo: " << fixed << setprecision(3) << new_bpm;
|
||
show_verbose_cursor_text (strs.str());
|
||
|
||
} else if (_movable) {
|
||
|
||
timepos_t pos = adjusted_current_time (event);
|
||
map->move_tempo (_marker->tempo(), pos, false);
|
||
show_verbose_cursor_time (_marker->tempo().time());
|
||
}
|
||
|
||
_editor->mid_tempo_change (Editor::TempoChanged);
|
||
}
|
||
|
||
|
||
void
|
||
TempoMarkerDrag::finished (GdkEvent* event, bool movement_occurred)
|
||
{
|
||
if (!_marker->tempo().active()) {
|
||
return;
|
||
}
|
||
|
||
if (!movement_occurred) {
|
||
/* reset the per-thread tempo map ptr back to the current
|
||
* official version
|
||
*/
|
||
|
||
_editor->abort_tempo_map_edit ();
|
||
|
||
if (was_double_click()) {
|
||
_editor->edit_tempo_marker (*_marker);
|
||
}
|
||
|
||
return;
|
||
}
|
||
|
||
/* push the current state of our writable map copy */
|
||
|
||
_editor->commit_tempo_map_edit (map);
|
||
XMLNode &after = map->get_state();
|
||
|
||
_editor->session()->add_command (new Temporal::TempoCommand (_("move tempo"), _before_state, &after));
|
||
_editor->commit_reversible_command ();
|
||
}
|
||
|
||
void
|
||
TempoMarkerDrag::aborted (bool moved)
|
||
{
|
||
/* reset the per-thread tempo map ptr back to the current
|
||
* official version
|
||
*/
|
||
|
||
_editor->abort_tempo_map_edit ();
|
||
|
||
// _point->end_float ();
|
||
_marker->set_position (timepos_t (_marker->tempo().beats()));
|
||
|
||
if (moved) { |