13
0

new tempo mapping drag objects (API and implementation)

This commit is contained in:
Paul Davis 2023-03-11 20:42:57 -07:00
parent 6d5273e514
commit 77cefd7721
5 changed files with 490 additions and 75 deletions

View File

@ -2523,6 +2523,8 @@ private:
void remove_gap_marker_callback (Temporal::timepos_t at, Temporal::timecnt_t distance);
void choose_mapping_drag (ArdourCanvas::Item*, GdkEvent*);
template<typename T>
Temporal::TimeDomain drag_time_domain (T* thing_with_time_domain) {
return thing_with_time_domain ? thing_with_time_domain->time_domain() : Temporal::AudioTime;
@ -2538,7 +2540,9 @@ private:
friend class RegionDrag;
friend class RegionMoveDrag;
friend class TrimDrag;
friend class MappingDrag;
friend class MappingTwistDrag;
friend class MappingLinearDrag;
friend class MappingStretchDrag;
friend class MeterMarkerDrag;
friend class BBTMarkerDrag;
friend class TempoMarkerDrag;

View File

@ -261,6 +261,7 @@ Editor::initialize_canvas ()
tempo_bar->Event.connect (sigc::bind (sigc::mem_fun (*this, &Editor::canvas_ruler_bar_event), tempo_bar, TempoBarItem, "tempo bar"));
mapping_bar->Event.connect (sigc::bind (sigc::mem_fun (*this, &Editor::canvas_ruler_bar_event), mapping_bar, MappingBarItem, "mapping bar"));
mapping_cursor->Event.connect (sigc::bind (sigc::mem_fun (*this, &Editor::canvas_ruler_bar_event), mapping_cursor, MappingCursorItem, "mapping cursor"));
meter_bar->Event.connect (sigc::bind (sigc::mem_fun (*this, &Editor::canvas_ruler_bar_event), meter_bar, MeterBarItem, "meter bar"));
marker_bar->Event.connect (sigc::bind (sigc::mem_fun (*this, &Editor::canvas_ruler_bar_event), marker_bar, MarkerBarItem, "marker bar"));
cd_marker_bar->Event.connect (sigc::bind (sigc::mem_fun (*this, &Editor::canvas_ruler_bar_event), cd_marker_bar, CdMarkerBarItem, "cd marker bar"));

View File

@ -3331,7 +3331,6 @@ TempoMarkerDrag::motion (GdkEvent* event, bool first_move)
}
}
void
TempoMarkerDrag::finished (GdkEvent* event, bool movement_occurred)
{
@ -3475,32 +3474,34 @@ BBTMarkerDrag::aborted (bool moved)
}
}
MappingDrag::MappingDrag (Editor* e, ArdourCanvas::Item* i)
/******************************************************************************/
MappingLinearDrag::MappingLinearDrag (Editor* e, ArdourCanvas::Item* i, Temporal::TempoMap::WritableSharedPtr& wmap)
: Drag (e, i, Temporal::BeatTime)
, _tempo (0)
, _grab_bpm (0)
, map (wmap)
, _before_state (0)
, _drag_valid (true)
{
DEBUG_TRACE (DEBUG::Drags, "New MappingDrag\n");
DEBUG_TRACE (DEBUG::Drags, "New MappingLinearDrag\n");
}
void
MappingDrag::start_grab (GdkEvent* event, Gdk::Cursor* cursor)
MappingLinearDrag::start_grab (GdkEvent* event, Gdk::Cursor* cursor)
{
map = _editor->begin_tempo_mapping ();
Drag::start_grab (event, cursor);
_tempo = const_cast<TempoPoint*> (&map->metric_at (raw_grab_time().beats()).tempo());
_grab_bpm = _tempo->note_types_per_minute();
if (adjusted_current_time (event, false) <= _tempo->time()) {
std::cerr << "too early for " << *_tempo << std::endl;
_drag_valid = false;
return;
}
_editor->tempo_curve_selected (_tempo, true);
ostringstream sstr;
if (_tempo->continuing()) {
TempoPoint const * prev = map->previous_tempo (*_tempo);
@ -3514,12 +3515,12 @@ MappingDrag::start_grab (GdkEvent* event, Gdk::Cursor* cursor)
}
void
MappingDrag::setup_pointer_offset ()
MappingLinearDrag::setup_pointer_offset ()
{
/* get current state */
_before_state = &map->get_state();
_grab_qn = max (Beats(), raw_grab_time().beats());
Beats grab_qn = max (Beats(), raw_grab_time().beats());
uint32_t divisions = _editor->get_grid_beat_divisions (_editor->grid_type());
@ -3527,52 +3528,33 @@ MappingDrag::setup_pointer_offset ()
divisions = 4;
}
_grab_qn = _grab_qn.round_to_subdivision (divisions, Temporal::RoundDownAlways);
_pointer_offset = timepos_t (_grab_qn).distance (raw_grab_time());
grab_qn = grab_qn.round_to_subdivision (divisions, Temporal::RoundDownAlways);
_pointer_offset = timepos_t (grab_qn).distance (raw_grab_time());
}
void
MappingDrag::motion (GdkEvent* event, bool first_move)
MappingLinearDrag::motion (GdkEvent* event, bool first_move)
{
if (!_drag_valid) {
return;
}
if (first_move) {
_editor->begin_reversible_command (_("stretch tempo"));
_editor->begin_reversible_command (_("map tempo"));
}
timepos_t pf;
if (_editor->grid_musical()) {
pf = adjusted_current_time (event, false);
} else {
pf = adjusted_current_time (event);
}
if (ArdourKeyboard::modifier_state_equals (event->button.state, Keyboard::PrimaryModifier)) {
/* adjust previous tempo to match pointer sample */
map->stretch_tempo (_tempo, timepos_t (_grab_qn).samples(), pf.samples(), _grab_qn, pf.beats());
_editor->mid_tempo_change (Editor::BBTChanged);
}
ostringstream sstr;
if (_tempo->continuing()) {
TempoPoint const * prev = map->previous_tempo (*_tempo);
if (prev) {
_editor->tempo_curve_selected (prev, true);
sstr << "end: " << fixed << setprecision(3) << prev->end_note_types_per_minute() << "\n";
}
}
sstr << "start: " << fixed << setprecision(3) << _tempo->note_types_per_minute();
show_verbose_cursor_text (sstr.str());
double new_bpm = std::max (1.5, _grab_bpm - ((current_pointer_x() - grab_x()) / 5.0));
stringstream strs;
Temporal::Tempo new_tempo (new_bpm, _tempo->note_type());
map->change_tempo (*_tempo, new_tempo);
_editor->mid_tempo_change (Editor::MappingChanged);
}
void
MappingDrag::finished (GdkEvent* event, bool movement_occurred)
MappingLinearDrag::finished (GdkEvent* event, bool movement_occurred)
{
if (!_drag_valid) {
_editor->abort_tempo_map_edit ();
_editor->abort_tempo_mapping ();
return;
}
@ -3580,21 +3562,12 @@ MappingDrag::finished (GdkEvent* event, bool movement_occurred)
/* click, no drag */
_editor->abort_tempo_map_edit ();
_editor->abort_tempo_mapping ();
_editor->session()->request_locate (grab_sample(), false, _was_rolling ? MustRoll : RollIfAppropriate);
return;
} else {
_editor->tempo_curve_selected (_tempo, false);
if (_tempo->continuing()) {
TempoPoint const * prev_tempo = map->previous_tempo (*_tempo);
if (prev_tempo) {
_editor->tempo_curve_selected (prev_tempo, false);
}
}
}
XMLNode &after = map->get_state();
@ -3612,7 +3585,253 @@ MappingDrag::finished (GdkEvent* event, bool movement_occurred)
}
void
MappingDrag::aborted (bool moved)
MappingLinearDrag::aborted (bool moved)
{
_editor->abort_tempo_mapping ();
}
/******************************************************************************/
MappingStretchDrag::MappingStretchDrag (Editor* e, ArdourCanvas::Item* i, Temporal::TempoMap::WritableSharedPtr& wmap)
: Drag (e, i, Temporal::BeatTime)
, _tempo (0)
, map (wmap)
, _before_state (0)
, _drag_valid (true)
{
DEBUG_TRACE (DEBUG::Drags, "New MappingStretchDrag\n");
}
void
MappingStretchDrag::start_grab (GdkEvent* event, Gdk::Cursor* cursor)
{
Drag::start_grab (event, cursor);
_tempo = const_cast<TempoPoint*> (&map->metric_at (raw_grab_time().beats()).tempo());
if (adjusted_current_time (event, false) <= _tempo->time()) {
std::cerr << "too early for " << *_tempo << std::endl;
_drag_valid = false;
return;
}
ostringstream sstr;
if (_tempo->continuing()) {
TempoPoint const * prev = map->previous_tempo (*_tempo);
if (prev) {
sstr << "end: " << fixed << setprecision(3) << prev->end_note_types_per_minute() << "\n";
}
}
sstr << "start: " << fixed << setprecision(3) << _tempo->note_types_per_minute();
show_verbose_cursor_text (sstr.str());
}
void
MappingStretchDrag::setup_pointer_offset ()
{
/* get current state */
_before_state = &map->get_state();
_grab_qn = max (Beats(), raw_grab_time().beats());
uint32_t divisions = _editor->get_grid_beat_divisions (_editor->grid_type());
if (divisions == 0) {
divisions = 4;
}
_grab_qn = _grab_qn.round_to_subdivision (divisions, Temporal::RoundDownAlways);
_pointer_offset = timepos_t (_grab_qn).distance (raw_grab_time());
}
void
MappingStretchDrag::motion (GdkEvent* event, bool first_move)
{
if (!_drag_valid) {
return;
}
if (first_move) {
_editor->begin_reversible_command (_("map tempo w/stretch"));
}
timepos_t pf;
if (_editor->grid_musical()) {
pf = adjusted_current_time (event, false);
} else {
pf = adjusted_current_time (event);
}
map->stretch_tempo (_tempo, timepos_t (_grab_qn).samples(), pf.samples(), _grab_qn, pf.beats());
_editor->mapping_cursor->set_position (Duple (_editor->time_to_pixel_unrounded (pf), _editor->mapping_cursor->position().y));
_editor->mid_tempo_change (Editor::MappingChanged);
}
void
MappingStretchDrag::finished (GdkEvent* event, bool movement_occurred)
{
if (!movement_occurred) {
/* click, no drag */
_editor->abort_tempo_mapping ();
_editor->session()->request_locate (grab_sample(), false, _was_rolling ? MustRoll : RollIfAppropriate);
return;
}
if (!_drag_valid) {
_editor->abort_tempo_mapping ();
return;
}
XMLNode &after = map->get_state();
_editor->session()->add_command (new Temporal::TempoCommand (_("move BBT point"), _before_state, &after));
_editor->commit_reversible_command ();
/* 2nd argument means "update tempo map display after the new map is
* installed. We need to do this because the code above has not
* actually changed anything about how tempo is displayed, it simply
* modified the map.
*/
_editor->commit_tempo_mapping (map);
}
void
MappingStretchDrag::aborted (bool moved)
{
_editor->abort_tempo_mapping ();
}
/******************************************************************************/
MappingTwistDrag::MappingTwistDrag (Editor* e, ArdourCanvas::Item* i, Temporal::TempoMap::WritableSharedPtr& wmap,
TempoPoint& prv,
TempoPoint& fcus,
TempoPoint& nxt)
: Drag (e, i, Temporal::BeatTime)
, prev (prv)
, focus (fcus)
, next (nxt)
, map (wmap)
, direction (0.)
, delta (0.)
, _before_state (0)
, _drag_valid (true)
{
DEBUG_TRACE (DEBUG::Drags, "New MappingTwistDrag\n");
initial_npm = focus.note_types_per_minute ();
}
void
MappingTwistDrag::start_grab (GdkEvent* event, Gdk::Cursor* cursor)
{
Drag::start_grab (event, cursor);
}
void
MappingTwistDrag::setup_pointer_offset ()
{
/* get current state */
_before_state = &map->get_state();
Beats grab_qn = max (Beats(), raw_grab_time().beats());
uint32_t divisions = _editor->get_grid_beat_divisions (_editor->grid_type());
if (divisions == 0) {
divisions = 4;
}
grab_qn = grab_qn.round_to_subdivision (divisions, Temporal::RoundDownAlways);
_pointer_offset = timepos_t (grab_qn).distance (raw_grab_time());
}
void
MappingTwistDrag::motion (GdkEvent* event, bool first_move)
{
if (first_move) {
_editor->begin_reversible_command (_("map tempo w/twist"));
}
samplepos_t mouse_pos;
if (_editor->grid_musical()) {
mouse_pos = adjusted_current_time (event, false).samples();
} else {
mouse_pos = adjusted_current_time (event).samples();
}
double pixels_moved = _drags->current_pointer_x() - last_pointer_x();
if (_drags->current_pointer_x() < last_pointer_x()) {
if (direction < 0.) {
direction = 1.;
initial_npm += delta;
delta = 0.;
}
} else {
if (direction >= 0.) {
direction = -1.;
initial_npm += delta;
delta = 0.;
}
}
if (_drags->current_pointer_time() >= timepos_t::from_superclock (next.sclock())) {
return;
}
if (_drags->current_pointer_time() <= timepos_t::from_superclock (prev.sclock())) {
return;
}
/* XXX needs to scale somehow with zoom level */
delta += 0.75 * (last_pointer_x() - _drags->current_pointer_x());
map->twist_tempi (prev, focus, next, initial_npm + delta);
// _editor->mapping_cursor->set_position (Duple (_editor->sample_to_pixel_unrounded (mouse_pos), _editor->mapping_cursor->position().y));
_editor->mid_tempo_change (Editor::MappingChanged);
}
void
MappingTwistDrag::finished (GdkEvent* event, bool movement_occurred)
{
if (!movement_occurred) {
/* click, no drag */
_editor->abort_tempo_mapping ();
_editor->session()->request_locate (grab_sample(), false, _was_rolling ? MustRoll : RollIfAppropriate);
return;
}
if (!_drag_valid) {
_editor->abort_tempo_mapping ();
return;
}
XMLNode &after = map->get_state();
_editor->session()->add_command (new Temporal::TempoCommand (_("move BBT point"), _before_state, &after));
_editor->commit_reversible_command ();
/* 2nd argument means "update tempo map display after the new map is
* installed. We need to do this because the code above has not
* actually changed anything about how tempo is displayed, it simply
* modified the map.
*/
_editor->commit_tempo_mapping (map);
}
void
MappingTwistDrag::aborted (bool moved)
{
_editor->abort_tempo_mapping ();
}
@ -3634,8 +3853,6 @@ TempoTwistDrag::start_grab (GdkEvent* event, Gdk::Cursor* cursor)
{
Drag::start_grab (event, cursor);
map = _editor->begin_tempo_mapping ();
/* Get the tempo point that starts this section */
_tempo = const_cast<TempoPoint*> (&map->tempo_at (raw_grab_time()));
@ -3685,7 +3902,7 @@ TempoTwistDrag::motion (GdkEvent* event, bool first_move)
}
/* adjust this and the next tempi to match pointer sample */
map->twist_tempi (_tempo, adjusted_time (grab_time(), 0, false).samples(), mouse_pos);
// map->twist_tempi (_tempo, adjusted_time (grab_time(), 0, false).samples(), mouse_pos);
ostringstream sstr;
sstr << "start: " << fixed << setprecision(3) << _tempo->note_types_per_minute() << "\n";

