1 /**********************************************************************
2 
3 Audacity: A Digital Audio Editor
4 
5 TimeShiftHandle.cpp
6 
7 Paul Licameli split from TrackPanel.cpp
8 
9 **********************************************************************/
10 
11 
12 #include "TimeShiftHandle.h"
13 
14 #include "TrackView.h"
15 #include "AColor.h"
16 #include "../../HitTestResult.h"
17 #include "../../ProjectAudioIO.h"
18 #include "../../ProjectHistory.h"
19 #include "../../ProjectSettings.h"
20 #include "../../RefreshCode.h"
21 #include "../../Snap.h"
22 #include "../../Track.h"
23 #include "../../TrackArtist.h"
24 #include "../../TrackPanelDrawingContext.h"
25 #include "../../TrackPanelMouseEvent.h"
26 #include "../../UndoManager.h"
27 #include "ViewInfo.h"
28 #include "../../../images/Cursors.h"
29 
TimeShiftHandle(const std::shared_ptr<Track> & pTrack,bool gripHit)30 TimeShiftHandle::TimeShiftHandle
31 ( const std::shared_ptr<Track> &pTrack, bool gripHit )
32    : mGripHit{ gripHit }
33 {
34    mClipMoveState.mCapturedTrack = pTrack;
35 }
36 
GetTrack() const37 std::shared_ptr<Track> TimeShiftHandle::GetTrack() const
38 {
39    return mClipMoveState.mCapturedTrack;
40 }
41 
WasMoved() const42 bool TimeShiftHandle::WasMoved() const
43 {
44     return mDidSlideVertically || (mClipMoveState.initialized && mClipMoveState.wasMoved);
45 }
46 
Clicked() const47 bool TimeShiftHandle::Clicked() const
48 {
49    return mClipMoveState.initialized;
50 }
51 
Enter(bool,AudacityProject *)52 void TimeShiftHandle::Enter(bool, AudacityProject *)
53 {
54 #ifdef EXPERIMENTAL_TRACK_PANEL_HIGHLIGHTING
55    mChangeHighlight = RefreshCode::RefreshCell;
56 #endif
57 }
58 
HitPreview(const AudacityProject * WXUNUSED (pProject),bool unsafe)59 HitTestPreview TimeShiftHandle::HitPreview
60 (const AudacityProject *WXUNUSED(pProject), bool unsafe)
61 {
62    static auto disabledCursor =
63       ::MakeCursor(wxCURSOR_NO_ENTRY, DisabledCursorXpm, 16, 16);
64    static auto slideCursor =
65       MakeCursor(wxCURSOR_SIZEWE, TimeCursorXpm, 16, 16);
66    // TODO: Should it say "track or clip" ?  Non-wave tracks can move, or clips in a wave track.
67    // TODO: mention effects of shift (move all clips of selected wave track) and ctrl (move vertically only) ?
68    //  -- but not all of that is available in multi tool.
69    auto message = XO("Click and drag to move a track in time");
70 
71    return {
72       message,
73       (unsafe
74        ? &*disabledCursor
75        : &*slideCursor)
76    };
77 }
78 
HitAnywhere(std::weak_ptr<TimeShiftHandle> & holder,const std::shared_ptr<Track> & pTrack,bool gripHit)79 UIHandlePtr TimeShiftHandle::HitAnywhere
80 (std::weak_ptr<TimeShiftHandle> &holder,
81  const std::shared_ptr<Track> &pTrack, bool gripHit)
82 {
83    auto result = std::make_shared<TimeShiftHandle>( pTrack, gripHit );
84    result = AssignUIHandlePtr(holder, result);
85    return result;
86 }
87 
HitTest(std::weak_ptr<TimeShiftHandle> & holder,const wxMouseState & state,const wxRect & rect,const std::shared_ptr<Track> & pTrack)88 UIHandlePtr TimeShiftHandle::HitTest
89 (std::weak_ptr<TimeShiftHandle> &holder,
90  const wxMouseState &state, const wxRect &rect,
91  const std::shared_ptr<Track> &pTrack)
92 {
93    /// method that tells us if the mouse event landed on a
94    /// time-slider that allows us to time shift the sequence.
95    /// (Those are the two "grips" drawn at left and right edges for multi tool mode.)
96 
97    // Perhaps we should delegate this to TrackArtist as only TrackArtist
98    // knows what the real sizes are??
99 
100    // The drag Handle width includes border, width and a little extra margin.
101    const int adjustedDragHandleWidth = 14;
102    // The hotspot for the cursor isn't at its centre.  Adjust for this.
103    const int hotspotOffset = 5;
104 
105    // We are doing an approximate test here - is the mouse in the right or left border?
106    if (!(state.m_x + hotspotOffset < rect.x + adjustedDragHandleWidth ||
107        state.m_x + hotspotOffset >= rect.x + rect.width - adjustedDragHandleWidth))
108       return {};
109 
110    return HitAnywhere( holder, pTrack, true );
111 }
112 
~TimeShiftHandle()113 TimeShiftHandle::~TimeShiftHandle()
114 {
115 }
116 
DoHorizontalOffset(double offset)117 void ClipMoveState::DoHorizontalOffset( double offset )
118 {
119    if ( !shifters.empty() ) {
120       for ( auto &pair : shifters )
121          pair.second->DoHorizontalOffset( offset );
122    }
123    else {
124       for (auto channel : TrackList::Channels( mCapturedTrack.get() ))
125          channel->Offset( offset );
126    }
127 }
128 
129 TrackShifter::TrackShifter() = default;
130 
131 TrackShifter::~TrackShifter() = default;
132 
UnfixIntervals(std::function<bool (const TrackInterval &)> pred)133 void TrackShifter::UnfixIntervals(
134    std::function< bool( const TrackInterval& ) > pred )
135 {
136    for ( auto iter = mFixed.begin(); iter != mFixed.end(); ) {
137       if ( pred( *iter) ) {
138          mMoving.push_back( std::move( *iter ) );
139          iter = mFixed.erase( iter );
140          mAllFixed = false;
141       }
142       else
143          ++iter;
144    }
145 }
146 
UnfixAll()147 void TrackShifter::UnfixAll()
148 {
149    std::move( mFixed.begin(), mFixed.end(), std::back_inserter(mMoving) );
150    mFixed = Intervals{};
151    mAllFixed = false;
152 }
153 
SelectInterval(const TrackInterval &)154 void TrackShifter::SelectInterval( const TrackInterval & )
155 {
156    UnfixAll();
157 }
158 
CommonSelectInterval(const TrackInterval & interval)159 void TrackShifter::CommonSelectInterval(const TrackInterval &interval)
160 {
161    UnfixIntervals( [&](auto &myInterval){
162       return !(interval.End() < myInterval.Start() ||
163                myInterval.End() < interval.Start());
164    });
165 }
166 
HintOffsetLarger(double desiredOffset)167 double TrackShifter::HintOffsetLarger(double desiredOffset)
168 {
169    return desiredOffset;
170 }
171 
QuantizeOffset(double desiredOffset)172 double TrackShifter::QuantizeOffset(double desiredOffset)
173 {
174    return desiredOffset;
175 }
176 
AdjustOffsetSmaller(double desiredOffset)177 double TrackShifter::AdjustOffsetSmaller(double desiredOffset)
178 {
179    return desiredOffset;
180 }
181 
MayMigrateTo(Track &)182 bool TrackShifter::MayMigrateTo(Track &)
183 {
184    return false;
185 }
186 
CommonMayMigrateTo(Track & otherTrack)187 bool TrackShifter::CommonMayMigrateTo(Track &otherTrack)
188 {
189    auto &track = GetTrack();
190 
191    // Both tracks need to be owned to decide this
192    auto pMyList = track.GetOwner().get();
193    auto pOtherList = otherTrack.GetOwner().get();
194    if (pMyList && pOtherList) {
195 
196       // Can migrate to another track of the same kind...
197       if ( otherTrack.SameKindAs( track ) ) {
198 
199          // ... with the same number of channels ...
200          auto myChannels = TrackList::Channels( &track );
201          auto otherChannels = TrackList::Channels( &otherTrack );
202          if (myChannels.size() == otherChannels.size()) {
203 
204             // ... and where this track and the other have corresponding
205             // positions
206             return myChannels.size() == 1 ||
207                std::distance(myChannels.first, pMyList->Find(&track)) ==
208                std::distance(otherChannels.first, pOtherList->Find(&otherTrack));
209 
210          }
211 
212       }
213 
214    }
215    return false;
216 }
217 
Detach()218 auto TrackShifter::Detach() -> Intervals
219 {
220    return {};
221 }
222 
AdjustFit(const Track &,const Intervals &,double &,double)223 bool TrackShifter::AdjustFit(
224    const Track &, const Intervals&, double &, double)
225 {
226    return false;
227 }
228 
Attach(Intervals)229 bool TrackShifter::Attach( Intervals )
230 {
231    return true;
232 }
233 
FinishMigration()234 bool TrackShifter::FinishMigration()
235 {
236    return true;
237 }
238 
DoHorizontalOffset(double offset)239 void TrackShifter::DoHorizontalOffset( double offset )
240 {
241    if (!AllFixed())
242       GetTrack().Offset( offset );
243 }
244 
AdjustT0(double t0) const245 double TrackShifter::AdjustT0(double t0) const
246 {
247    return t0;
248 }
249 
InitIntervals()250 void TrackShifter::InitIntervals()
251 {
252    mMoving.clear();
253    mFixed = GetTrack().GetIntervals();
254 }
255 
CoarseTrackShifter(Track & track)256 CoarseTrackShifter::CoarseTrackShifter( Track &track )
257    : mpTrack{ track.SharedPointer() }
258 {
259    InitIntervals();
260 }
261 
262 CoarseTrackShifter::~CoarseTrackShifter() = default;
263 
HitTest(double,const ViewInfo &,HitTestParams *)264 auto CoarseTrackShifter::HitTest(
265    double, const ViewInfo&, HitTestParams* ) -> HitTestResult
266 {
267    return HitTestResult::Track;
268 }
269 
SyncLocks()270 bool CoarseTrackShifter::SyncLocks()
271 {
272    return false;
273 }
274 
DEFINE_ATTACHED_VIRTUAL(MakeTrackShifter)275 DEFINE_ATTACHED_VIRTUAL(MakeTrackShifter) {
276    return [](Track &track, AudacityProject&) {
277       return std::make_unique<CoarseTrackShifter>(track);
278    };
279 }
280 
Init(AudacityProject & project,Track & capturedTrack,TrackShifter::HitTestResult hitTestResult,std::unique_ptr<TrackShifter> pHit,double clickTime,const ViewInfo & viewInfo,TrackList & trackList,bool syncLocked)281 void ClipMoveState::Init(
282    AudacityProject &project,
283    Track &capturedTrack,
284    TrackShifter::HitTestResult hitTestResult,
285    std::unique_ptr<TrackShifter> pHit,
286    double clickTime,
287    const ViewInfo &viewInfo,
288    TrackList &trackList, bool syncLocked )
289 {
290    shifters.clear();
291 
292    initialized = true;
293 
294    auto &state = *this;
295    state.mCapturedTrack = capturedTrack.SharedPointer();
296 
297    switch (hitTestResult) {
298       case TrackShifter::HitTestResult::Miss:
299          wxASSERT(false);
300          pHit.reset();
301          break;
302       case TrackShifter::HitTestResult::Track:
303          pHit.reset();
304          break;
305       case TrackShifter::HitTestResult::Intervals:
306          break;
307       case TrackShifter::HitTestResult::Selection:
308          state.movingSelection = true;
309          break;
310       default:
311          break;
312    }
313 
314    if (!pHit)
315       return;
316 
317    state.shifters[&capturedTrack] = std::move( pHit );
318 
319    // Collect TrackShifters for the rest of the tracks
320    for ( auto track : trackList.Any() ) {
321       auto &pShifter = state.shifters[track];
322       if (!pShifter)
323          pShifter = MakeTrackShifter::Call( *track, project );
324    }
325 
326    if ( state.movingSelection ) {
327       // All selected tracks may move some intervals
328       const TrackInterval interval{
329          viewInfo.selectedRegion.t0(),
330          viewInfo.selectedRegion.t1()
331       };
332       for ( const auto &pair : state.shifters ) {
333          auto &shifter = *pair.second;
334          auto &track = shifter.GetTrack();
335          if (&track == &capturedTrack)
336             // Don't change the choice of intervals made by HitTest
337             continue;
338          if ( track.IsSelected() )
339             shifter.SelectInterval( interval );
340       }
341    }
342    else {
343       // Move intervals only of the chosen channel group
344 
345       auto selectIntervals = [&](const TrackShifter::Intervals& intervals) {
346          for (auto channel : TrackList::Channels(&capturedTrack)) {
347             auto& shifter = *state.shifters[channel];
348             if (channel != &capturedTrack)
349             {
350                for (auto& interval : intervals)
351                {
352                   shifter.SelectInterval(interval);
353                }
354             }
355          }
356       };
357       if (capturedTrack.GetLinkType() == Track::LinkType::Aligned ||
358          capturedTrack.IsAlignedWithLeader())
359          //for aligned tracks we always match the whole clip that was
360          //positively hit tested
361          selectIntervals(state.shifters[&capturedTrack]->MovingIntervals());
362       else
363       {
364          TrackShifter::Intervals intervals;
365          intervals.emplace_back(TrackInterval { clickTime, clickTime });
366          //for not align, match clips from other channels that are
367          //exactly at clicked time point
368          selectIntervals(intervals);
369       }
370    }
371 
372    // Sync lock propagation of unfixing of intervals
373    if ( syncLocked ) {
374       bool change = true;
375       while( change ) {
376          change = false;
377 
378          // Iterate over all unfixed intervals in all tracks
379          // that do propagation and are in sync lock groups ...
380          for ( auto &pair : state.shifters ) {
381             auto &shifter = *pair.second.get();
382             if (!shifter.SyncLocks())
383                continue;
384             auto &track = shifter.GetTrack();
385             auto group = TrackList::SyncLockGroup(&track);
386             if ( group.size() <= 1 )
387                continue;
388 
389             auto &intervals = shifter.MovingIntervals();
390             for (auto &interval : intervals) {
391 
392                // ...and tell all other tracks in the sync lock group
393                // to select that interval...
394                for ( auto pTrack2 : group ) {
395                   if (pTrack2 == &track)
396                      continue;
397 
398                   auto &shifter2 = *shifters[pTrack2];
399                   auto size = shifter2.MovingIntervals().size();
400                   shifter2.SelectInterval( interval );
401                   change = change ||
402                      (shifter2.SyncLocks() &&
403                       size != shifter2.MovingIntervals().size());
404                }
405 
406             }
407          }
408 
409          // ... and repeat if any other interval became unfixed in a
410          // shifter that propagates
411       }
412    }
413 }
414 
CapturedInterval() const415 const TrackInterval *ClipMoveState::CapturedInterval() const
416 {
417    auto pTrack = mCapturedTrack.get();
418    if ( pTrack ) {
419       auto iter = shifters.find( pTrack );
420       if ( iter != shifters.end() ) {
421          auto &pShifter = iter->second;
422          if ( pShifter ) {
423             auto &intervals = pShifter->MovingIntervals();
424             if ( !intervals.empty() )
425                return &intervals[0];
426          }
427       }
428    }
429    return nullptr;
430 }
431 
DoSlideHorizontal(double desiredSlideAmount)432 double ClipMoveState::DoSlideHorizontal( double desiredSlideAmount )
433 {
434    auto &state = *this;
435    auto &capturedTrack = *state.mCapturedTrack;
436 
437    // Given a signed slide distance, move clips, but subject to constraint of
438    // non-overlapping with other clips, so the distance may be adjusted toward
439    // zero.
440    if ( !state.shifters.empty() ) {
441       double initialAllowed = 0;
442       do { // loop to compute allowed, does not actually move anything yet
443          initialAllowed = desiredSlideAmount;
444 
445          for (auto &pair : shifters) {
446             auto newAmount = pair.second->AdjustOffsetSmaller( desiredSlideAmount );
447             if ( desiredSlideAmount != newAmount ) {
448                if ( newAmount * desiredSlideAmount < 0 ||
449                     fabs(newAmount) > fabs(desiredSlideAmount) ) {
450                   wxASSERT( false ); // AdjustOffsetSmaller didn't honor postcondition!
451                   newAmount = 0; // Be sure the loop progresses to termination!
452                }
453                desiredSlideAmount = newAmount;
454                state.snapLeft = state.snapRight = -1; // see bug 1067
455             }
456             if (newAmount == 0)
457                break;
458          }
459       } while ( desiredSlideAmount != initialAllowed );
460    }
461 
462    // Whether moving intervals or a whole track,
463    // finally, here is where clips are moved
464    if ( desiredSlideAmount != 0.0 )
465       state.DoHorizontalOffset( desiredSlideAmount );
466 
467    //attempt to move a clip is counted to
468    wasMoved = true;
469 
470    return (state.hSlideAmount = desiredSlideAmount);
471 }
472 
473 namespace {
FindCandidates(const TrackList & tracks,const ClipMoveState::ShifterMap & shifters)474 SnapPointArray FindCandidates(
475    const TrackList &tracks, const ClipMoveState::ShifterMap &shifters )
476 {
477    // Compare with the other function FindCandidates in Snap
478    // Make the snap manager more selective than it would be if just constructed
479    // from the track list
480    SnapPointArray candidates;
481    for ( const auto &pair : shifters ) {
482       auto &shifter = pair.second;
483       auto &track = shifter->GetTrack();
484       for (const auto &interval : shifter->FixedIntervals() ) {
485          candidates.emplace_back( interval.Start(), &track );
486          if ( interval.Start() != interval.End() )
487             candidates.emplace_back( interval.End(), &track );
488       }
489    }
490    return candidates;
491 }
492 }
493 
Click(const TrackPanelMouseEvent & evt,AudacityProject * pProject)494 UIHandle::Result TimeShiftHandle::Click
495 (const TrackPanelMouseEvent &evt, AudacityProject *pProject)
496 {
497    using namespace RefreshCode;
498    const bool unsafe = ProjectAudioIO::Get( *pProject ).IsAudioActive();
499    if ( unsafe )
500       return Cancelled;
501 
502    const wxMouseEvent &event = evt.event;
503    const wxRect &rect = evt.rect;
504    auto &viewInfo = ViewInfo::Get( *pProject );
505 
506    const auto pView = std::static_pointer_cast<TrackView>(evt.pCell);
507    const auto pTrack = pView ? pView->FindTrack().get() : nullptr;
508    if (!pTrack)
509       return RefreshCode::Cancelled;
510 
511    auto &trackList = TrackList::Get( *pProject );
512 
513    mClipMoveState.clear();
514    mDidSlideVertically = false;
515 
516    const bool multiToolModeActive =
517       (ToolCodes::multiTool == ProjectSettings::Get( *pProject ).GetTool());
518 
519    const double clickTime =
520       viewInfo.PositionToTime(event.m_x, rect.x);
521 
522    auto pShifter = MakeTrackShifter::Call( *pTrack, *pProject );
523 
524    auto hitTestResult = TrackShifter::HitTestResult::Track;
525    if (!event.ShiftDown()) {
526       TrackShifter::HitTestParams params{
527          rect, event.m_x, event.m_y
528       };
529       hitTestResult = pShifter->HitTest( clickTime, viewInfo, &params );
530       switch( hitTestResult ) {
531       case TrackShifter::HitTestResult::Miss:
532          return Cancelled;
533       default:
534          break;
535       }
536    }
537    else {
538       // just do shifting of one whole track
539    }
540 
541    mClipMoveState.Init( *pProject, *pTrack,
542       hitTestResult,
543       std::move( pShifter ),
544       clickTime,
545 
546       viewInfo, trackList,
547       ProjectSettings::Get( *pProject ).IsSyncLocked() );
548 
549    mSlideUpDownOnly = event.CmdDown() && !multiToolModeActive;
550    mRect = rect;
551    mClipMoveState.mMouseClickX = event.m_x;
552    mSnapManager =
553    std::make_shared<SnapManager>(*trackList.GetOwner(),
554        FindCandidates( trackList, mClipMoveState.shifters ),
555        viewInfo,
556        true, // don't snap to time
557        kPixelTolerance);
558    mClipMoveState.snapLeft = -1;
559    mClipMoveState.snapRight = -1;
560    auto pInterval = mClipMoveState.CapturedInterval();
561    mSnapPreferRightEdge = pInterval &&
562       (fabs(clickTime - pInterval->End()) <
563        fabs(clickTime - pInterval->Start()));
564 
565    return RefreshNone;
566 }
567 
568 namespace {
FindDesiredSlideAmount(const ViewInfo & viewInfo,wxCoord xx,const wxMouseEvent & event,SnapManager * pSnapManager,bool slideUpDownOnly,bool snapPreferRightEdge,ClipMoveState & state,Track & track)569    double FindDesiredSlideAmount(
570       const ViewInfo &viewInfo, wxCoord xx, const wxMouseEvent &event,
571       SnapManager *pSnapManager,
572       bool slideUpDownOnly, bool snapPreferRightEdge,
573       ClipMoveState &state,
574       Track &track )
575    {
576       auto &capturedTrack = *state.mCapturedTrack;
577       if (slideUpDownOnly)
578          return 0.0;
579       else {
580          double desiredSlideAmount =
581             viewInfo.PositionToTime(event.m_x) -
582             viewInfo.PositionToTime(state.mMouseClickX);
583          double clipLeft = 0, clipRight = 0;
584 
585          if (!state.shifters.empty())
586             desiredSlideAmount =
587                state.shifters[ &track ]->QuantizeOffset( desiredSlideAmount );
588 
589          // Adjust desiredSlideAmount using SnapManager
590          if (pSnapManager) {
591             auto pInterval = state.CapturedInterval();
592             if (pInterval) {
593                clipLeft = pInterval->Start() + desiredSlideAmount;
594                clipRight = pInterval->End() + desiredSlideAmount;
595             }
596             else {
597                clipLeft = capturedTrack.GetStartTime() + desiredSlideAmount;
598                clipRight = capturedTrack.GetEndTime() + desiredSlideAmount;
599             }
600 
601             auto results =
602                pSnapManager->Snap(&capturedTrack, clipLeft, false);
603             auto newClipLeft = results.outTime;
604             results =
605                pSnapManager->Snap(&capturedTrack, clipRight, false);
606             auto newClipRight = results.outTime;
607 
608             // Only one of them is allowed to snap
609             if (newClipLeft != clipLeft && newClipRight != clipRight) {
610                // Un-snap the un-preferred edge
611                if (snapPreferRightEdge)
612                   newClipLeft = clipLeft;
613                else
614                   newClipRight = clipRight;
615             }
616 
617             // Take whichever one snapped (if any) and compute the NEW desiredSlideAmount
618             state.snapLeft = -1;
619             state.snapRight = -1;
620             if (newClipLeft != clipLeft) {
621                const double difference = (newClipLeft - clipLeft);
622                desiredSlideAmount += difference;
623                state.snapLeft =
624                   viewInfo.TimeToPosition(newClipLeft, xx);
625             }
626             else if (newClipRight != clipRight) {
627                const double difference = (newClipRight - clipRight);
628                desiredSlideAmount += difference;
629                state.snapRight =
630                   viewInfo.TimeToPosition(newClipRight, xx);
631             }
632          }
633          return desiredSlideAmount;
634       }
635    }
636 
637    using Correspondence = std::unordered_map< Track*, Track* >;
638 
FindCorrespondence(Correspondence & correspondence,TrackList & trackList,Track & capturedTrack,Track & track,ClipMoveState & state)639    bool FindCorrespondence(
640       Correspondence &correspondence,
641       TrackList &trackList, Track &capturedTrack, Track &track,
642       ClipMoveState &state)
643    {
644       if (state.shifters.empty())
645          // Shift + Dragging hasn't yet supported vertical movement
646          return false;
647 
648       // Accumulate new pairs for the correspondence, and merge them
649       // into the given correspondence only on success
650       Correspondence newPairs;
651 
652       auto sameType = [&]( auto pTrack ){
653          return capturedTrack.SameKindAs( *pTrack );
654       };
655       if (!sameType(&track))
656          return false;
657 
658       // All tracks of the same kind as the captured track
659       auto range = trackList.Any() + sameType;
660 
661       // Find how far this track would shift down among those (signed)
662       const auto myPosition =
663          std::distance( range.first, trackList.Find( &capturedTrack ) );
664       const auto otherPosition =
665          std::distance( range.first, trackList.Find( &track ) );
666       auto diff = otherPosition - myPosition;
667 
668       // Point to destination track
669       auto iter = range.first.advance( diff > 0 ? diff : 0 );
670 
671       for (auto pTrack : range) {
672          auto &pShifter = state.shifters[pTrack];
673          if ( !pShifter->MovingIntervals().empty() ) {
674             // One of the interesting tracks
675 
676             auto pOther = *iter;
677             if ( diff < 0 || !pOther )
678                // No corresponding track
679                return false;
680 
681             if ( !pShifter->MayMigrateTo(*pOther) )
682                // Rejected for other reason
683                return false;
684 
685             if ( correspondence.count(pTrack) )
686                // Don't overwrite the given correspondence
687                return false;
688 
689             newPairs[ pTrack ] = pOther;
690          }
691 
692          if ( diff < 0 )
693             ++diff; // Still consuming initial tracks
694          else
695             ++iter; // Safe to increment TrackIter even at end of range
696       }
697 
698       // Success
699       if (correspondence.empty())
700          correspondence.swap(newPairs);
701       else
702          std::copy( newPairs.begin(), newPairs.end(),
703             std::inserter( correspondence, correspondence.end() ) );
704       return true;
705    }
706 
707    using DetachedIntervals =
708       std::unordered_map<Track*, TrackShifter::Intervals>;
709 
CheckFit(ClipMoveState & state,const Correspondence & correspondence,const DetachedIntervals & intervals,double tolerance,double & desiredSlideAmount)710    bool CheckFit(
711       ClipMoveState &state, const Correspondence &correspondence,
712       const DetachedIntervals &intervals,
713       double tolerance, double &desiredSlideAmount )
714    {
715       bool ok = true;
716       double firstTolerance = tolerance;
717 
718       // The desiredSlideAmount may change and the tolerance may get used up.
719       for ( unsigned iPass = 0; iPass < 2 && ok; ++iPass ) {
720          for ( auto &pair : state.shifters ) {
721             auto *pSrcTrack = pair.first;
722             auto iter = correspondence.find( pSrcTrack );
723             if ( iter != correspondence.end() )
724                if( auto *pOtherTrack = iter->second )
725                   if ( !(ok = pair.second->AdjustFit(
726                      *pOtherTrack, intervals.at(pSrcTrack),
727                      desiredSlideAmount /*in,out*/, tolerance)) )
728                      break;
729          }
730 
731          // If it fits ok, desiredSlideAmount could have been updated to get
732          // the clip to fit.
733          // Check again, in the new position, this time with zero tolerance.
734          if (firstTolerance == 0)
735             break;
736          else
737             tolerance = 0.0;
738       }
739 
740       return ok;
741    }
742 
MigrationFailure()743    [[noreturn]] void MigrationFailure() {
744       // Tracks may be in an inconsistent state; throw to the application
745       // handler which restores consistency from undo history
746       throw SimpleMessageBoxException{ ExceptionType::Internal,
747          XO("Could not shift between tracks")};
748    }
749 
750    struct TemporaryClipRemover {
TemporaryClipRemover__anon07b69c570511::TemporaryClipRemover751       TemporaryClipRemover( ClipMoveState &clipMoveState )
752          : state( clipMoveState )
753       {
754          // Pluck the moving clips out of their tracks
755          for (auto &pair : state.shifters)
756             detached[pair.first] = pair.second->Detach();
757       }
758 
Reinsert__anon07b69c570511::TemporaryClipRemover759       void Reinsert(
760          std::unordered_map< Track*, Track* > *pCorrespondence )
761       {
762          for (auto &pair : detached) {
763             auto pTrack = pair.first;
764             if (pCorrespondence && pCorrespondence->count(pTrack))
765                pTrack = (*pCorrespondence)[pTrack];
766             auto &pShifter = state.shifters[pTrack];
767             if (!pShifter->Attach( std::move( pair.second ) ))
768                MigrationFailure();
769          }
770       }
771 
772       ClipMoveState &state;
773       DetachedIntervals detached;
774    };
775 }
776 
DoSlideVertical(ViewInfo & viewInfo,wxCoord xx,ClipMoveState & state,TrackList & trackList,Track & dstTrack,double & desiredSlideAmount)777 bool TimeShiftHandle::DoSlideVertical
778 ( ViewInfo &viewInfo, wxCoord xx,
779   ClipMoveState &state, TrackList &trackList,
780   Track &dstTrack, double &desiredSlideAmount )
781 {
782    Correspondence correspondence;
783 
784    // See if captured track corresponds to another
785    auto &capturedTrack = *state.mCapturedTrack;
786    if (!FindCorrespondence(
787       correspondence, trackList, capturedTrack, dstTrack, state ))
788       return false;
789 
790    // Try to extend the correpondence
791    auto tryExtend = [&](bool forward){
792       auto begin = trackList.begin(), end = trackList.end();
793       auto pCaptured = trackList.Find( &capturedTrack );
794       auto pDst = trackList.Find( &dstTrack );
795       // Scan for more correspondences
796       while ( true ) {
797          // Remember that TrackIter wraps circularly to the end iterator when
798          // decrementing it
799 
800          // First move to a track with moving intervals and
801          // without a correspondent
802          do
803             forward ? ++pCaptured : --pCaptured;
804          while ( pCaptured != end &&
805             ( correspondence.count(*pCaptured) || state.shifters[*pCaptured]->MovingIntervals().empty() ) );
806          if ( pCaptured == end )
807             break;
808 
809          // Change the choice of possible correspondent track too
810          do
811             forward ? ++pDst : --pDst;
812          while ( pDst != end && correspondence.count(*pDst) );
813          if ( pDst == end )
814             break;
815 
816          // Make correspondence if we can
817          if (!FindCorrespondence(
818             correspondence, trackList, **pCaptured, **pDst, state ))
819             break;
820       }
821    };
822    // Try extension, backward first, then forward
823    // (anticipating the case of dragging a label that is under a clip)
824    tryExtend(false);
825    tryExtend(true);
826 
827    // Having passed that test, remove clips temporarily from their
828    // tracks, so moving clips don't interfere with each other
829    // when we call CanInsertClip()
830    TemporaryClipRemover remover{ state };
831 
832    // Now check that the move is possible
833    double slide = desiredSlideAmount; // remember amount requested.
834    // The test for tolerance will need review with FishEye!
835    // The tolerance is supposed to be the time for one pixel,
836    // i.e. one pixel tolerance at current zoom.
837    double tolerance =
838       viewInfo.PositionToTime(xx + 1) - viewInfo.PositionToTime(xx);
839    bool ok = CheckFit( state, correspondence, remover.detached,
840       tolerance, desiredSlideAmount /*in,out*/ );
841 
842    if (!ok) {
843       // Failure, even with using tolerance.
844       remover.Reinsert( nullptr );
845       return false;
846    }
847 
848    // Make the offset permanent; start from a "clean slate"
849    state.mMouseClickX = xx;
850    remover.Reinsert( &correspondence );
851    return true;
852 }
853 
Drag(const TrackPanelMouseEvent & evt,AudacityProject * pProject)854 UIHandle::Result TimeShiftHandle::Drag
855 (const TrackPanelMouseEvent &evt, AudacityProject *pProject)
856 {
857    using namespace RefreshCode;
858    const bool unsafe = ProjectAudioIO::Get( *pProject ).IsAudioActive();
859    if (unsafe) {
860       this->Cancel(pProject);
861       return RefreshAll | Cancelled;
862    }
863 
864    const wxMouseEvent &event = evt.event;
865    auto &viewInfo = ViewInfo::Get( *pProject );
866 
867    TrackView *trackView = dynamic_cast<TrackView*>(evt.pCell.get());
868    Track *track = trackView ? trackView->FindTrack().get() : nullptr;
869 
870    // Uncommenting this permits drag to continue to work even over the controls area
871    /*
872    track = static_cast<CommonTrackPanelCell*>(evt.pCell)->FindTrack().get();
873    */
874 
875    if (!track) {
876       // Allow sliding if the pointer is not over any track, but only if x is
877       // within the bounds of the tracks area.
878       if (event.m_x >= mRect.GetX() &&
879          event.m_x < mRect.GetX() + mRect.GetWidth())
880           track = mClipMoveState.mCapturedTrack.get();
881    }
882 
883    // May need a shared_ptr to reassign mCapturedTrack below
884    auto pTrack = Track::SharedPointer( track );
885    if (!pTrack)
886       return RefreshCode::RefreshNone;
887 
888 
889    auto &trackList = TrackList::Get( *pProject );
890 
891    // GM: slide now implementing snap-to
892    // samples functionality based on sample rate.
893 
894    // Start by undoing the current slide amount; everything
895    // happens relative to the original horizontal position of
896    // each clip...
897    mClipMoveState.DoHorizontalOffset( -mClipMoveState.hSlideAmount );
898 
899    if ( mClipMoveState.movingSelection ) {
900       // Slide the selection, too
901       viewInfo.selectedRegion.move( -mClipMoveState.hSlideAmount );
902    }
903    mClipMoveState.hSlideAmount = 0.0;
904 
905    double desiredSlideAmount =
906       FindDesiredSlideAmount( viewInfo, mRect.x, event, mSnapManager.get(),
907          mSlideUpDownOnly, mSnapPreferRightEdge, mClipMoveState,
908          *pTrack );
909 
910    // Scroll during vertical drag.
911    // If the mouse is over a track that isn't the captured track,
912    // decide which tracks the captured clips should go to.
913    // EnsureVisible(pTrack); //vvv Gale says this has problems on Linux, per bug 393 thread. Revert for 2.0.2.
914    bool slidVertically = (
915        pTrack != mClipMoveState.mCapturedTrack
916        /* && !mCapturedClipIsSelection*/
917       && DoSlideVertical( viewInfo, event.m_x, mClipMoveState,
918                   trackList, *pTrack, desiredSlideAmount ) );
919    if (slidVertically)
920    {
921       mClipMoveState.mCapturedTrack = pTrack;
922       mDidSlideVertically = true;
923    }
924 
925    if (desiredSlideAmount == 0.0)
926       return RefreshAll;
927 
928    // Note that mouse dragging doesn't use TrackShifter::HintOffsetLarger()
929 
930    mClipMoveState.DoSlideHorizontal( desiredSlideAmount );
931 
932    if (mClipMoveState.movingSelection) {
933       // Slide the selection, too
934       viewInfo.selectedRegion.move( mClipMoveState.hSlideAmount );
935    }
936 
937    if (slidVertically) {
938       // NEW origin
939       mClipMoveState.hSlideAmount = 0;
940    }
941 
942    return RefreshAll;
943 }
944 
Preview(const TrackPanelMouseState &,AudacityProject * pProject)945 HitTestPreview TimeShiftHandle::Preview
946 (const TrackPanelMouseState &, AudacityProject *pProject)
947 {
948    // After all that, it still may be unsafe to drag.
949    // Even if so, make an informative cursor change from default to "banned."
950    const bool unsafe = ProjectAudioIO::Get( *pProject ).IsAudioActive();
951    return HitPreview(pProject, unsafe);
952 }
953 
Release(const TrackPanelMouseEvent &,AudacityProject * pProject,wxWindow *)954 UIHandle::Result TimeShiftHandle::Release
955 (const TrackPanelMouseEvent &, AudacityProject *pProject,
956  wxWindow *)
957 {
958    using namespace RefreshCode;
959    const bool unsafe = ProjectAudioIO::Get( *pProject ).IsAudioActive();
960    if (unsafe)
961       return this->Cancel(pProject);
962 
963    Result result = RefreshNone;
964 
965    // Do not draw yellow lines
966    if ( mClipMoveState.snapLeft != -1 || mClipMoveState.snapRight != -1) {
967       mClipMoveState.snapLeft = mClipMoveState.snapRight = -1;
968       result |= RefreshAll;
969    }
970 
971    if ( !mDidSlideVertically && mClipMoveState.hSlideAmount == 0 )
972       return result;
973 
974    for ( auto &pair : mClipMoveState.shifters )
975       if ( !pair.second->FinishMigration() )
976          MigrationFailure();
977 
978    TranslatableString msg;
979    bool consolidate;
980    if (mDidSlideVertically) {
981       msg = XO("Moved clips to another track");
982       consolidate = false;
983       for (auto& pair : mClipMoveState.shifters)
984          pair.first->LinkConsistencyCheck();
985    }
986    else {
987       msg = ( mClipMoveState.hSlideAmount > 0
988          ? XO("Time shifted tracks/clips right %.02f seconds")
989          : XO("Time shifted tracks/clips left %.02f seconds")
990       )
991          .Format( fabs( mClipMoveState.hSlideAmount ) );
992       consolidate = true;
993    }
994    ProjectHistory::Get( *pProject ).PushState(msg, XO("Time-Shift"),
995       consolidate ? (UndoPush::CONSOLIDATE) : (UndoPush::NONE));
996 
997    return result | FixScrollbars;
998 }
999 
Cancel(AudacityProject * pProject)1000 UIHandle::Result TimeShiftHandle::Cancel(AudacityProject *pProject)
1001 {
1002    if(mClipMoveState.initialized)
1003    {
1004       ProjectHistory::Get( *pProject ).RollbackState();
1005       return RefreshCode::RefreshAll;
1006    }
1007    return RefreshCode::RefreshNone;
1008 }
1009 
Draw(TrackPanelDrawingContext & context,const wxRect & rect,unsigned iPass)1010 void TimeShiftHandle::Draw(
1011    TrackPanelDrawingContext &context,
1012    const wxRect &rect, unsigned iPass )
1013 {
1014    if ( iPass == TrackArtist::PassSnapping ) {
1015       auto &dc = context.dc;
1016       // Draw snap guidelines if we have any
1017       if ( mSnapManager ) {
1018          mSnapManager->Draw(
1019             &dc, mClipMoveState.snapLeft, mClipMoveState.snapRight );
1020       }
1021    }
1022 }
1023 
DrawingArea(TrackPanelDrawingContext &,const wxRect & rect,const wxRect & panelRect,unsigned iPass)1024 wxRect TimeShiftHandle::DrawingArea(
1025    TrackPanelDrawingContext &,
1026    const wxRect &rect, const wxRect &panelRect, unsigned iPass )
1027 {
1028    if ( iPass == TrackArtist::PassSnapping )
1029       return MaximizeHeight( rect, panelRect );
1030    else
1031       return rect;
1032 }
1033