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