View File

@ -913,12 +913,10 @@ private:
XMLNode* _before_state;
};
/** BBT Ruler drag */
class MappingDrag : public Drag
class MappingLinearDrag : public Drag
{
public:
MappingDrag (Editor *, ArdourCanvas::Item *);
MappingLinearDrag (Editor *, ArdourCanvas::Item *, Temporal::TempoMap::WritableSharedPtr&);
void start_grab (GdkEvent *, Gdk::Cursor* c = 0);
void motion (GdkEvent *, bool);
@ -936,14 +934,82 @@ public:
void setup_pointer_offset ();
private:
Temporal::Beats _grab_qn;
Temporal::TempoPoint* _tempo;
double _grab_bpm;
Temporal::TempoMap::WritableSharedPtr map;
XMLNode* _before_state;
bool _drag_valid;
};
class MappingStretchDrag : public Drag
{
public:
MappingStretchDrag (Editor *, ArdourCanvas::Item *, Temporal::TempoMap::WritableSharedPtr&);
void start_grab (GdkEvent *, Gdk::Cursor* c = 0);
void motion (GdkEvent *, bool);
void finished (GdkEvent *, bool);
void aborted (bool);
bool allow_vertical_autoscroll () const {
return false;
}
bool y_movement_matters () const {
return false;
}
void setup_pointer_offset ();
private:
Temporal::TempoPoint* _tempo;
Temporal::TempoMap::WritableSharedPtr map;
Temporal::Beats _grab_qn;
XMLNode* _before_state;
bool _drag_valid;
};
class MappingTwistDrag : public Drag
{
public:
MappingTwistDrag (Editor *, ArdourCanvas::Item *, Temporal::TempoMap::WritableSharedPtr&,
Temporal::TempoPoint& prev,
Temporal::TempoPoint& focus,
Temporal::TempoPoint& next);
void start_grab (GdkEvent *, Gdk::Cursor* c = 0);
void motion (GdkEvent *, bool);
void finished (GdkEvent *, bool);
void aborted (bool);
bool allow_vertical_autoscroll () const {
return false;
}
bool y_movement_matters () const {
return false;
}
void setup_pointer_offset ();
private:
Temporal::TempoPoint& prev;
Temporal::TempoPoint& focus;
Temporal::TempoPoint& next;
double _grab_bpm;
Temporal::TempoMap::WritableSharedPtr map;
double direction;
double delta;
double initial_npm;
XMLNode* _before_state;
bool _drag_valid;
};
/** tempo curve twist drag */
class TempoTwistDrag : public Drag
{

View File

@ -817,14 +817,8 @@ Editor::button_press_handler_1 (ArdourCanvas::Item* item, GdkEvent* event, ItemT
break;
case MappingBarItem:
if (!Keyboard::modifier_state_equals (event->button.state, Keyboard::PrimaryModifier)
&& !ArdourKeyboard::indicates_constraint (event->button.state)) {
_drags->set (new CursorDrag (this, *_playhead_cursor, false), event);
} else if (Keyboard::modifier_state_equals (event->button.state, Keyboard::PrimaryModifier|Keyboard::TertiaryModifier)) {
_drags->set (new TempoTwistDrag (this, item), event);
} else if (Keyboard::modifier_state_contains (event->button.state, Keyboard::PrimaryModifier)) {
_drags->set (new MappingDrag (this, item), event);
}
case MappingCursorItem:
choose_mapping_drag (item, event);
return true;
case TempoBarItem:
@ -1698,6 +1692,7 @@ Editor::button_release_handler (ArdourCanvas::Item* item, GdkEvent* event, ItemT
case CdMarkerBarItem:
case TempoBarItem:
case MappingBarItem:
case MappingCursorItem:
case TempoCurveItem:
case MeterBarItem:
case VideoBarItem:
@ -1823,7 +1818,9 @@ Editor::button_release_handler (ArdourCanvas::Item* item, GdkEvent* event, ItemT
mouse_add_new_marker (where, Location::IsCueMarker);
}
return true;
case MappingBarItem:
case MappingCursorItem:
return true;
case TempoBarItem:
@ -1987,8 +1984,13 @@ Editor::enter_handler (ArdourCanvas::Item* item, GdkEvent* event, ItemType item_
choose_canvas_cursor_on_entry (item_type);
switch (item_type) {
case MappingCursorItem:
/* nothing to do ??? */
break;
case MappingBarItem:
mapping_cursor->show ();
mapping_cursor->raise_to_top ();
break;
case ControlPointItem:
@ -2136,6 +2138,10 @@ Editor::leave_handler (ArdourCanvas::Item* item, GdkEvent*, ItemType item_type)
}
switch (item_type) {
case MappingCursorItem:
/* ignore */
break;
case MappingBarItem:
mapping_cursor->hide ();
break;
@ -2324,9 +2330,11 @@ Editor::motion_handler (ArdourCanvas::Item* item, GdkEvent* event, bool from_aut
//drags change the snapped_cursor location, because we are snapping the thing being dragged, not the actual mouse cursor
return _drags->motion_handler (event, from_autoscroll);
} else {
bool ignored;
bool peaks_visible = false;
samplepos_t where;
if (mouse_sample (where, ignored)) {
/* display peaks */
@ -2338,14 +2346,48 @@ Editor::motion_handler (ArdourCanvas::Item* item, GdkEvent* event, bool from_aut
}
}
/* the snapped_cursor shows where an operation (like Split) is going to occur */
timepos_t t (where);
snap_to_with_modifier (t, event);
set_snapped_cursor_position (t);
bool move_snapped_cursor = true;
if (item == mapping_bar) {
double const new_pos = sample_to_pixel_unrounded (where);
mapping_cursor->set_center (ArdourCanvas::Duple (new_pos, mapping_cursor->center().y));
if (item == mapping_bar || item == mapping_cursor) {
/* Snap to the nearest beat, and figure out how
* many pixels from the pointer cursor that is.
*/
timepos_t snapped = _snap_to_bbt (t, RoundNearest, SnapToGrid_Unscaled, GridTypeBeat);
const double unsnapped_pos = time_to_pixel_unrounded (t);
const double snapped_pos = time_to_pixel_unrounded (snapped);
if (std::abs (snapped_pos - unsnapped_pos) < 10 * UIConfiguration::instance().get_ui_scale()) {
/* Close to a beat, so snap the mapping
* cursor *and* the snapped cursor to
* the beat.
*/
mapping_cursor->show ();
mapping_cursor->raise_to_top ();
mapping_cursor->set_position (ArdourCanvas::Duple (snapped_pos, mapping_cursor->position().y));
set_snapped_cursor_position (snapped);
move_snapped_cursor = false;
} else {
/* Not close to a beat, hide the
* mapping cursor, then move the
* snapped cursor as normal.
*/
mapping_cursor->hide ();
}
}
if (move_snapped_cursor) {
snap_to_with_modifier (t, event);
set_snapped_cursor_position (t);
}
}
@ -2901,3 +2943,88 @@ Editor::get_pointer_position (double& x, double& y) const
_track_canvas->get_pointer (px, py);
_track_canvas->window_to_canvas (px, py, x, y);
}
void
Editor::choose_mapping_drag (ArdourCanvas::Item* item, GdkEvent* event)
{
if (item != mapping_cursor && item != mapping_bar) {
return;
}
Temporal::TempoMap::WritableSharedPtr map = begin_tempo_mapping ();
if (item == mapping_bar) {
/* Drag on the bar, not the cursor: just adjust tempo up or
* down.
*/
_drags->set (new MappingLinearDrag (this, item, map), event);
std::cerr << ":Linear\n";
return;
}
/* Decide between a tempo twist drag, which we do if the
* pointer is between two tempo markers, and a tempo stretch
* drag, which we do if the pointer is after the last tempo
* marker before the end of the map or a BBT Marker.
*/
timepos_t pointer_time (canvas_event_sample (event, nullptr, nullptr));
Temporal::TempoPoint& tempo = const_cast<Temporal::TempoPoint&>(map->tempo_at (pointer_time));
TempoPoint* after = const_cast<TempoPoint*> (map->next_tempo (tempo));
if (!after || dynamic_cast<MusicTimePoint*>(after)) {
/* This is the final tempo, or the next one is a BBT marker.
* No twisting, just stretch this one.
*/
std::cerr << "stretch!\n";
_drags->set (new MappingStretchDrag (this, item, map), event);
std::cerr << ":Stretch\n";
return;
}
std::cerr << "Pointer time: " << pointer_time << std::endl;
BBT_Argument bbt = map->bbt_at (pointer_time);
std::cerr << " bbt " << bbt << std::endl;
bbt = BBT_Argument (bbt.reference(), bbt.round_to_beat ());
std::cerr << "rounded to " << bbt << " vs. " << tempo.bbt() << std::endl;
TempoPoint* before;
TempoPoint* focus;
if (tempo.bbt() < bbt) {
/* Add a new tempo marker at the nearest beat point
(essentially the snapped grab point for the drag), so that
it becomes the middle one of three used by the twist tempo
operation.
*/
before = const_cast<TempoPoint*> (&tempo);
Tempo copied_no_ramp (map->tempo_at (bbt));
TempoPoint& added = const_cast<TempoPoint&> (map->set_tempo (copied_no_ramp, bbt));
focus = &added;
std::cerr << "Focus will be " << *focus << std::endl;
reset_tempo_marks ();
} else {
before = const_cast<TempoPoint*> (map->previous_tempo (tempo));
if (!before) {
return;
}
focus = &tempo;
}
std::cerr << "Prev: " << *before << std::endl;
std::cerr << "Focus: " << *focus << std::endl;
std::cerr << "Next: " << *after << std::endl;
std::cerr << "focus says next at " << focus->superclock_at (after->beats()) << " vs. " << after->sclock() << std::endl;
_drags->set (new MappingTwistDrag (this, item, map, *before, *focus, *after), event);
std::cerr << ":Twist\n";
}