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 SyncLockState::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);
559 auto pInterval = mClipMoveState.CapturedInterval();
560 mSnapPreferRightEdge = pInterval &&
561 (fabs(clickTime - pInterval->End()) <
562 fabs(clickTime - pInterval->Start()));
563
564 return RefreshNone;
565}
566
567namespace {
569 const ViewInfo &viewInfo, wxCoord xx,
570 SnapManager *pSnapManager,
571 bool snapPreferRightEdge,
572 ClipMoveState &state,
573 double& desiredSlideAmount)
574 {
575 auto track = state.mCapturedTrack.get();
576 double clipLeft, clipRight;
577
578 // Adjust desiredSlideAmount using SnapManager
579 if (pSnapManager) {
580 auto pInterval = state.CapturedInterval();
581 if (pInterval) {
582 clipLeft = pInterval->Start() + desiredSlideAmount;
583 clipRight = pInterval->End() + desiredSlideAmount;
584 }
585 else {
586 clipLeft = track->GetStartTime() + desiredSlideAmount;
587 clipRight = track->GetEndTime() + desiredSlideAmount;
588 }
589
590 auto results =
591 pSnapManager->Snap(track, clipLeft, false);
592 auto newClipLeft = results.outTime;
593 results =
594 pSnapManager->Snap(track, clipRight, false);
595 auto newClipRight = results.outTime;
596
597 // Only one of them is allowed to snap
598 if (newClipLeft != clipLeft && newClipRight != clipRight) {
599 // Un-snap the un-preferred edge
600 if (snapPreferRightEdge)
601 newClipLeft = clipLeft;
602 else
603 newClipRight = clipRight;
604 }
605
606 // Take whichever one snapped (if any) and compute the NEW desiredSlideAmount
607 state.snapLeft = -1;
608 state.snapRight = -1;
609 if (newClipLeft != clipLeft) {
610 const double difference = (newClipLeft - clipLeft);
611 desiredSlideAmount += difference;
612 state.snapLeft =
613 viewInfo.TimeToPosition(newClipLeft, xx);
614 }
615 else if (newClipRight != clipRight) {
616 const double difference = (newClipRight - clipRight);
617 desiredSlideAmount += difference;
618 state.snapRight =
619 viewInfo.TimeToPosition(newClipRight, xx);
620 }
621 }
622 }
623
624 using Correspondence = std::unordered_map< Track*, Track* >;
625
627 Correspondence &correspondence,
628 TrackList &trackList, Track &capturedTrack, Track &track,
629 ClipMoveState &state)
630 {
631 if (state.shifters.empty())
632 // Shift + Dragging hasn't yet supported vertical movement
633 return false;
634
635 // Accumulate new pairs for the correspondence, and merge them
636 // into the given correspondence only on success
637 Correspondence newPairs;
638
639 auto sameType = [&]( auto pTrack ){
640 return capturedTrack.SameKindAs( *pTrack );
641 };
642 if (!sameType(&track))
643 return false;
644
645 // All tracks of the same kind as the captured track
646 auto range = trackList.Any() + sameType;
647
648 // Find how far this track would shift down among those (signed)
649 const auto myPosition =
650 std::distance( range.first, trackList.Find( &capturedTrack ) );
651 const auto otherPosition =
652 std::distance( range.first, trackList.Find( &track ) );
653 auto diff = otherPosition - myPosition;
654
655 // Point to destination track
656 auto iter = range.first.advance( diff > 0 ? diff : 0 );
657
658 for (auto pTrack : range) {
659 auto &pShifter = state.shifters[pTrack];
660 if ( !pShifter->MovingIntervals().empty() ) {
661 // One of the interesting tracks
662
663 auto pOther = *iter;
664 if ( diff < 0 || !pOther )
665 // No corresponding track
666 return false;
667
668 if ( !pShifter->MayMigrateTo(*pOther) )
669 // Rejected for other reason
670 return false;
671
672 if ( correspondence.count(pTrack) )
673 // Don't overwrite the given correspondence
674 return false;
675
676 newPairs[ pTrack ] = pOther;
677 }
678
679 if ( diff < 0 )
680 ++diff; // Still consuming initial tracks
681 else
682 ++iter; // Safe to increment TrackIter even at end of range
683 }
684
685 // Success
686 if (correspondence.empty())
687 correspondence.swap(newPairs);
688 else
689 std::copy( newPairs.begin(), newPairs.end(),
690 std::inserter( correspondence, correspondence.end() ) );
691 return true;
692 }
693
695 std::unordered_map<Track*, TrackShifter::Intervals>;
696
698 ClipMoveState &state, const Correspondence &correspondence,
699 const DetachedIntervals &intervals,
700 double tolerance, double &desiredSlideAmount )
701 {
702 bool ok = true;
703 double firstTolerance = tolerance;
704
705 // The desiredSlideAmount may change and the tolerance may get used up.
706 for ( unsigned iPass = 0; iPass < 2 && ok; ++iPass ) {
707 for ( auto &pair : state.shifters ) {
708 auto *pSrcTrack = pair.first;
709 auto iter = correspondence.find( pSrcTrack );
710 if ( iter != correspondence.end() )
711 if( auto *pOtherTrack = iter->second )
712 if ( !(ok = pair.second->AdjustFit(
713 *pOtherTrack, intervals.at(pSrcTrack),
714 desiredSlideAmount /*in,out*/, tolerance)) )
715 break;
716 }
717
718 // If it fits ok, desiredSlideAmount could have been updated to get
719 // the clip to fit.
720 // Check again, in the new position, this time with zero tolerance.
721 if (firstTolerance == 0)
722 break;
723 else
724 tolerance = 0.0;
725 }
726
727 return ok;
728 }
729
730 [[noreturn]] void MigrationFailure() {
731 // Tracks may be in an inconsistent state; throw to the application
732 // handler which restores consistency from undo history
734 XO("Could not shift between tracks")};
735 }
736
739 : state( clipMoveState )
740 {
741 // Pluck the moving clips out of their tracks
742 for (auto &pair : state.shifters)
743 detached[pair.first] = pair.second->Detach();
744 }
745
747 std::unordered_map< Track*, Track* > *pCorrespondence, double offset )
748 {
749 for (auto &pair : detached) {
750 auto pTrack = pair.first;
751 if (pCorrespondence && pCorrespondence->count(pTrack))
752 pTrack = (*pCorrespondence)[pTrack];
753 auto &pShifter = state.shifters[pTrack];
754 if (!pShifter->Attach( std::move( pair.second ), offset ))
756 }
757 }
758
761 };
762}
763
765( ViewInfo &viewInfo, wxCoord xx,
766 TrackList &trackList,
767 const std::shared_ptr<Track>& dstTrack, double& desiredSlideAmount )
768{
769 Correspondence correspondence;
770
771 // See if captured track corresponds to another
772 auto &capturedTrack = *mClipMoveState.mCapturedTrack;
774 correspondence, trackList, capturedTrack, *dstTrack, mClipMoveState ))
775 return;
776
777 // Try to extend the correpondence
778 auto tryExtend = [&](bool forward){
779 auto begin = trackList.begin(), end = trackList.end();
780 auto pCaptured = trackList.Find( &capturedTrack );
781 auto pDst = trackList.Find( dstTrack.get() );
782 // Scan for more correspondences
783 while ( true ) {
784 // Remember that TrackIter wraps circularly to the end iterator when
785 // decrementing it
786
787 // First move to a track with moving intervals and
788 // without a correspondent
789 do
790 forward ? ++pCaptured : --pCaptured;
791 while ( pCaptured != end &&
792 ( correspondence.count(*pCaptured) || mClipMoveState.shifters[*pCaptured]->MovingIntervals().empty() ) );
793 if ( pCaptured == end )
794 break;
795
796 // Change the choice of possible correspondent track too
797 do
798 forward ? ++pDst : --pDst;
799 while ( pDst != end && correspondence.count(*pDst) );
800 if ( pDst == end )
801 break;
802
803 // Make correspondence if we can
805 correspondence, trackList, **pCaptured, **pDst, mClipMoveState ))
806 break;
807 }
808 };
809 // Try extension, backward first, then forward
810 // (anticipating the case of dragging a label that is under a clip)
811 tryExtend(false);
812 tryExtend(true);
813
814 // Having passed that test, remove clips temporarily from their
815 // tracks, so moving clips don't interfere with each other
816 // when we call CanInsertClip()
817 TemporaryClipRemover remover{ mClipMoveState };
818
819 // Now check that the move is possible
820 auto slideAmount = desiredSlideAmount;
821 // The test for tolerance will need review with FishEye!
822 // The tolerance is supposed to be the time for one pixel,
823 // i.e. one pixel tolerance at current zoom.
824 double tolerance =
825 viewInfo.PositionToTime(xx + 1) - viewInfo.PositionToTime(xx);
826 bool ok = CheckFit( mClipMoveState, correspondence, remover.detached,
827 tolerance, slideAmount /*in,out*/ );
828
829 if (!ok) {
830 // Failure, even with using tolerance.
831 remover.Reinsert( nullptr, .0 );
832 return;
833 }
834
835 remover.Reinsert( &correspondence, slideAmount );
836
838 viewInfo.selectedRegion.move( slideAmount );
839
840 // Make the offset permanent; start from a "clean slate"
843 mDidSlideVertically = true;
844 desiredSlideAmount = .0;
845}
846
848(const TrackPanelMouseEvent &evt, AudacityProject *pProject)
849{
850 using namespace RefreshCode;
851 const bool unsafe = ProjectAudioIO::Get( *pProject ).IsAudioActive();
852 if (unsafe) {
853 this->Cancel(pProject);
854 return RefreshAll | Cancelled;
855 }
856
857 const wxMouseEvent &event = evt.event;
858 auto &viewInfo = ViewInfo::Get( *pProject );
859
860 TrackView *trackView = dynamic_cast<TrackView*>(evt.pCell.get());
861 Track *track = trackView ? trackView->FindTrack().get() : nullptr;
862
863 // Uncommenting this permits drag to continue to work even over the controls area
864 /*
865 track = static_cast<CommonTrackPanelCell*>(evt.pCell)->FindTrack().get();
866 */
867
868 if (!track) {
869 // Allow sliding if the pointer is not over any track, but only if x is
870 // within the bounds of the tracks area.
871 if (event.m_x >= mRect.GetX() &&
872 event.m_x < mRect.GetX() + mRect.GetWidth())
873 track = mClipMoveState.mCapturedTrack.get();
874 }
875
876 // May need a shared_ptr to reassign mCapturedTrack below
877 auto pTrack = Track::SharedPointer( track );
878 if (!pTrack)
880
881
882 auto &trackList = TrackList::Get( *pProject );
883
884 // GM: slide now implementing snap-to
885 // samples functionality based on sample rate.
886
887 // Start by undoing the current slide amount; everything
888 // happens relative to the original horizontal position of
889 // each clip...
891
893 // Slide the selection, too
894 viewInfo.selectedRegion.move( -mClipMoveState.hSlideAmount );
895 }
897
898 double desiredSlideAmount = 0.0;
900 {
901 desiredSlideAmount =
902 viewInfo.PositionToTime(event.m_x) -
903 viewInfo.PositionToTime(mClipMoveState.mMouseClickX);
904
905 if (!mClipMoveState.shifters.empty())
906 desiredSlideAmount =
907 mClipMoveState.shifters[ track ]->QuantizeOffset( desiredSlideAmount );
908 }
909
910 if(mClipMoveState.mCapturedTrack != pTrack)
911 {
912 // Scroll during vertical drag.
913 // If the mouse is over a track that isn't the captured track,
914 // decide which tracks the captured clips should go to.
915 // EnsureVisible(pTrack); //vvv Gale says this has problems on Linux, per bug 393 thread. Revert for 2.0.2.
916
917 //move intervals with new start/end times
918 DoSlideVertical(viewInfo, event.m_x, trackList, pTrack, desiredSlideAmount);
919 }
920
922 {
923 AdjustToSnap(viewInfo, mRect.x,
924 mSnapManager.get(),
927 desiredSlideAmount);
928 }
929
930 if (desiredSlideAmount == 0.0)
931 return RefreshAll;
932
933 // Note that mouse dragging doesn't use TrackShifter::HintOffsetLarger()
934
935 mClipMoveState.DoSlideHorizontal( desiredSlideAmount );
936
938 // Slide the selection, too
939 viewInfo.selectedRegion.move( mClipMoveState.hSlideAmount );
940 }
941
942 return RefreshAll;
943}
944
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
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) {
968 result |= RefreshAll;
969 }
970
972 return result;
973
974 for ( auto &pair : mClipMoveState.shifters )
975 if ( !pair.second->FinishMigration() )
977
979 bool consolidate;
981 msg = XO("Moved clips to another track");
982 consolidate = false;
983 for (auto& pair : mClipMoveState.shifters)
984 pair.first->LinkConsistencyFix();
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 )
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
1001{
1003 {
1004 ProjectHistory::Get( *pProject ).RollbackState();
1006 }
1008}
1009
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(
1020 }
1021 }
1022}
1023
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}
@ Internal
Indicates internal failure from Audacity.
std::shared_ptr< UIHandle > UIHandlePtr
Definition: CellularPanel.h:28
EffectDistortionSettings params
Definition: Distortion.cpp:77
XO("Cut/Copy/Paste")
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:186
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:90
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:128
double End() const
Definition: Track.h:129
Abstract base class used in importing a file.
double t1() const
Definition: ViewInfo.h:36
double t0() const
Definition: ViewInfo.h:35
void move(double delta)
Definition: ViewInfo.cpp:97
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)
int GetTool() const
A MessageBoxException that shows a given, unvarying string.
SnapResults Snap(Track *currentTrack, double t, bool rightEdge)
Definition: Snap.cpp:264
static TrackIterRange< Track > Group(Track *pTrack)
Definition: SyncLock.cpp:161
bool IsSyncLocked() const
Definition: SyncLock.cpp:43
static SyncLockState & Get(AudacityProject &project)
Definition: SyncLock.cpp:26
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
void DoSlideVertical(ViewInfo &viewInfo, wxCoord xx, TrackList &trackList, const std::shared_ptr< Track > &dstTrack, double &desiredSlideAmount)
Abstract base class for an object holding data associated with points on a time axis.
Definition: Track.h:161
void Offset(double t)
Definition: Track.h:403
bool SameKindAs(const Track &track) const
Definition: Track.h:452
std::shared_ptr< TrackList > GetOwner() const
Definition: Track.h:333
std::shared_ptr< Subclass > SharedPointer()
Definition: Track.h:224
virtual ConstIntervals GetIntervals() const
Report times on the track where important intervals begin and end, for UI to snap to.
Definition: Track.cpp:1138
LinkType GetLinkType() const noexcept
Definition: Track.cpp:1212
bool IsAlignedWithLeader() const
Returns true if the leader track has link type LinkType::Aligned.
Definition: Track.cpp:1217
A start and an end time, and mutative access to optional extra information.
Definition: Track.h:141
A flat linked list of tracks supporting Add, Remove, Clear, and Contains, serialization of the list o...
Definition: Track.h:1201
iterator end()
Definition: Track.h:1256
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:1264
auto Any() -> TrackIterRange< TrackType >
Definition: Track.h:1302
static TrackList & Get(AudacityProject &project)
Definition: Track.cpp:360
iterator begin()
Definition: Track.h:1255
static auto Channels(TrackType *pTrack) -> TrackIterRange< TrackType >
Definition: Track.h:1406
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 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.
virtual bool Attach(Intervals intervals, double offset)
Put moving intervals into the track, which may have migrated from another.
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:219
static ViewInfo & Get(AudacityProject &project)
Definition: ViewInfo.cpp:235
double PositionToTime(int64 position, int64 origin=0, bool ignoreFisheye=false) const
Definition: ZoomInfo.cpp:35
int64 TimeToPosition(double time, int64 origin=0, bool ignoreFisheye=false) const
STM: Converts a project time to screen x position.
Definition: ZoomInfo.cpp:45
auto end(const Ptr< Type, BaseDeleter > &p)
Enables range-for.
Definition: PackedArray.h:159
auto begin(const Ptr< Type, BaseDeleter > &p)
Enables range-for.
Definition: PackedArray.h:150
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)
void AdjustToSnap(const ViewInfo &viewInfo, wxCoord xx, SnapManager *pSnapManager, bool snapPreferRightEdge, ClipMoveState &state, double &desiredSlideAmount)
std::unordered_map< Track *, Track * > Correspondence
bool FindCorrespondence(Correspondence &correspondence, TrackList &trackList, Track &capturedTrack, Track &track, ClipMoveState &state)
void copy(const T *src, T *dst, int32_t n)
Definition: VectorOps.h:32
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, double offset)