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#include "TimeShiftHandle.h"
11
12#include "ChannelView.h"
13#include "AColor.h"
14#include "../../HitTestResult.h"
15#include "ProjectAudioIO.h"
16#include "ProjectHistory.h"
17#include "../../ProjectSettings.h"
18#include "../../RefreshCode.h"
19#include "Snap.h"
20#include "SyncLock.h"
21#include "Track.h"
22#include "../../TrackArt.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
30#include <cassert>
31
32TimeShiftHandle::TimeShiftHandle(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
118std::shared_ptr<const Track> TimeShiftHandle::FindTrack() const
119{
120 return GetTrack();
121}
122
124{
125 if (!shifters.empty()) {
126 for (auto &pair : shifters)
127 pair.second->DoHorizontalOffset(offset);
128 }
129 else if (mCapturedTrack)
130 mCapturedTrack->ShiftBy(offset);
131}
132
134
136
138 std::function<bool(const ChannelGroupInterval&)> pred)
139{
140 for (auto iter = mFixed.begin(); iter != mFixed.end();) {
141 if (pred(**iter)) {
142 mMoving.push_back(std::move(*iter));
143 iter = mFixed.erase(iter);
144 mAllFixed = false;
145 }
146 else
147 ++iter;
148 }
149}
150
152{
153 std::move(mFixed.begin(), mFixed.end(), std::back_inserter(mMoving));
154 mFixed = Intervals{};
155 mAllFixed = false;
156}
157
159{
160 UnfixAll();
161}
162
164{
165 UnfixIntervals( [&](auto &myInterval){
166 return !(interval.End() < myInterval.Start() ||
167 myInterval.End() < interval.Start());
168 });
169}
170
171double TrackShifter::HintOffsetLarger(double desiredOffset)
172{
173 return desiredOffset;
174}
175
176double TrackShifter::QuantizeOffset(double desiredOffset)
177{
178 return desiredOffset;
179}
180
181double TrackShifter::AdjustOffsetSmaller(double desiredOffset)
182{
183 return desiredOffset;
184}
185
187{
188 return false;
189}
190
192{
193 auto &track = GetTrack();
194 // Both tracks need to be owned to decide this
195 auto pMyList = track.GetOwner().get();
196 auto pOtherList = otherTrack.GetOwner().get();
197 if (pMyList && pOtherList) {
198 // Can migrate to another track of the same kind...
199 if (otherTrack.SameKindAs(track)) {
200 // ... with the same number of channels
201 auto myChannels = track.Channels();
202 auto otherChannels = otherTrack.Channels();
203 return (myChannels.size() == otherChannels.size());
204 }
205 }
206 return false;
207}
208
210{
211 return {};
212}
213
215 const Track &, const Intervals&, double &, double)
216{
217 return false;
218}
219
221{
222 return true;
223}
224
226{
227 return true;
228}
229
231{
232 if (!AllFixed())
233 GetTrack().ShiftBy(offset);
234}
235
236double TrackShifter::AdjustT0(double t0) const
237{
238 return t0;
239}
240
242{
243 auto &track = GetTrack();
244 mMoving.clear();
245 const auto &range = track.Intervals();
246 std::copy(range.begin(), range.end(), back_inserter(mFixed));
247}
248
250 : mpTrack{ track.SharedPointer() }
251{
253}
254
256
258 double, const ViewInfo&, HitTestParams* ) -> HitTestResult
259{
260 return HitTestResult::Track;
261}
262
264{
265 return false;
266}
267
269 return [](Track &track, AudacityProject&) {
270 return std::make_unique<CoarseTrackShifter>(track);
271 };
272}
273
276 Track &capturedTrack,
277 TrackShifter::HitTestResult hitTestResult,
278 std::unique_ptr<TrackShifter> pHit,
279 double clickTime,
280 const ViewInfo &viewInfo,
281 TrackList &trackList, bool syncLocked )
282{
283 shifters.clear();
284
285 initialized = true;
286
287 auto &state = *this;
288 state.mCapturedTrack = capturedTrack.SharedPointer();
289
290 switch (hitTestResult) {
292 wxASSERT(false);
293 pHit.reset();
294 break;
296 pHit.reset();
297 break;
299 break;
301 state.movingSelection = true;
302 break;
303 default:
304 break;
305 }
306
307 if (!pHit)
308 return;
309
310 state.shifters[&capturedTrack] = std::move( pHit );
311
312 // Collect TrackShifters for the rest of the tracks
313 for (auto track : trackList) {
314 auto &pShifter = state.shifters[track];
315 if (!pShifter)
316 pShifter = MakeTrackShifter::Call(*track, project);
317 }
318
319 if ( state.movingSelection ) {
320 // All selected tracks may move some intervals
321 const TrackShifter::TimeInterval interval{
322 viewInfo.selectedRegion.t0(),
323 viewInfo.selectedRegion.t1()
324 };
325 for ( const auto &pair : state.shifters ) {
326 auto &shifter = *pair.second;
327 auto &track = shifter.GetTrack();
328 if (&track == &capturedTrack)
329 // Don't change the choice of intervals made by HitTest
330 continue;
331 if ( track.IsSelected() )
332 shifter.SelectInterval( interval );
333 }
334 }
335
336 // Sync lock propagation of unfixing of intervals
337 if ( syncLocked ) {
338 bool change = true;
339 while( change ) {
340 change = false;
341
342 // Iterate over all unfixed intervals in all tracks
343 // that do propagation and are in sync lock groups ...
344 for ( auto &pair : state.shifters ) {
345 auto &shifter = *pair.second.get();
346 if (!shifter.SyncLocks())
347 continue;
348 auto &track = shifter.GetTrack();
349 auto group = SyncLock::Group(track);
350 if (group.size() <= 1)
351 continue;
352
353 auto &intervals = shifter.MovingIntervals();
354 for (auto &interval : intervals) {
355
356 // ...and tell all other tracks in the sync lock group
357 // to select that interval...
358 for (auto pTrack2 : group) {
359 if (pTrack2 == &track)
360 continue;
361 auto &shifter2 = *shifters[pTrack2];
362 auto size = shifter2.MovingIntervals().size();
363 shifter2.SelectInterval({
364 interval->Start(), interval->End() });
365 change = change ||
366 (shifter2.SyncLocks() &&
367 size != shifter2.MovingIntervals().size());
368 }
369
370 }
371 }
372
373 // ... and repeat if any other interval became unfixed in a
374 // shifter that propagates
375 }
376 }
377}
378
380{
381 auto pTrack = mCapturedTrack.get();
382 if (pTrack) {
383 auto iter = shifters.find(pTrack);
384 if (iter != shifters.end()) {
385 auto &pShifter = iter->second;
386 if (pShifter) {
387 auto &intervals = pShifter->MovingIntervals();
388 if (!intervals.empty())
389 return intervals[0].get();
390 }
391 }
392 }
393 return nullptr;
394}
395
396double ClipMoveState::DoSlideHorizontal(double desiredSlideAmount)
397{
398 auto &state = *this;
399
400 // Given a signed slide distance, move clips, but subject to constraint of
401 // non-overlapping with other clips, so the distance may be adjusted toward
402 // zero.
403 if ( !state.shifters.empty() ) {
404 double initialAllowed = 0;
405 do { // loop to compute allowed, does not actually move anything yet
406 initialAllowed = desiredSlideAmount;
407
408 for (auto &pair : shifters) {
409 auto newAmount = pair.second->AdjustOffsetSmaller( desiredSlideAmount );
410 if ( desiredSlideAmount != newAmount ) {
411 if ( newAmount * desiredSlideAmount < 0 ||
412 fabs(newAmount) > fabs(desiredSlideAmount) ) {
413 wxASSERT( false ); // AdjustOffsetSmaller didn't honor postcondition!
414 newAmount = 0; // Be sure the loop progresses to termination!
415 }
416 desiredSlideAmount = newAmount;
417 state.snapLeft = state.snapRight = -1; // see bug 1067
418 }
419 if (newAmount == 0)
420 break;
421 }
422 } while ( desiredSlideAmount != initialAllowed );
423 }
424
425 // Whether moving intervals or a whole track,
426 // finally, here is where clips are moved
427 if ( desiredSlideAmount != 0.0 )
428 state.DoHorizontalOffset( desiredSlideAmount );
429
430 //attempt to move a clip is counted to
431 wasMoved = true;
432
433 return (state.hSlideAmount = desiredSlideAmount);
434}
435
436namespace {
438 const TrackList &tracks, const ClipMoveState::ShifterMap &shifters)
439{
440 // Compare with the other function FindCandidates in Snap
441 // Make the snap manager more selective than it would be if just constructed
442 // from the track list
443 SnapPointArray candidates;
444 for (const auto &pair : shifters) {
445 auto &shifter = pair.second;
446 auto &track = shifter->GetTrack();
447 for (const auto &interval : shifter->FixedIntervals()) {
448 candidates.emplace_back(interval->Start(), &track);
449 if (interval->Start() != interval->End())
450 candidates.emplace_back(interval->End(), &track);
451 }
452 }
453 return candidates;
454}
455}
456
458 const TrackPanelMouseEvent &evt, AudacityProject *pProject)
459{
460 using namespace RefreshCode;
461 const bool unsafe = ProjectAudioIO::Get( *pProject ).IsAudioActive();
462 if ( unsafe )
463 return Cancelled;
464
465 const wxMouseEvent &event = evt.event;
466 const wxRect &rect = evt.rect;
467 auto &viewInfo = ViewInfo::Get( *pProject );
468
469 const auto pView =
470 std::dynamic_pointer_cast<CommonTrackPanelCell>(evt.pCell);
471 const auto pTrack = pView ? pView->FindTrack() : nullptr;
472 if (!pTrack)
474
476 mDidSlideVertically = false;
477
478 const bool multiToolModeActive =
480
481 const double clickTime =
482 viewInfo.PositionToTime(event.m_x, rect.x);
483
484 auto pShifter = MakeTrackShifter::Call(*pTrack, *pProject);
485
486 auto hitTestResult = TrackShifter::HitTestResult::Track;
487 if (!event.ShiftDown()) {
489 rect, event.m_x, event.m_y
490 };
491 hitTestResult = pShifter->HitTest( clickTime, viewInfo, &params );
492 switch( hitTestResult ) {
494 return Cancelled;
495 default:
496 break;
497 }
498 }
499 else {
500 // just do shifting of one whole track
501 }
502
503 auto &trackList = TrackList::Get(*pProject);
504 mClipMoveState.Init(*pProject, *pTrack,
505 hitTestResult, move(pShifter), clickTime,
506 viewInfo, trackList,
507 SyncLockState::Get( *pProject ).IsSyncLocked() );
508
509 mSlideUpDownOnly = event.CmdDown() && !multiToolModeActive;
510 mRect = rect;
511 mClipMoveState.mMouseClickX = event.m_x;
513 std::make_shared<SnapManager>(*trackList.GetOwner(),
515 viewInfo);
518 auto pInterval = mClipMoveState.CapturedInterval();
519 mSnapPreferRightEdge = pInterval &&
520 (fabs(clickTime - pInterval->End()) <
521 fabs(clickTime - pInterval->Start()));
522
523 return RefreshNone;
524}
525
526namespace {
528 const ViewInfo &viewInfo, wxCoord xx,
529 SnapManager *pSnapManager,
530 bool snapPreferRightEdge,
531 ClipMoveState &state,
532 double& desiredSlideAmount)
533 {
534 auto track = state.mCapturedTrack.get();
535 double clipLeft, clipRight;
536
537 // Adjust desiredSlideAmount using SnapManager
538 if (pSnapManager) {
539 auto pInterval = state.CapturedInterval();
540 if (pInterval) {
541 clipLeft = pInterval->Start() + desiredSlideAmount;
542 clipRight = pInterval->End() + desiredSlideAmount;
543 }
544 else {
545 clipLeft = track->GetStartTime() + desiredSlideAmount;
546 clipRight = track->GetEndTime() + desiredSlideAmount;
547 }
548
549 auto results =
550 pSnapManager->Snap(track, clipLeft, false);
551 auto newClipLeft = results.outTime;
552 results =
553 pSnapManager->Snap(track, clipRight, false);
554 auto newClipRight = results.outTime;
555
556 // Only one of them is allowed to snap
557 if (newClipLeft != clipLeft && newClipRight != clipRight) {
558 // Un-snap the un-preferred edge
559 if (snapPreferRightEdge)
560 newClipLeft = clipLeft;
561 else
562 newClipRight = clipRight;
563 }
564
565 // Take whichever one snapped (if any) and compute the NEW desiredSlideAmount
566 state.snapLeft = -1;
567 state.snapRight = -1;
568 if (newClipLeft != clipLeft) {
569 const double difference = (newClipLeft - clipLeft);
570 desiredSlideAmount += difference;
571 state.snapLeft =
572 viewInfo.TimeToPosition(newClipLeft, xx);
573 }
574 else if (newClipRight != clipRight) {
575 const double difference = (newClipRight - clipRight);
576 desiredSlideAmount += difference;
577 state.snapRight =
578 viewInfo.TimeToPosition(newClipRight, xx);
579 }
580 }
581 }
582
583 using Correspondence = std::unordered_map< Track*, Track* >;
584
586 Correspondence &correspondence,
587 TrackList &trackList, Track &capturedTrack, Track &track,
588 ClipMoveState &state)
589 {
590 if (state.shifters.empty())
591 // Shift + Dragging hasn't yet supported vertical movement
592 return false;
593
594 // Accumulate new pairs for the correspondence, and merge them
595 // into the given correspondence only on success
596 Correspondence newPairs;
597
598 auto sameType = [&](auto pTrack){
599 return capturedTrack.SameKindAs(*pTrack);
600 };
601 if (!sameType(&track))
602 return false;
603
604 // All tracks of the same kind as the captured track
605 auto range = trackList.Any() + sameType;
606
607 // Find how far this track would shift down among those (signed)
608 const auto myPosition =
609 std::distance(range.first, trackList.Find(&capturedTrack));
610 const auto otherPosition =
611 std::distance(range.first, trackList.Find(&track));
612 auto diff = otherPosition - myPosition;
613
614 // Point to destination track for first of range, when diff >= 0
615 // Otherwise the loop below iterates -diff times checking that initial
616 // members of the range are not the shifting tracks
617 auto iter = range.first.advance(diff >= 0 ? diff : 0);
618
619 for (auto pTrack : range) {
620 auto &pShifter = state.shifters[pTrack];
621 if (!pShifter->MovingIntervals().empty()) {
622 // One of the interesting tracks
623
624 auto pOther = *iter;
625 if (diff < 0 || !pOther)
626 // No corresponding track
627 return false;
628
629 if (!pShifter->MayMigrateTo(*pOther))
630 // Rejected for other reason
631 return false;
632
633 if (correspondence.count(pTrack))
634 // Don't overwrite the given correspondence
635 return false;
636
637 newPairs[pTrack] = pOther;
638 }
639
640 if (diff < 0)
641 ++diff; // Still consuming initial tracks
642 else
643 ++iter; // Safe to increment TrackIter even at end of range
644 }
645
646 // Success
647 if (correspondence.empty())
648 correspondence.swap(newPairs);
649 else
650 copy(newPairs.begin(), newPairs.end(),
651 inserter(correspondence, correspondence.end()));
652 return true;
653 }
654
656 std::unordered_map<Track*, TrackShifter::Intervals>;
657
659 ClipMoveState &state, const Correspondence &correspondence,
660 const DetachedIntervals &intervals,
661 double tolerance, double &desiredSlideAmount )
662 {
663 bool ok = true;
664 double firstTolerance = tolerance;
665
666 // The desiredSlideAmount may change and the tolerance may get used up.
667 for ( unsigned iPass = 0; iPass < 2 && ok; ++iPass ) {
668 for ( auto &pair : state.shifters ) {
669 auto *pSrcTrack = pair.first;
670 auto iter = correspondence.find( pSrcTrack );
671 if ( iter != correspondence.end() )
672 if( auto *pOtherTrack = iter->second )
673 if ( !(ok = pair.second->AdjustFit(
674 *pOtherTrack, intervals.at(pSrcTrack),
675 desiredSlideAmount /*in,out*/, tolerance)) )
676 break;
677 }
678
679 // If it fits ok, desiredSlideAmount could have been updated to get
680 // the clip to fit.
681 // Check again, in the new position, this time with zero tolerance.
682 if (firstTolerance == 0)
683 break;
684 else
685 tolerance = 0.0;
686 }
687
688 return ok;
689 }
690
691 [[noreturn]] void MigrationFailure() {
692 // Tracks may be in an inconsistent state; throw to the application
693 // handler which restores consistency from undo history
695 XO("Could not shift between tracks")};
696 }
697
700 : state( clipMoveState )
701 {
702 // Pluck the moving clips out of their tracks
703 for (auto &pair : state.shifters)
704 detached[pair.first] = pair.second->Detach();
705 }
706
708 std::unordered_map< Track*, Track* > *pCorrespondence, double offset )
709 {
710 for (auto &pair : detached) {
711 auto pTrack = pair.first;
712 if (pCorrespondence && pCorrespondence->count(pTrack))
713 pTrack = (*pCorrespondence)[pTrack];
714 auto &pShifter = state.shifters[pTrack];
715 if (!pShifter->Attach( std::move( pair.second ), offset ))
717 }
718 }
719
722 };
723}
724
726 ViewInfo &viewInfo, wxCoord xx,
727 TrackList &trackList, Track *dstTrack, double& desiredSlideAmount)
728{
729 Correspondence correspondence;
730
731 // See if captured track corresponds to another
732 auto &capturedTrack = *mClipMoveState.mCapturedTrack;
734 correspondence, trackList, capturedTrack, *dstTrack, mClipMoveState ))
735 return;
736
737 // Try to extend the correpondence
738 auto tryExtend = [&](bool forward) {
739 auto range = trackList.Any();
740 auto begin = range.begin(), end = range.end();
741 auto pCaptured = trackList.Find(&capturedTrack);
742 auto pDst = trackList.Find(dstTrack);
743 // Scan for more correspondences
744 while (true) {
745 // Remember that TrackIter wraps circularly to the end iterator when
746 // decrementing it
747
748 // First move to a track with moving intervals and
749 // without a correspondent
750 do
751 forward ? ++pCaptured : --pCaptured;
752 while (pCaptured != end &&
753 (correspondence.count(*pCaptured) ||
754 mClipMoveState.shifters[*pCaptured]->MovingIntervals().empty()));
755 if (pCaptured == end)
756 break;
757
758 // Change the choice of possible correspondent track too
759 do
760 forward ? ++pDst : --pDst;
761 while (pDst != end && correspondence.count(*pDst));
762 if (pDst == end)
763 break;
764
765 // Make correspondence if we can
767 correspondence, trackList, **pCaptured, **pDst, mClipMoveState))
768 break;
769 }
770 };
771 // Try extension, backward first, then forward
772 // (anticipating the case of dragging a label that is under a clip)
773 tryExtend(false);
774 tryExtend(true);
775
776 // Having passed that test, remove clips temporarily from their
777 // tracks, so moving clips don't interfere with each other
778 // when we call CanInsertClip()
779 TemporaryClipRemover remover{ mClipMoveState };
780
781 // Now check that the move is possible
782 auto slideAmount = desiredSlideAmount;
783 // The test for tolerance will need review with FishEye!
784 // The tolerance is supposed to be the time for twenty pixels,
785 // i.e. twenty pixel tolerance at current zoom.
786 double tolerance =
787 viewInfo.PositionToTime(xx + 10) - viewInfo.PositionToTime(xx - 10);
788 bool ok = CheckFit( mClipMoveState, correspondence, remover.detached,
789 tolerance, slideAmount /*in,out*/ );
790
791 if (!ok) {
792 // Failure, even with using tolerance.
793 remover.Reinsert( nullptr, .0 );
794 return;
795 }
796
797 remover.Reinsert( &correspondence, slideAmount );
798
800 viewInfo.selectedRegion.move( slideAmount );
801
802 // Make the offset permanent; start from a "clean slate"
805 mDidSlideVertically = true;
806 desiredSlideAmount = .0;
807}
808
810(const TrackPanelMouseEvent &evt, AudacityProject *pProject)
811{
812 using namespace RefreshCode;
813 const bool unsafe = ProjectAudioIO::Get( *pProject ).IsAudioActive();
814 if (unsafe) {
815 this->Cancel(pProject);
816 return RefreshAll | Cancelled;
817 }
818
819 const wxMouseEvent &event = evt.event;
820 auto &viewInfo = ViewInfo::Get( *pProject );
821
822 auto &trackList = TrackList::Get(*pProject);
823 ChannelView *trackView = dynamic_cast<ChannelView*>(evt.pCell.get());
824 const auto pChannel = trackView ? trackView->FindChannel() : nullptr;
825 auto track = pChannel
826 ? dynamic_cast<Track *>(&pChannel->GetChannelGroup())
827 : nullptr;
828
829 // Uncommenting this permits drag to continue to work even over the controls area
830 /*
831 track = static_cast<CommonTrackPanelCell*>(evt.pCell)->FindTrack().get();
832 */
833
834 if (!track) {
835 // Allow sliding if the pointer is not over any track, but only if x is
836 // within the bounds of the tracks area.
837 if (event.m_x >= mRect.GetX() &&
838 event.m_x < mRect.GetX() + mRect.GetWidth())
839 track = mClipMoveState.mCapturedTrack.get();
840 }
841
842 // May need a shared_ptr to reassign mCapturedTrack below
843 auto pTrack = Track::SharedPointer( track );
844 if (!pTrack)
846
847 // GM: slide now implementing snap-to
848 // samples functionality based on sample rate.
849
850 // Start by undoing the current slide amount; everything
851 // happens relative to the original horizontal position of
852 // each clip...
854
856 // Slide the selection, too
857 viewInfo.selectedRegion.move( -mClipMoveState.hSlideAmount );
858 }
860
861 double desiredSlideAmount = 0.0;
863 {
864 desiredSlideAmount =
865 viewInfo.PositionToTime(event.m_x) -
866 viewInfo.PositionToTime(mClipMoveState.mMouseClickX);
867
868 if (!mClipMoveState.shifters.empty())
869 desiredSlideAmount =
870 mClipMoveState.shifters[track]->QuantizeOffset(desiredSlideAmount);
871 }
872
873 if(mClipMoveState.mCapturedTrack != pTrack)
874 {
875 // Scroll during vertical drag.
876 // If the mouse is over a track that isn't the captured track,
877 // decide which tracks the captured clips should go to.
878 // Viewport::Get(*pProject).ShowTrack(pTrack); //vvv Gale says this has problems on Linux, per bug 393 thread. Revert for 2.0.2.
879
880 //move intervals with new start/end times
882 viewInfo, event.m_x, trackList, pTrack.get(), desiredSlideAmount);
883 }
884
886 {
887 AdjustToSnap(viewInfo, mRect.x,
888 mSnapManager.get(),
891 desiredSlideAmount);
892 }
893
894 if (desiredSlideAmount == 0.0)
895 return RefreshAll;
896
897 // Note that mouse dragging doesn't use TrackShifter::HintOffsetLarger()
898
899 mClipMoveState.DoSlideHorizontal( desiredSlideAmount );
900
902 // Slide the selection, too
903 viewInfo.selectedRegion.move( mClipMoveState.hSlideAmount );
904 }
905
906 return RefreshAll;
907}
908
910(const TrackPanelMouseState &, AudacityProject *pProject)
911{
912 // After all that, it still may be unsafe to drag.
913 // Even if so, make an informative cursor change from default to "banned."
914 const bool unsafe = ProjectAudioIO::Get( *pProject ).IsAudioActive();
915 return HitPreview(pProject, unsafe);
916}
917
919(const TrackPanelMouseEvent &, AudacityProject *pProject,
920 wxWindow *)
921{
922 using namespace RefreshCode;
923 const bool unsafe = ProjectAudioIO::Get( *pProject ).IsAudioActive();
924 if (unsafe)
925 return this->Cancel(pProject);
926
927 Result result = RefreshNone;
928
929 // Do not draw yellow lines
930 if ( mClipMoveState.snapLeft != -1 || mClipMoveState.snapRight != -1) {
932 result |= RefreshAll;
933 }
934
936 return result;
937
938 for ( auto &pair : mClipMoveState.shifters )
939 if (!pair.second->FinishMigration())
941
943 bool consolidate;
945 msg = XO("Moved clips to another track");
946 consolidate = false;
947 }
948 else {
949 msg = ( mClipMoveState.hSlideAmount > 0
950 ? XO("Time shifted tracks/clips right %.02f seconds")
951 : XO("Time shifted tracks/clips left %.02f seconds")
952 )
954 consolidate = true;
955 }
956 ProjectHistory::Get( *pProject ).PushState(msg, XO("Move Clip"),
957 consolidate ? (UndoPush::CONSOLIDATE) : (UndoPush::NONE));
958
959 return result | FixScrollbars;
960}
961
963{
965 {
966 ProjectHistory::Get( *pProject ).RollbackState();
968 }
970}
971
974 const wxRect &rect, unsigned iPass )
975{
976 if ( iPass == TrackArtist::PassSnapping ) {
977 auto &dc = context.dc;
978 // Draw snap guidelines if we have any
979 if ( mSnapManager ) {
982 }
983 }
984}
985
988 const wxRect &rect, const wxRect &panelRect, unsigned iPass )
989{
990 if ( iPass == TrackArtist::PassSnapping )
991 return MaximizeHeight( rect, panelRect );
992 else
993 return rect;
994}
@ 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
const auto tracks
const auto project
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:189
std::shared_ptr< Subclass > AssignUIHandlePtr(std::weak_ptr< Subclass > &holder, const std::shared_ptr< Subclass > &pNew)
Definition: UIHandle.h:164
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
IteratorRange< ChannelIterator< ChannelType > > Channels()
Get range of channels with mutative access.
Definition: Channel.h:381
void ShiftBy(double t)
Change start time by given duration.
Definition: Channel.h:296
A start and an end time, and whatever else subclasses associate with them.
Definition: Channel.h:29
virtual double Start() const =0
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.
auto FindChannel() -> std::shared_ptr< Subtype >
May return null.
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:262
static TrackIterRange< Track > Group(Track &track)
Definition: SyncLock.cpp:150
bool IsSyncLocked() const
Definition: SyncLock.cpp:44
static SyncLockState & Get(AudacityProject &project)
Definition: SyncLock.cpp:27
bool Clicked() const
ClipMoveState mClipMoveState
std::shared_ptr< const Track > FindTrack() const override
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, Track *dstTrack, double &desiredSlideAmount)
Abstract base class for an object holding data associated with points on a time axis.
Definition: Track.h:110
bool SameKindAs(const Track &track) const
Definition: Track.h:373
std::shared_ptr< TrackList > GetOwner() const
Definition: Track.h:230
std::shared_ptr< Subclass > SharedPointer()
Definition: Track.h:146
A flat linked list of tracks supporting Add, Remove, Clear, and Contains, serialization of the list o...
Definition: Track.h:850
TrackIter< Track > Find(Track *pTrack)
Definition: Track.cpp:470
auto Any() -> TrackIterRange< TrackType >
Definition: Track.h:950
static TrackList & Get(AudacityProject &project)
Definition: Track.cpp:314
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)
virtual Intervals Detach()
Remove all moving intervals from the track, if possible.
virtual Track & GetTrack() const =0
There is always an associated track.
void UnfixAll()
Change all intervals from fixed to moving.
virtual void DoHorizontalOffset(double offset)
void CommonSelectInterval(TimeInterval interval)
bool AllFixed() const
virtual void SelectInterval(TimeInterval interval)
Notifies the shifter that a region is selected, so it may update its fixed and moving intervals.
virtual double AdjustT0(double t0) const
void InitIntervals()
Derived class constructor can initialize all intervals reported by the track as fixed,...
virtual bool MayMigrateTo(Track &otherTrack)
virtual double HintOffsetLarger(double desiredOffset)
Given amount to shift by horizontally, maybe adjust it from zero to suggest minimum distance.
void UnfixIntervals(std::function< bool(const ChannelGroupInterval &)> pred)
Change intervals satisfying a predicate from fixed to moving.
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
std::vector< std::shared_ptr< ChannelGroupInterval > > Intervals
Holds a msgid for the translation catalog; may also bind format arguments.
Result mChangeHighlight
Definition: UIHandle.h:152
unsigned Result
Definition: UIHandle.h:40
NotifyingSelectedRegion selectedRegion
Definition: ViewInfo.h:215
static ViewInfo & Get(AudacityProject &project)
Definition: ViewInfo.cpp:235
double PositionToTime(int64 position, int64 origin=0, bool ignoreFisheye=false) const
Definition: ZoomInfo.cpp:34
int64 TimeToPosition(double time, int64 origin=0, bool ignoreFisheye=false) const
STM: Converts a project time to screen x position.
Definition: ZoomInfo.cpp:44
Namespace containing an enum 'what to do on a refresh?'.
Definition: RefreshCode.h:16
AUDACITY_DLL_API void DrawSnapLines(wxDC *dc, wxInt64 snap0, wxInt64 snap1)
Definition: TrackArt.cpp:771
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)
const char * end(const char *str) noexcept
Definition: StringUtils.h:106
const char * begin(const char *str) noexcept
Definition: StringUtils.h:101
void copy(const T *src, T *dst, int32_t n)
Definition: VectorOps.h:40
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.
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.
const ChannelGroupInterval * CapturedInterval() const
Return pointer to the first fixed interval of the captured track, if there is one.
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.
A simple time interval.
void Reinsert(std::unordered_map< Track *, Track * > *pCorrespondence, double offset)