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