1
0
mirror of https://github.com/cookiengineer/audacity synced 2025-06-16 16:10:06 +02:00

Implement a dragging handle to adjust wave track sub-view heights

This commit is contained in:
Paul Licameli 2019-07-10 10:47:52 -04:00
parent d26b55ee3c
commit 54e0df5660

View File

@ -12,11 +12,15 @@ Paul Licameli split from TrackPanel.cpp
#include "../../../../Experimental.h"
#include <numeric>
#include <wx/graphics.h>
#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<WaveTrackView>( 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<WaveTrack>( 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<UIHandlePtr>
@ -36,8 +369,20 @@ std::pair<
const std::shared_ptr<WaveTrack> &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 )