/********************************************************************** 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 "../../ProjectAudioIO.h" #include "../../ProjectHistory.h" #include "../../ProjectSettings.h" #include "../../RefreshCode.h" #include "../../Snap.h" #include "../../Track.h" #include "../../TrackArtist.h" #include "../../TrackPanelDrawingContext.h" #include "../../TrackPanelMouseEvent.h" #include "../../UndoManager.h" #include "../../ViewInfo.h" #include "../../../images/Cursors.h" TimeShiftHandle::TimeShiftHandle ( const std::shared_ptr &pTrack, bool gripHit ) : mGripHit{ gripHit } { mClipMoveState.mCapturedTrack = pTrack; } void TimeShiftHandle::Enter(bool, AudacityProject *) { #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 = XO("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() { } void ClipMoveState::DoHorizontalOffset( double offset ) { if ( !shifters.empty() ) { for ( auto &pair : shifters ) pair.second->DoHorizontalOffset( offset ); } else { for (auto channel : TrackList::Channels( mCapturedTrack.get() )) channel->Offset( offset ); } } TrackShifter::~TrackShifter() = default; void TrackShifter::UnfixIntervals( std::function< bool( const TrackInterval& ) > pred ) { for ( auto iter = mFixed.begin(); iter != mFixed.end(); ) { if ( pred( *iter) ) { mMoving.push_back( std::move( *iter ) ); iter = mFixed.erase( iter ); mAllFixed = false; } else ++iter; } } void TrackShifter::UnfixAll() { std::move( mFixed.begin(), mFixed.end(), std::back_inserter(mMoving) ); mFixed = Intervals{}; mAllFixed = false; } void TrackShifter::SelectInterval( const TrackInterval & ) { UnfixAll(); } void TrackShifter::CommonSelectInterval(const TrackInterval &interval) { UnfixIntervals( [&](auto &myInterval){ return !(interval.End() < myInterval.Start() || myInterval.End() < interval.Start()); }); } double TrackShifter::HintOffsetLarger(double desiredOffset) { return desiredOffset; } double TrackShifter::QuantizeOffset(double desiredOffset) { return desiredOffset; } double TrackShifter::AdjustOffsetSmaller(double desiredOffset) { return desiredOffset; } bool TrackShifter::MayMigrateTo(Track &) { return false; } bool TrackShifter::CommonMayMigrateTo(Track &otherTrack) { auto &track = GetTrack(); // Both tracks need to be owned to decide this auto pMyList = track.GetOwner().get(); auto pOtherList = otherTrack.GetOwner().get(); if (pMyList && pOtherList) { // Can migrate to another track of the same kind... if ( otherTrack.SameKindAs( track ) ) { // ... with the same number of channels ... auto myChannels = TrackList::Channels( &track ); auto otherChannels = TrackList::Channels( &otherTrack ); if (myChannels.size() == otherChannels.size()) { // ... and where this track and the other have corresponding // positions return myChannels.size() == 1 || std::distance(myChannels.first, pMyList->Find(&track)) == std::distance(otherChannels.first, pOtherList->Find(&otherTrack)); } } } return false; } auto TrackShifter::Detach() -> Intervals { return {}; } bool TrackShifter::AdjustFit( const Track &, const Intervals&, double &, double) { return false; } bool TrackShifter::Attach( Intervals ) { return true; } bool TrackShifter::FinishMigration() { return true; } void TrackShifter::DoHorizontalOffset( double offset ) { if (!AllFixed()) GetTrack().Offset( offset ); } void TrackShifter::InitIntervals() { mMoving.clear(); mFixed = GetTrack().GetIntervals(); } CoarseTrackShifter::CoarseTrackShifter( Track &track ) : mpTrack{ track.SharedPointer() } { InitIntervals(); } CoarseTrackShifter::~CoarseTrackShifter() = default; auto CoarseTrackShifter::HitTest( double, const ViewInfo&, HitTestParams* ) -> HitTestResult { return HitTestResult::Track; } bool CoarseTrackShifter::SyncLocks() { return false; } template<> auto MakeTrackShifter::Implementation() -> Function { return [](Track &track, AudacityProject&) { return std::make_unique(track); }; } void ClipMoveState::Init( AudacityProject &project, Track &capturedTrack, TrackShifter::HitTestResult hitTestResult, std::unique_ptr pHit, double clickTime, const ViewInfo &viewInfo, TrackList &trackList, bool syncLocked ) { shifters.clear(); auto &state = *this; state.mCapturedTrack = capturedTrack.SharedPointer(); switch (hitTestResult) { case TrackShifter::HitTestResult::Miss: wxASSERT(false); pHit.reset(); break; case TrackShifter::HitTestResult::Track: pHit.reset(); break; case TrackShifter::HitTestResult::Intervals: break; case TrackShifter::HitTestResult::Selection: state.movingSelection = true; break; default: break; } if (!pHit) return; state.shifters[&capturedTrack] = std::move( pHit ); // Collect TrackShifters for the rest of the tracks for ( auto track : trackList.Any() ) { auto &pShifter = state.shifters[track]; if (!pShifter) pShifter = MakeTrackShifter::Call( *track, project ); } if ( state.movingSelection ) { // All selected tracks may move some intervals const TrackInterval interval{ viewInfo.selectedRegion.t0(), viewInfo.selectedRegion.t1() }; for ( const auto &pair : state.shifters ) { auto &shifter = *pair.second; auto &track = shifter.GetTrack(); if (&track == &capturedTrack) // Don't change the choice of intervals made by HitTest continue; if ( track.IsSelected() ) shifter.SelectInterval( interval ); } } else { // Move intervals only of the chosen channel group for ( auto channel : TrackList::Channels( &capturedTrack ) ) { auto &shifter = *state.shifters[channel]; if ( channel != &capturedTrack ) shifter.SelectInterval(TrackInterval{clickTime, clickTime}); } } // Sync lock propagation of unfixing of intervals if ( syncLocked ) { bool change = true; while( change ) { change = false; // Iterate over all unfixed intervals in all shifters // that do propagation... for ( auto &pair : state.shifters ) { auto &shifter = *pair.second.get(); if (!shifter.SyncLocks()) continue; auto &track = shifter.GetTrack(); auto &intervals = shifter.MovingIntervals(); for (auto &interval : intervals) { // ...and tell all other tracks to select that interval... for ( auto &pair2 : state.shifters ) { auto &shifter2 = *pair2.second.get(); if (&shifter2.GetTrack() == &track) continue; auto size = shifter2.MovingIntervals().size(); shifter2.SelectInterval( interval ); change = change || (shifter2.SyncLocks() && size != shifter2.MovingIntervals().size()); } } } // ... and repeat if any other interval became unfixed in a // shifter that propagates } } } const TrackInterval *ClipMoveState::CapturedInterval() const { auto pTrack = mCapturedTrack.get(); if ( pTrack ) { auto iter = shifters.find( pTrack ); if ( iter != shifters.end() ) { auto &pShifter = iter->second; if ( pShifter ) { auto &intervals = pShifter->MovingIntervals(); if ( !intervals.empty() ) return &intervals[0]; } } } return nullptr; } double ClipMoveState::DoSlideHorizontal( double desiredSlideAmount ) { auto &state = *this; auto &capturedTrack = *state.mCapturedTrack; // 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.shifters.empty() ) { double initialAllowed = 0; do { // loop to compute allowed, does not actually move anything yet initialAllowed = desiredSlideAmount; for (auto &pair : shifters) { auto newAmount = pair.second->AdjustOffsetSmaller( desiredSlideAmount ); if ( desiredSlideAmount != newAmount ) { if ( newAmount * desiredSlideAmount < 0 || fabs(newAmount) > fabs(desiredSlideAmount) ) { wxASSERT( false ); // AdjustOffsetSmaller didn't honor postcondition! newAmount = 0; // Be sure the loop progresses to termination! } desiredSlideAmount = newAmount; state.snapLeft = state.snapRight = -1; // see bug 1067 } if (newAmount == 0) break; } } while ( desiredSlideAmount != initialAllowed ); } // Whether moving intervals or a whole track, // finally, here is where clips are moved if ( desiredSlideAmount != 0.0 ) state.DoHorizontalOffset( desiredSlideAmount ); return (state.hSlideAmount = desiredSlideAmount); } namespace { SnapPointArray FindCandidates( const TrackList &tracks, const ClipMoveState::ShifterMap &shifters ) { // Compare with the other function FindCandidates in Snap // Make the snap manager more selective than it would be if just constructed // from the track list SnapPointArray candidates; for ( const auto &pair : shifters ) { auto &shifter = pair.second; auto &track = shifter->GetTrack(); for (const auto &interval : shifter->FixedIntervals() ) { candidates.emplace_back( interval.Start(), &track ); if ( interval.Start() != interval.End() ) candidates.emplace_back( interval.End(), &track ); } } return candidates; } } 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); auto pShifter = MakeTrackShifter::Call( *pTrack, *pProject ); auto hitTestResult = TrackShifter::HitTestResult::Track; if (!event.ShiftDown()) { TrackShifter::HitTestParams params{ rect, event.m_x, event.m_y }; hitTestResult = pShifter->HitTest( clickTime, viewInfo, ¶ms ); switch( hitTestResult ) { case TrackShifter::HitTestResult::Miss: return Cancelled; default: break; } } else { // just do shifting of one whole track } mClipMoveState.Init( *pProject, *pTrack, hitTestResult, std::move( pShifter ), clickTime, viewInfo, trackList, ProjectSettings::Get( *pProject ).IsSyncLocked() ); mSlideUpDownOnly = event.CmdDown() && !multiToolModeActive; mRect = rect; mClipMoveState.mMouseClickX = event.m_x; mSnapManager = std::make_shared(*trackList.GetOwner(), FindCandidates( trackList, mClipMoveState.shifters ), viewInfo, true, // don't snap to time kPixelTolerance); mClipMoveState.snapLeft = -1; mClipMoveState.snapRight = -1; auto pInterval = mClipMoveState.CapturedInterval(); mSnapPreferRightEdge = pInterval && (fabs(clickTime - pInterval->End()) < fabs(clickTime - pInterval->Start())); return RefreshNone; } namespace { double FindDesiredSlideAmount( const ViewInfo &viewInfo, wxCoord xx, const wxMouseEvent &event, SnapManager *pSnapManager, bool slideUpDownOnly, bool snapPreferRightEdge, ClipMoveState &state, Track &track ) { auto &capturedTrack = *state.mCapturedTrack; if (slideUpDownOnly) return 0.0; else { double desiredSlideAmount = viewInfo.PositionToTime(event.m_x) - viewInfo.PositionToTime(state.mMouseClickX); double clipLeft = 0, clipRight = 0; if (!state.shifters.empty()) desiredSlideAmount = state.shifters[ &track ]->QuantizeOffset( desiredSlideAmount ); // Adjust desiredSlideAmount using SnapManager if (pSnapManager) { auto pInterval = state.CapturedInterval(); if (pInterval) { clipLeft = pInterval->Start() + desiredSlideAmount; clipRight = pInterval->End() + 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; } } using Correspondence = std::unordered_map< Track*, Track* >; bool FindCorrespondence( Correspondence &correspondence, TrackList &trackList, Track &track, ClipMoveState &state) { auto &capturedTrack = state.mCapturedTrack; auto sameType = [&]( auto pTrack ){ return capturedTrack->SameKindAs( *pTrack ); }; if (!sameType(&track)) return false; // All tracks of the same kind as the captured track auto range = trackList.Any() + sameType; // Find how far this track would shift down among those (signed) const auto myPosition = std::distance( range.first, trackList.Find( capturedTrack.get() ) ); const auto otherPosition = std::distance( range.first, trackList.Find( &track ) ); auto diff = otherPosition - myPosition; // Point to destination track auto iter = range.first.advance( diff > 0 ? diff : 0 ); for (auto pTrack : range) { auto &pShifter = state.shifters[pTrack]; if ( !pShifter->MovingIntervals().empty() ) { // One of the interesting tracks auto pOther = *iter; if ( diff < 0 || !pOther ) // No corresponding track return false; if ( !pShifter->MayMigrateTo(*pOther) ) // Rejected for other reason return false; correspondence[ pTrack ] = pOther; } if ( diff < 0 ) ++diff; // Still consuming initial tracks else ++iter; // Safe to increment TrackIter even at end of range } return true; } using DetachedIntervals = std::unordered_map; bool CheckFit( ClipMoveState &state, const Correspondence &correspondence, const DetachedIntervals &intervals, double tolerance, double &desiredSlideAmount ) { 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 &pair : state.shifters ) { auto *pSrcTrack = pair.first; auto iter = correspondence.find( pSrcTrack ); if ( iter != correspondence.end() ) if( auto *pOtherTrack = iter->second ) if ( !(ok = pair.second->AdjustFit( *pOtherTrack, intervals.at(pSrcTrack), desiredSlideAmount /*in,out*/, tolerance)) ) 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; } [[noreturn]] void MigrationFailure() { // Tracks may be in an inconsistent state; throw to the application // handler which restores consistency from undo history throw SimpleMessageBoxException{ XO("Could not shift between tracks")}; } struct TemporaryClipRemover { TemporaryClipRemover( ClipMoveState &clipMoveState ) : state( clipMoveState ) { // Pluck the moving clips out of their tracks for (auto &pair : state.shifters) detached[pair.first] = pair.second->Detach(); } void Fail() { failed = true; } void Reinsert( std::unordered_map< Track*, Track* > &correspondence ) { for (auto &pair : detached) { auto pTrack = pair.first; if (!failed && correspondence.count(pTrack)) pTrack = correspondence[pTrack]; auto &pShifter = state.shifters[pTrack]; if (!pShifter->Attach( std::move( pair.second ) )) MigrationFailure(); } } ClipMoveState &state; DetachedIntervals detached; bool failed = false; }; } bool TimeShiftHandle::DoSlideVertical ( ViewInfo &viewInfo, wxCoord xx, ClipMoveState &state, TrackList &trackList, Track &dstTrack, double &desiredSlideAmount ) { Correspondence correspondence; if (!FindCorrespondence( correspondence, trackList, dstTrack, 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( state, correspondence, remover.detached, tolerance, desiredSlideAmount /*in,out*/ ); if (!ok) { // Failure, even with using tolerance. remover.Fail(); remover.Reinsert( correspondence ); return false; } // Make the offset permanent; start from a "clean slate" state.mMouseClickX = xx; remover.Reinsert( correspondence ); 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 = mClipMoveState.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: slide 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... mClipMoveState.DoHorizontalOffset( -mClipMoveState.hSlideAmount ); if ( mClipMoveState.movingSelection ) { // 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, *pTrack ); // Scroll during vertical drag. // If the mouse is over a track that isn't the captured track, // decide which tracks the captured clips should go to. // EnsureVisible(pTrack); //vvv Gale says this has problems on Linux, per bug 393 thread. Revert for 2.0.2. bool slidVertically = ( pTrack != mClipMoveState.mCapturedTrack /* && !mCapturedClipIsSelection*/ && DoSlideVertical( viewInfo, event.m_x, mClipMoveState, trackList, *pTrack, desiredSlideAmount ) ); if (slidVertically) { mClipMoveState.mCapturedTrack = pTrack; mDidSlideVertically = true; } if (desiredSlideAmount == 0.0) return RefreshAll; // Note that mouse dragging doesn't use TrackShifter::HintOffsetLarger() mClipMoveState.DoSlideHorizontal( desiredSlideAmount ); if (mClipMoveState.movingSelection) { // Slide the selection, too viewInfo.selectedRegion.move( mClipMoveState.hSlideAmount ); } if (slidVertically) { // NEW origin mClipMoveState.hSlideAmount = 0; } return RefreshAll; } HitTestPreview TimeShiftHandle::Preview (const TrackPanelMouseState &, 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 &pair : mClipMoveState.shifters ) if ( !pair.second->FinishMigration() ) MigrationFailure(); TranslatableString msg; bool consolidate; if (mDidSlideVertically) { msg = XO("Moved clips to another track"); consolidate = false; } else { msg = ( mClipMoveState.hSlideAmount > 0 ? XO("Time shifted tracks/clips right %.02f seconds") : XO("Time shifted tracks/clips left %.02f seconds") ) .Format( fabs( mClipMoveState.hSlideAmount ) ); consolidate = true; } ProjectHistory::Get( *pProject ).PushState(msg, XO("Time-Shift"), consolidate ? (UndoPush::CONSOLIDATE) : (UndoPush::NONE)); 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( TrackPanelDrawingContext &, const wxRect &rect, const wxRect &panelRect, unsigned iPass ) { if ( iPass == TrackArtist::PassSnapping ) return MaximizeHeight( rect, panelRect ); else return rect; }