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, ¶ms );
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