diff --git a/src/tracks/playabletrack/wavetrack/ui/WaveTrackView.cpp b/src/tracks/playabletrack/wavetrack/ui/WaveTrackView.cpp index f3250ab82..69039b1be 100644 --- a/src/tracks/playabletrack/wavetrack/ui/WaveTrackView.cpp +++ b/src/tracks/playabletrack/wavetrack/ui/WaveTrackView.cpp @@ -12,11 +12,15 @@ Paul Licameli split from TrackPanel.cpp #include "../../../../Experimental.h" +#include #include #include "../../../../WaveClip.h" #include "../../../../WaveTrack.h" +#include "../../../../HitTestResult.h" +#include "../../../../ProjectHistory.h" +#include "../../../../RefreshCode.h" #include "../../../../TrackArtist.h" #include "../../../../TrackPanelDrawingContext.h" #include "../../../../TrackPanelMouseEvent.h" @@ -27,6 +31,335 @@ Paul Licameli split from TrackPanel.cpp #include "../../../ui/TimeShiftHandle.h" +namespace { + +using WaveTrackSubViewPtrs = std::vector< std::shared_ptr< WaveTrackSubView > >; + +// Structure that collects and modifies information on sub-view positions +// Written with great generality, allowing any number of sub-views +struct SubViewAdjuster +{ + enum { HotZoneSize = 5 }; // so many pixels at top and bottom of each subview + + SubViewAdjuster( WaveTrackView &view ) + : mwView{ + std::static_pointer_cast( view.shared_from_this() ) } + { + mSubViews = view.GetAllSubViews(); + mOrigPlacements = mNewPlacements = view.SavePlacements(); + FindPermutation(); + } + + void FindPermutation() + { + // Find a certain sort of the sub-views + auto size = mOrigPlacements.size(); + wxASSERT( mSubViews.size() == size ); + mPermutation.resize( size ); + const auto begin = mPermutation.begin(), end = mPermutation.end(); + std::iota( begin, end, 0 ); + static auto invisible = []( const WaveTrackSubViewPlacement &placement ){ + return placement.index < 0 || placement.fraction <= 0; + }; + const auto comp = [this]( size_t ii, size_t jj ){ + auto &pi = mOrigPlacements[ii]; + bool iInvisible = invisible( pi ); + + auto &pj = mOrigPlacements[jj]; + bool jInvisible = invisible( pj ); + + // Sort the invisibles to the front, rest by index + if ( iInvisible != jInvisible ) + return iInvisible; + else if ( !iInvisible ) + return pi.index < pj.index; + else + // Minor sort among the invisible views by their type + return mSubViews[ii]->SubViewType() < mSubViews[jj]->SubViewType(); + }; + std::sort( begin, end, comp ); + // Find the start of visible sub-views + auto first = std::find_if( begin, end, [this](size_t ii){ + return !invisible( mOrigPlacements[ii] ); + } ); + mFirstSubView = first - begin; + } + + bool ModifyPermutation( bool top ) + { + bool rotated = false; + const auto pBegin = mPermutation.begin(), pEnd = mPermutation.end(); + auto pFirst = pBegin + mFirstSubView; + if ( mFirstSubView > 0 ) { + // In case of dragging the top edge of the topmost view, or the + // bottom edge of the bottommost, decide which of the invisible + // views can become visible, and reassign the sequence. + // For definiteness, that choice depends on the subview type numbers; + // see the sorting criteria above. + --mFirstSubView; + --pFirst; + if ( top ) { + // If you drag down the top, the greatest-numbered invisible + // subview type will appear there. + mNewPlacements[ *pFirst ].fraction = 0; + } + else { + // If you drag up the bottom, let the least-numbered invisible + // subview type appear there. + mNewPlacements[ *pBegin ].fraction = 0; + std::rotate( pBegin, pBegin + 1, pEnd ); + rotated = true; + } + } + // Reassign index numbers to all sub-views and 0 fraction to invisibles + for ( auto pIter = pBegin; pIter != pFirst; ++pIter ) { + auto &placement = mNewPlacements[ *pIter ]; + placement.index = -1; + placement.fraction = 0; + } + size_t index = 0; + for ( auto pIter = pFirst; pIter != pEnd; ++pIter ) + mNewPlacements[ *pIter ].index = index++; + return rotated; + } + + std::pair< size_t, bool > + HitTest( WaveTrackSubView &subView, + wxCoord yy, wxCoord top, wxCoord height ) + { + const auto begin = mPermutation.begin(), end = mPermutation.end(); + auto iter = std::find_if( begin, end, [&](size_t ii){ + return mSubViews[ ii ].get() == &subView; + } ); + auto index = iter - begin; + auto size = mPermutation.size(); + if ( index < size ) { + yy -= top; + if ( yy >= 0 && yy < HotZoneSize && index > 0 ) + return { index, true }; // top hit + if ( yy < height && yy >= height - HotZoneSize && + // Have not yet called ModifyPermutation; dragging bottom of + // bottommost view allowed only if at least one view is invisible + ( index < size - 1 || mFirstSubView > 0 ) ) + return { index, false }; // bottom hit + } + return { size, false }; // not hit + } + + void UpdateViews( bool rollback ) + { + auto pView = mwView.lock(); + if ( pView ) { + auto pTrack = static_cast< WaveTrack* >( pView->FindTrack().get() ); + for ( auto pChannel : TrackList::Channels( pTrack ) ) + WaveTrackView::Get( *pChannel ).RestorePlacements( + rollback ? mOrigPlacements : mNewPlacements ); + } + } + + std::weak_ptr< WaveTrackView > mwView; + WaveTrackSubViewPtrs mSubViews; + WaveTrackSubViewPlacements mOrigPlacements, mNewPlacements; + // Array mapping ordinal into the placement and subview arrays + std::vector< size_t > mPermutation; + // index into mPermutation + size_t mFirstSubView{}; +}; + +class SubViewAdjustHandle : public UIHandle +{ +public: + enum { MinHeight = SubViewAdjuster::HotZoneSize }; + + static UIHandlePtr HitTest( + WaveTrackView &view, WaveTrackSubView &subView, + const TrackPanelMouseState &state ) + { + SubViewAdjuster adjuster{ view }; + auto hit = adjuster.HitTest( subView, + state.state.GetY(), state.rect.GetTop(), state.rect.GetHeight() ); + auto index = hit.first; + + if ( index < adjuster.mPermutation.size() ) + return std::make_shared< SubViewAdjustHandle >( + std::move( adjuster ), index, hit.second + ); + else + return {}; + } + + SubViewAdjustHandle( + SubViewAdjuster &&adjuster, size_t subViewIndex, bool top ) + : mAdjuster{ std::move( adjuster ) } + , mMySubView{ subViewIndex } + , mTop{ top } + { + if ( mAdjuster.ModifyPermutation( top ) ) + --mMySubView; + } + + Result Click( + const TrackPanelMouseEvent &event, AudacityProject * ) override + { + using namespace RefreshCode; + const auto &permutation = mAdjuster.mPermutation; + const auto size = permutation.size(); + if ( mMySubView >= size ) + return Cancelled; + + const auto &rect = event.rect; + const auto height = rect.GetHeight(); + mOrigHeight = height; + + wxASSERT( height == + mAdjuster.mOrigPlacements[ mAdjuster.mPermutation[ mMySubView ] ] + .fraction + ); + + // Find the total height of the sub-views that may resize + // Note that this depends on the redenomination of fractions that + // happened in the last call to GetSubViews + mTotalHeight = 0; + const auto begin = permutation.begin(); + auto iter = begin + ( mTop ? mAdjuster.mFirstSubView : mMySubView ); + const auto end = ( mTop ? begin + mMySubView + 1 : permutation.end() ); + for (; iter != end; ++iter) { + const auto &placement = mAdjuster.mOrigPlacements[ *iter ]; + mTotalHeight += placement.fraction; + } + + // Compute the maximum and minimum Y coordinates for drag effect + if ( mTop ) { + mOrigY = rect.GetTop(); + mYMax = rect.GetBottom(); + mYMin = mYMax - mTotalHeight + 1; + } + else { + mOrigY = rect.GetBottom(); + mYMin = rect.GetTop(); + mYMax = mYMin + mTotalHeight - 1; + } + + return RefreshNone; + } + + Result Drag( const TrackPanelMouseEvent &event, AudacityProject * ) override + { + using namespace RefreshCode; + auto pView = mAdjuster.mwView.lock(); + if ( !pView ) + return Cancelled; + + // Find new height for the dragged sub-view + auto newY = std::max( mYMin, std::min( mYMax, event.event.GetY() ) ); + const auto delta = newY - mOrigY; + wxCoord newHeight = mTop + ? mOrigHeight - delta + : mOrigHeight + delta + ; + wxASSERT( newHeight >= 0 && newHeight <= mTotalHeight ); + if ( newHeight < MinHeight ) + // Snap the dragged sub-view to nothing + newHeight = 0; + + // Reassign height for the dragged sub-view + auto &myPlacement = + mAdjuster.mNewPlacements[ mAdjuster.mPermutation[ mMySubView ] ]; + myPlacement.fraction = newHeight; + + // Grow or shrink other sub-views + auto excess = newHeight - mOrigHeight; // maybe negative + const auto adjustHeight = [&](size_t ii) { + if (excess == 0) + return true; + + auto index = mAdjuster.mPermutation[ ii ]; + + const auto &origPlacement = mAdjuster.mOrigPlacements[ index ]; + const auto oldFraction = origPlacement.fraction; + + auto &placement = mAdjuster.mNewPlacements[ index ]; + auto &fraction = placement.fraction; + + if (excess > oldFraction) { + excess -= oldFraction, fraction = 0; + return false; + } + else { + auto newFraction = oldFraction - excess; + if ( newFraction < MinHeight ) { + // This snaps very short sub-views to nothing + myPlacement.fraction += newFraction; + fraction = 0; + } + else + fraction = newFraction; + return true; + } + }; + if ( mTop ) { + for ( size_t ii = mMySubView; ii > 0; ) { + --ii; + if ( adjustHeight( ii ) ) + break; + } + } + else { + for ( size_t ii = mMySubView + 1, size = mAdjuster.mPermutation.size(); + ii < size; ++ii + ) { + if ( adjustHeight( ii ) ) + break; + } + } + + // Save adjustment to the track and request a redraw + mAdjuster.UpdateViews( false ); + return RefreshAll; + } + + HitTestPreview Preview( + const TrackPanelMouseState &state, const AudacityProject * ) override + { + static wxCursor resizeCursor{ wxCURSOR_SIZENS }; + return { + _("Click and drag to adjust sizes of sub-views."), + &resizeCursor + }; + } + + Result Release( + const TrackPanelMouseEvent &event, AudacityProject *pProject, + wxWindow *pParent) override + { + ProjectHistory::Get( *pProject ).ModifyState( false ); + return RefreshCode::RefreshNone; + } + + Result Cancel( AudacityProject * ) override + { + mAdjuster.UpdateViews( true ); + return RefreshCode::RefreshAll; + } + +private: + + SubViewAdjuster mAdjuster; + + // An index into mAdjuster.mPermutation + size_t mMySubView{}; + + wxCoord mYMin{}, mYMax{}; + wxCoord mTotalHeight{}; + wxCoord mOrigHeight{}; + wxCoord mOrigY{}; + + // Whether we drag the top or the bottom of the sub-view + bool mTop{}; +}; + +} + std::pair< bool, // if true, hit-testing is finished std::vector @@ -36,8 +369,20 @@ std::pair< const std::shared_ptr &wt, CommonTrackView &view) { - return WaveTrackView::DoDetailedHitTest( - state, pProject, currentTool, bMultiTool, wt, view); + auto results = WaveTrackView::DoDetailedHitTest( + state, pProject, currentTool, bMultiTool, wt, view ); + if ( results.first ) + return results; + + auto pWaveTrackView = mwWaveTrackView.lock(); + if ( pWaveTrackView && !state.state.HasModifiers() ) { + auto pHandle = SubViewAdjustHandle::HitTest( + *pWaveTrackView, *this, state ); + if (pHandle) + results.second.push_back( pHandle ); + } + + return results; } WaveTrackView &WaveTrackView::Get( WaveTrack &track )