/********************************************************************** Audacity: A Digital Audio Editor TimeShiftHandle.cpp Paul Licameli split from TrackPanel.cpp **********************************************************************/ #include "../../Audacity.h" // for USE_* macros #include "TimeShiftHandle.h" #include "../../Experimental.h" #include "TrackView.h" #include "../../AColor.h" #include "../../HitTestResult.h" #include "../../NoteTrack.h" #include "../../ProjectAudioIO.h" #include "../../ProjectHistory.h" #include "../../ProjectSettings.h" #include "../../RefreshCode.h" #include "../../TrackArtist.h" #include "../../TrackPanelDrawingContext.h" #include "../../TrackPanelMouseEvent.h" #include "../../UndoManager.h" #include "../../WaveClip.h" #include "../../ViewInfo.h" #include "../../WaveTrack.h" #include "../../../images/Cursors.h" TimeShiftHandle::TimeShiftHandle ( const std::shared_ptr &pTrack, bool gripHit ) : mCapturedTrack{ pTrack } , mGripHit{ gripHit } { } void TimeShiftHandle::Enter(bool) { #ifdef EXPERIMENTAL_TRACK_PANEL_HIGHLIGHTING mChangeHighlight = RefreshCode::RefreshCell; #endif } HitTestPreview TimeShiftHandle::HitPreview (const AudacityProject *WXUNUSED(pProject), bool unsafe) { static auto disabledCursor = ::MakeCursor(wxCURSOR_NO_ENTRY, DisabledCursorXpm, 16, 16); static auto slideCursor = MakeCursor(wxCURSOR_SIZEWE, TimeCursorXpm, 16, 16); // TODO: Should it say "track or clip" ? Non-wave tracks can move, or clips in a wave track. // TODO: mention effects of shift (move all clips of selected wave track) and ctrl (move vertically only) ? // -- but not all of that is available in multi tool. auto message = _("Click and drag to move a track in time"); return { message, (unsafe ? &*disabledCursor : &*slideCursor) }; } UIHandlePtr TimeShiftHandle::HitAnywhere (std::weak_ptr &holder, const std::shared_ptr &pTrack, bool gripHit) { auto result = std::make_shared( pTrack, gripHit ); result = AssignUIHandlePtr(holder, result); return result; } UIHandlePtr TimeShiftHandle::HitTest (std::weak_ptr &holder, const wxMouseState &state, const wxRect &rect, const std::shared_ptr &pTrack) { /// method that tells us if the mouse event landed on a /// time-slider that allows us to time shift the sequence. /// (Those are the two "grips" drawn at left and right edges for multi tool mode.) // Perhaps we should delegate this to TrackArtist as only TrackArtist // knows what the real sizes are?? // The drag Handle width includes border, width and a little extra margin. const int adjustedDragHandleWidth = 14; // The hotspot for the cursor isn't at its centre. Adjust for this. const int hotspotOffset = 5; // We are doing an approximate test here - is the mouse in the right or left border? if (!(state.m_x + hotspotOffset < rect.x + adjustedDragHandleWidth || state.m_x + hotspotOffset >= rect.x + rect.width - adjustedDragHandleWidth)) return {}; return HitAnywhere( holder, pTrack, true ); } TimeShiftHandle::~TimeShiftHandle() { } namespace { // Adds a track's clips to state.capturedClipArray within a specified time void AddClipsToCaptured ( ClipMoveState &state, Track *t, double t0, double t1 ) { bool exclude = true; auto &clips = state.capturedClipArray; t->TypeSwitch( [&](WaveTrack *wt) { exclude = false; for(const auto &clip: wt->GetClips()) if ( ! clip->AfterClip(t0) && ! clip->BeforeClip(t1) && // Avoid getting clips that were already captured ! std::any_of( clips.begin(), clips.end(), [&](const TrackClip &c) { return c.clip == clip.get(); } ) ) clips.emplace_back( t, clip.get() ); }, #ifdef USE_MIDI [&](NoteTrack *, const Track::Fallthrough &fallthrough){ // do not add NoteTrack if the data is outside of time bounds if (t->GetEndTime() < t0 || t->GetStartTime() > t1) return; else fallthrough(); }, #endif [&](Track *t) { // This handles label tracks rather heavy-handedly -- // it would be nice to // treat individual labels like clips // Avoid adding a track twice if( !std::any_of( clips.begin(), clips.end(), [&](const TrackClip &c) { return c.track == t; } ) ) { clips.emplace_back( t, nullptr ); } } ); if (exclude) state.trackExclusions.push_back(t); } // Helper for the above, adds a track's clips to capturedClipArray (eliminates // duplication of this logic) void AddClipsToCaptured ( ClipMoveState &state, const ViewInfo &viewInfo, Track *t, bool withinSelection ) { if (withinSelection) AddClipsToCaptured( state, t, viewInfo.selectedRegion.t0(), viewInfo.selectedRegion.t1() ); else AddClipsToCaptured( state, t, t->GetStartTime(), t->GetEndTime() ); } WaveTrack *NthChannel(WaveTrack &leader, int nn) { if (nn < 0) return nullptr; return *TrackList::Channels( &leader ).begin().advance(nn); } int ChannelPosition(const Track *pChannel) { return static_cast( TrackList::Channels( pChannel ) .EndingAfter( pChannel ).size() ) - 1; } // Don't count right channels. WaveTrack *NthAudioTrack(TrackList &list, int nn) { if (nn >= 0) { for ( auto pTrack : list.Leaders< WaveTrack >() ) if (nn -- == 0) return pTrack; } return NULL; } // Don't count right channels. int TrackPosition(TrackList &list, const Track *pFindTrack) { pFindTrack = *list.FindLeader(pFindTrack); int nn = 0; for ( auto pTrack : list.Leaders< const WaveTrack >() ) { if (pTrack == pFindTrack) return nn; ++nn; } return -1; } WaveClip *FindClipAtTime(WaveTrack *pTrack, double time) { if (pTrack) { // WaveClip::GetClipAtX doesn't work unless the clip is on the screen and can return bad info otherwise // instead calculate the time manually double rate = pTrack->GetRate(); auto s0 = (sampleCount)(time * rate + 0.5); if (s0 >= 0) return pTrack->GetClipAtSample(s0); } return 0; } void DoOffset( ClipMoveState &state, Track *pTrack, double offset, WaveClip *pExcludedClip = nullptr ) { auto &clips = state.capturedClipArray; if ( !clips.empty() ) { for (auto &clip : clips) { if (clip.clip) { if (clip.clip != pExcludedClip) clip.clip->Offset( offset ); } else clip.track->Offset( offset ); } } else if ( pTrack ) // Was a shift-click for (auto channel : TrackList::Channels( pTrack )) channel->Offset( offset ); } } void TimeShiftHandle::CreateListOfCapturedClips ( ClipMoveState &state, const ViewInfo &viewInfo, Track &capturedTrack, TrackList &trackList, bool syncLocked, double clickTime ) { // The captured clip is the focus, but we need to create a list // of all clips that have to move, also... state.capturedClipArray.clear(); // First, if click was in selection, capture selected clips; otherwise // just the clicked-on clip if ( state.capturedClipIsSelection ) for (auto t : trackList.Selected()) AddClipsToCaptured( state, viewInfo, t, true ); else { state.capturedClipArray.push_back (TrackClip( &capturedTrack, state.capturedClip )); if (state.capturedClip) { // Check for other channels auto wt = static_cast(&capturedTrack); for ( auto channel : TrackList::Channels( wt ).Excluding( wt ) ) if (WaveClip *const clip = FindClipAtTime(channel, clickTime)) state.capturedClipArray.push_back(TrackClip(channel, clip)); } } // Now, if sync-lock is enabled, capture any clip that's linked to a // captured clip. if ( syncLocked ) { // AWD: capturedClipArray expands as the loop runs, so newly-added // clips are considered (the effect is like recursion and terminates // because AddClipsToCaptured doesn't add duplicate clips); to remove // this behavior just store the array size beforehand. for (unsigned int i = 0; i < state.capturedClipArray.size(); ++i) { auto trackClip = state.capturedClipArray[i]; { // Capture based on tracks that have clips -- that means we // don't capture based on links to label tracks for now (until // we can treat individual labels as clips) if ( trackClip.clip ) { // Iterate over sync-lock group tracks. for (auto t : TrackList::SyncLockGroup( trackClip.track )) AddClipsToCaptured(state, t, trackClip.clip->GetStartTime(), trackClip.clip->GetEndTime() ); } } #ifdef USE_MIDI { // Capture additional clips from NoteTracks trackClip.track->TypeSwitch( [&](NoteTrack *nt) { // Iterate over sync-lock group tracks. for (auto t : TrackList::SyncLockGroup(nt)) AddClipsToCaptured ( state, t, nt->GetStartTime(), nt->GetEndTime() ); }); } #endif } } } void TimeShiftHandle::DoSlideHorizontal ( ClipMoveState &state, TrackList &trackList, Track &capturedTrack ) { // Given a signed slide distance, move clips, but subject to constraint of // non-overlapping with other clips, so the distance may be adjusted toward // zero. if ( state.capturedClipArray.size() ) { double allowed; double initialAllowed; double safeBigDistance = 1000 + 2.0 * ( trackList.GetEndTime() - trackList.GetStartTime() ); do { // loop to compute allowed, does not actually move anything yet initialAllowed = state.hSlideAmount; for ( auto &trackClip : state.capturedClipArray ) { if (const auto clip = trackClip.clip) { // only audio clips are used to compute allowed const auto track = static_cast( trackClip.track ); // Move all other selected clips totally out of the way // temporarily because they're all moving together and // we want to find out if OTHER clips are in the way, // not one of the moving ones DoOffset( state, nullptr, -safeBigDistance, clip ); auto cleanup = finally( [&] { DoOffset( state, nullptr, safeBigDistance, clip ); } ); if ( track->CanOffsetClip(clip, state.hSlideAmount, &allowed) ) { if ( state.hSlideAmount != allowed ) { state.hSlideAmount = allowed; state.snapLeft = state.snapRight = -1; // see bug 1067 } } else { state.hSlideAmount = 0.0; state.snapLeft = state.snapRight = -1; // see bug 1067 } } } } while ( state.hSlideAmount != initialAllowed ); // finally, here is where clips are moved if ( state.hSlideAmount != 0.0 ) DoOffset( state, nullptr, state.hSlideAmount ); } else // For Shift key down, or // For non wavetracks, specifically label tracks ... DoOffset( state, &capturedTrack, state.hSlideAmount ); } UIHandle::Result TimeShiftHandle::Click (const TrackPanelMouseEvent &evt, AudacityProject *pProject) { using namespace RefreshCode; const bool unsafe = ProjectAudioIO::Get( *pProject ).IsAudioActive(); if ( unsafe ) return Cancelled; const wxMouseEvent &event = evt.event; const wxRect &rect = evt.rect; auto &viewInfo = ViewInfo::Get( *pProject ); const auto pView = std::static_pointer_cast(evt.pCell); const auto pTrack = pView ? pView->FindTrack().get() : nullptr; if (!pTrack) return RefreshCode::Cancelled; auto &trackList = TrackList::Get( *pProject ); mClipMoveState.clear(); mDidSlideVertically = false; const bool multiToolModeActive = (ToolCodes::multiTool == ProjectSettings::Get( *pProject ).GetTool()); const double clickTime = viewInfo.PositionToTime(event.m_x, rect.x); mClipMoveState.capturedClipIsSelection = (pTrack->GetSelected() && clickTime >= viewInfo.selectedRegion.t0() && clickTime < viewInfo.selectedRegion.t1()); mClipMoveState.capturedClip = NULL; mClipMoveState.capturedClipArray.clear(); bool ok = true; bool captureClips = false; if (!event.ShiftDown()) pTrack->TypeSwitch( [&](WaveTrack *wt) { if (nullptr == (mClipMoveState.capturedClip = wt->GetClipAtX(event.m_x))) ok = false; else captureClips = true; #ifdef USE_MIDI }, [&](NoteTrack *) { captureClips = true; #endif } ); if ( ! ok ) return Cancelled; else if ( captureClips ) CreateListOfCapturedClips( mClipMoveState, viewInfo, *pTrack, trackList, ProjectSettings::Get( *pProject ).IsSyncLocked(), clickTime ); mSlideUpDownOnly = event.CmdDown() && !multiToolModeActive; mRect = rect; mClipMoveState.mMouseClickX = event.m_x; mSnapManager = std::make_shared(&trackList, &viewInfo, &mClipMoveState.capturedClipArray, &mClipMoveState.trackExclusions, true); // don't snap to time mClipMoveState.snapLeft = -1; mClipMoveState.snapRight = -1; mSnapPreferRightEdge = mClipMoveState.capturedClip && (fabs(clickTime - mClipMoveState.capturedClip->GetEndTime()) < fabs(clickTime - mClipMoveState.capturedClip->GetStartTime())); return RefreshNone; } namespace { double FindDesiredSlideAmount( const ViewInfo &viewInfo, wxCoord xx, const wxMouseEvent &event, SnapManager *pSnapManager, bool slideUpDownOnly, bool snapPreferRightEdge, ClipMoveState &state, Track &capturedTrack, Track &track ) { if (slideUpDownOnly) return 0.0; else { double desiredSlideAmount = viewInfo.PositionToTime(event.m_x) - viewInfo.PositionToTime(state.mMouseClickX); double clipLeft = 0, clipRight = 0; track.TypeSwitch( [&](WaveTrack *mtw){ const double rate = mtw->GetRate(); // set it to a sample point desiredSlideAmount = rint(desiredSlideAmount * rate) / rate; }); // Adjust desiredSlideAmount using SnapManager if (pSnapManager) { if (state.capturedClip) { clipLeft = state.capturedClip->GetStartTime() + desiredSlideAmount; clipRight = state.capturedClip->GetEndTime() + desiredSlideAmount; } else { clipLeft = capturedTrack.GetStartTime() + desiredSlideAmount; clipRight = capturedTrack.GetEndTime() + desiredSlideAmount; } auto results = pSnapManager->Snap(&capturedTrack, clipLeft, false); auto newClipLeft = results.outTime; results = pSnapManager->Snap(&capturedTrack, clipRight, false); auto newClipRight = results.outTime; // Only one of them is allowed to snap if (newClipLeft != clipLeft && newClipRight != clipRight) { // Un-snap the un-preferred edge if (snapPreferRightEdge) newClipLeft = clipLeft; else newClipRight = clipRight; } // Take whichever one snapped (if any) and compute the NEW desiredSlideAmount state.snapLeft = -1; state.snapRight = -1; if (newClipLeft != clipLeft) { const double difference = (newClipLeft - clipLeft); desiredSlideAmount += difference; state.snapLeft = viewInfo.TimeToPosition(newClipLeft, xx); } else if (newClipRight != clipRight) { const double difference = (newClipRight - clipRight); desiredSlideAmount += difference; state.snapRight = viewInfo.TimeToPosition(newClipRight, xx); } } return desiredSlideAmount; } } bool FindCorrespondence( TrackList &trackList, Track &track, Track &capturedTrack, ClipMoveState &state) { const int diff = TrackPosition(trackList, &track) - TrackPosition(trackList, &capturedTrack); for ( auto &trackClip : state.capturedClipArray ) { if (trackClip.clip) { // Move all clips up or down by an equal count of audio tracks. // Can only move between tracks with equal numbers of channels, // and among corresponding channels. Track *const pSrcTrack = trackClip.track; auto pDstTrack = NthAudioTrack(trackList, diff + TrackPosition(trackList, pSrcTrack)); if (!pDstTrack) return false; if (TrackList::Channels(pSrcTrack).size() != TrackList::Channels(pDstTrack).size()) return false; auto pDstChannel = NthChannel( *pDstTrack, ChannelPosition(pSrcTrack)); if (!pDstChannel) { wxASSERT(false); return false; } trackClip.dstTrack = pDstChannel; } } return true; } bool CheckFit( const ViewInfo &viewInfo, wxCoord xx, ClipMoveState &state, double tolerance, double &desiredSlideAmount ) { (void)xx;// Compiler food (void)viewInfo;// Compiler food bool ok = true; double firstTolerance = tolerance; // The desiredSlideAmount may change and the tolerance may get used up. for ( unsigned iPass = 0; iPass < 2 && ok; ++iPass ) { for ( auto &trackClip : state.capturedClipArray ) { WaveClip *const pSrcClip = trackClip.clip; if (pSrcClip) { ok = trackClip.dstTrack->CanInsertClip( pSrcClip, desiredSlideAmount, tolerance ); if( !ok ) break; } } // If it fits ok, desiredSlideAmount could have been updated to get // the clip to fit. // Check again, in the new position, this time with zero tolerance. if (firstTolerance == 0) break; else tolerance = 0.0; } return ok; } struct TemporaryClipRemover { TemporaryClipRemover( ClipMoveState &clipMoveState ) : state( clipMoveState ) { // Pluck the moving clips out of their tracks for ( auto &trackClip : state.capturedClipArray ) { WaveClip *const pSrcClip = trackClip.clip; if (pSrcClip) trackClip.holder = // Assume track is wave because it has a clip static_cast(trackClip.track)-> RemoveAndReturnClip(pSrcClip); } } void Fail() { // Cause destructor to put all clips back where they came from for ( auto &trackClip : state.capturedClipArray ) trackClip.dstTrack = static_cast(trackClip.track); } ~TemporaryClipRemover() { // Complete (or roll back) the vertical move. // Put moving clips into their destination tracks // which become the source tracks when we move again for ( auto &trackClip : state.capturedClipArray ) { WaveClip *const pSrcClip = trackClip.clip; if (pSrcClip) { const auto dstTrack = trackClip.dstTrack; dstTrack->AddClip(std::move(trackClip.holder)); trackClip.track = dstTrack; } } } ClipMoveState &state; }; } bool TimeShiftHandle::DoSlideVertical ( ViewInfo &viewInfo, wxCoord xx, ClipMoveState &state, TrackList &trackList, Track &capturedTrack, Track &dstTrack, double &desiredSlideAmount ) { if (!FindCorrespondence( trackList, dstTrack, capturedTrack, state)) return false; // Having passed that test, remove clips temporarily from their // tracks, so moving clips don't interfere with each other // when we call CanInsertClip() TemporaryClipRemover remover{ state }; // Now check that the move is possible double slide = desiredSlideAmount; // remember amount requested. // The test for tolerance will need review with FishEye! // The tolerance is supposed to be the time for one pixel, // i.e. one pixel tolerance at current zoom. double tolerance = viewInfo.PositionToTime(xx + 1) - viewInfo.PositionToTime(xx); bool ok = CheckFit( viewInfo, xx, state, tolerance, desiredSlideAmount ); if (!ok) { // Failure, even with using tolerance. remover.Fail(); // Failure -- we'll put clips back where they were // ok will next indicate if a horizontal slide is OK. tolerance = 0.0; desiredSlideAmount = slide; ok = CheckFit( viewInfo, xx, state, tolerance, desiredSlideAmount ); for ( auto &trackClip : state.capturedClipArray) { WaveClip *const pSrcClip = trackClip.clip; if (pSrcClip){ // Attempt to move to a new track did not work. // Put the clip back appropriately shifted! if( ok) trackClip.holder->Offset(slide); } } // Make the offset permanent; start from a "clean slate" if( ok ) { state.mMouseClickX = xx; if (state.capturedClipIsSelection) { // Slide the selection, too viewInfo.selectedRegion.move( slide ); } state.hSlideAmount = 0; } return false; } // Make the offset permanent; start from a "clean slate" state.mMouseClickX = xx; return true; } UIHandle::Result TimeShiftHandle::Drag (const TrackPanelMouseEvent &evt, AudacityProject *pProject) { using namespace RefreshCode; const bool unsafe = ProjectAudioIO::Get( *pProject ).IsAudioActive(); if (unsafe) { this->Cancel(pProject); return RefreshAll | Cancelled; } const wxMouseEvent &event = evt.event; auto &viewInfo = ViewInfo::Get( *pProject ); TrackView *trackView = dynamic_cast(evt.pCell.get()); Track *track = trackView ? trackView->FindTrack().get() : nullptr; // Uncommenting this permits drag to continue to work even over the controls area /* track = static_cast(evt.pCell)->FindTrack().get(); */ if (!track) { // Allow sliding if the pointer is not over any track, but only if x is // within the bounds of the tracks area. if (event.m_x >= mRect.GetX() && event.m_x < mRect.GetX() + mRect.GetWidth()) track = mCapturedTrack.get(); } // May need a shared_ptr to reassign mCapturedTrack below auto pTrack = Track::SharedPointer( track ); if (!pTrack) return RefreshCode::RefreshNone; auto &trackList = TrackList::Get( *pProject ); // GM: DoSlide now implementing snap-to // samples functionality based on sample rate. // Start by undoing the current slide amount; everything // happens relative to the original horizontal position of // each clip... DoOffset( mClipMoveState, mCapturedTrack.get(), -mClipMoveState.hSlideAmount ); if ( mClipMoveState.capturedClipIsSelection ) { // Slide the selection, too viewInfo.selectedRegion.move( -mClipMoveState.hSlideAmount ); } mClipMoveState.hSlideAmount = 0.0; double desiredSlideAmount = FindDesiredSlideAmount( viewInfo, mRect.x, event, mSnapManager.get(), mSlideUpDownOnly, mSnapPreferRightEdge, mClipMoveState, *mCapturedTrack, *pTrack ); // Scroll during vertical drag. // EnsureVisible(pTrack); //vvv Gale says this has problems on Linux, per bug 393 thread. Revert for 2.0.2. bool slidVertically = false; // If the mouse is over a track that isn't the captured track, // decide which tracks the captured clips should go to. bool fail = ( mClipMoveState.capturedClip && pTrack != mCapturedTrack /* && !mCapturedClipIsSelection*/ && pTrack->TypeSwitch( [&] (WaveTrack *) { if ( DoSlideVertical( viewInfo, event.m_x, mClipMoveState, trackList, *mCapturedTrack, *pTrack, desiredSlideAmount ) ) { mCapturedTrack = pTrack; mDidSlideVertically = true; } else return true; // Not done yet, check for horizontal movement. slidVertically = true; return false; }) ); if (fail) return RefreshAll; if (desiredSlideAmount == 0.0) return RefreshAll; mClipMoveState.hSlideAmount = desiredSlideAmount; DoSlideHorizontal( mClipMoveState, trackList, *mCapturedTrack ); if (mClipMoveState.capturedClipIsSelection) { // Slide the selection, too viewInfo.selectedRegion.move( mClipMoveState.hSlideAmount ); } if (slidVertically) { // NEW origin mClipMoveState.hSlideAmount = 0; } return RefreshAll; } HitTestPreview TimeShiftHandle::Preview (const TrackPanelMouseState &, const AudacityProject *pProject) { // After all that, it still may be unsafe to drag. // Even if so, make an informative cursor change from default to "banned." const bool unsafe = ProjectAudioIO::Get( *pProject ).IsAudioActive(); return HitPreview(pProject, unsafe); } UIHandle::Result TimeShiftHandle::Release (const TrackPanelMouseEvent &, AudacityProject *pProject, wxWindow *) { using namespace RefreshCode; const bool unsafe = ProjectAudioIO::Get( *pProject ).IsAudioActive(); if (unsafe) return this->Cancel(pProject); Result result = RefreshNone; // Do not draw yellow lines if ( mClipMoveState.snapLeft != -1 || mClipMoveState.snapRight != -1) { mClipMoveState.snapLeft = mClipMoveState.snapRight = -1; result |= RefreshAll; } if ( !mDidSlideVertically && mClipMoveState.hSlideAmount == 0 ) return result; for ( auto &trackClip : mClipMoveState.capturedClipArray ) { WaveClip* pWaveClip = trackClip.clip; // Note that per AddClipsToCaptured(Track *t, double t0, double t1), // in the non-WaveTrack case, the code adds a NULL clip to capturedClipArray, // so we have to check for that any time we're going to deref it. // Previous code did not check it here, and that caused bug 367 crash. if (pWaveClip && trackClip.track != trackClip.origTrack) { // Now that user has dropped the clip into a different track, // make sure the sample rate matches the destination track. // Assume the clip was dropped in a wave track pWaveClip->Resample (static_cast(trackClip.track)->GetRate()); pWaveClip->MarkChanged(); } } wxString msg; bool consolidate; if (mDidSlideVertically) { msg = _("Moved clips to another track"); consolidate = false; } else { msg.Printf( ( mClipMoveState.hSlideAmount > 0 ? _("Time shifted tracks/clips right %.02f seconds") : _("Time shifted tracks/clips left %.02f seconds") ), fabs( mClipMoveState.hSlideAmount ) ); consolidate = true; } ProjectHistory::Get( *pProject ).PushState(msg, _("Time-Shift"), consolidate ? (UndoPush::CONSOLIDATE) : (UndoPush::AUTOSAVE)); return result | FixScrollbars; } UIHandle::Result TimeShiftHandle::Cancel(AudacityProject *pProject) { ProjectHistory::Get( *pProject ).RollbackState(); return RefreshCode::RefreshAll; } void TimeShiftHandle::Draw( TrackPanelDrawingContext &context, const wxRect &rect, unsigned iPass ) { if ( iPass == TrackArtist::PassSnapping ) { auto &dc = context.dc; // Draw snap guidelines if we have any if ( mSnapManager ) { mSnapManager->Draw( &dc, mClipMoveState.snapLeft, mClipMoveState.snapRight ); } } } wxRect TimeShiftHandle::DrawingArea( const wxRect &rect, const wxRect &panelRect, unsigned iPass ) { if ( iPass == TrackArtist::PassSnapping ) return MaximizeHeight( rect, panelRect ); else return rect; }