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:
parent
d26b55ee3c
commit
54e0df5660
@ -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 )
|
||||
|
Loading…
x
Reference in New Issue
Block a user