1
0
mirror of https://github.com/cookiengineer/audacity synced 2025-05-08 07:42:39 +02:00
2021-07-16 05:56:27 -04:00

1320 lines
41 KiB
C++

/**********************************************************************
Audacity: A Digital Audio Editor
WaveTrackView.cpp
Paul Licameli split from TrackPanel.cpp
**********************************************************************/
#include "WaveTrackView.h"
#include <unordered_set>
#include "CutlineHandle.h"
#include <numeric>
#include <wx/dc.h>
#include <wx/graphics.h>
#include "../../../../AColor.h"
#include "../../../../WaveClip.h"
#include "../../../../WaveTrack.h"
#include "../../../../../images/Cursors.h"
#include "../../../../AllThemeResources.h"
#include "../../../../HitTestResult.h"
#include "../../../../ProjectHistory.h"
#include "../../../../RefreshCode.h"
#include "../../../../TrackArtist.h"
#include "../../../../TrackPanelDrawingContext.h"
#include "../../../../TrackPanelMouseEvent.h"
#include "../../../../ViewInfo.h"
#include "../../../../prefs/TracksPrefs.h"
#include "../../../ui/TimeShiftHandle.h"
#include "../../../ui/ButtonHandle.h"
#include "../../../../TrackInfo.h"
#include "WaveTrackAffordanceControls.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;
}
size_t NVisible() const
{ return mPermutation.size() - mFirstSubView; }
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;
}
size_t FindIndex( WaveTrackSubView &subView ) const
{
const auto begin = mPermutation.begin(), end = mPermutation.end();
auto iter = std::find_if( begin, end, [&](size_t ii){
return mSubViews[ ii ].get() == &subView;
} );
return iter - begin;
}
std::pair< size_t, bool >
HitTest( WaveTrackSubView &subView,
wxCoord yy, wxCoord top, wxCoord height )
{
const auto index = FindIndex( subView );
auto size = mPermutation.size();
if ( index < (int)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 < (int)size - 1 || mFirstSubView > 0 ) )
return { index, false }; // bottom hit
}
return { size, false }; // not hit
}
std::vector<wxCoord> ComputeHeights( wxCoord totalHeight )
{
// Compute integer-valued heights
float total = 0;
for (const auto index : mPermutation ) {
const auto &placement = mOrigPlacements[ index ];
total += std::max( 0.f, placement.fraction );
}
float partial = 0;
wxCoord lastCoord = 0;
std::vector<wxCoord> result;
for (const auto index : mPermutation ) {
const auto &placement = mOrigPlacements[ index ];
auto fraction = std::max( 0.f, placement.fraction );
wxCoord coord = ( (partial + fraction ) / total ) * totalHeight;
auto height = coord - lastCoord;
result.emplace_back( height );
mNewPlacements[ index ].fraction = height;
lastCoord = coord;
partial += fraction;
}
return result;
}
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( std::weak_ptr<UIHandle> &holder,
WaveTrackView &view,
WaveTrackSubView &subView,
const TrackPanelMouseState &state )
{
if ( !view.GetMultiView() )
return {};
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() ) {
UIHandlePtr result = std::make_shared< SubViewAdjustHandle >(
std::move( adjuster ), index, view.GetLastHeight(), hit.second
);
result = AssignUIHandlePtr( holder, result );
return result;
}
else
return {};
}
SubViewAdjustHandle(
SubViewAdjuster &&adjuster, size_t subViewIndex,
wxCoord viewHeight, bool top )
: mAdjuster{ std::move( adjuster ) }
, mMySubView{ subViewIndex }
, mViewHeight{ viewHeight }
, mTop{ top }
{
if ( mAdjuster.ModifyPermutation( top ) )
--mMySubView;
}
Result Click(
const TrackPanelMouseEvent &event, AudacityProject *pProject ) override
{
using namespace RefreshCode;
const auto &permutation = mAdjuster.mPermutation;
const auto size = permutation.size();
if ( mMySubView >= size )
return Cancelled;
if (event.event.LeftDClick()) {
for ( auto &placement : mAdjuster.mNewPlacements ) {
if ( placement.index >= 0 )
placement.fraction = 1.0f;
else
placement.fraction = 0.0f;
}
mAdjuster.UpdateViews( false );
ProjectHistory::Get( *pProject ).ModifyState( false );
// Do not start a drag
return Cancelled | RefreshAll;
}
const auto &rect = event.rect;
const auto height = rect.GetHeight();
mOrigHeight = height;
mOrigHeights = mAdjuster.ComputeHeights( mViewHeight );
// Find the total height of the sub-views that may resize
mTotalHeight = 0;
auto index = ( mTop ? mAdjuster.mFirstSubView : mMySubView );
const auto end = ( mTop ? mMySubView + 1 : permutation.size() );
for (; index != end; ++index)
mTotalHeight += mOrigHeights[ index ];
wxASSERT( height == mOrigHeights[ mMySubView ] );
// 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;
const auto oldFraction = mOrigHeights[ ii ];
auto index = mAdjuster.mPermutation[ ii ];
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, AudacityProject * ) override
{
static auto resizeCursor =
::MakeCursor(wxCURSOR_ARROW, SubViewsCursorXpm, 16, 16);
return {
XO(
"Click and drag to adjust sizes of sub-views, double-click to split evenly"),
&*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;
std::vector<wxCoord> mOrigHeights;
// An index into mAdjuster.mPermutation
size_t mMySubView{};
wxCoord mYMin{}, mYMax{};
wxCoord mViewHeight{}; // Total height of all sub-views
wxCoord mTotalHeight{}; // Total height of adjusting sub-views only
wxCoord mOrigHeight{};
wxCoord mOrigY{};
// Whether we drag the top or the bottom of the sub-view
bool mTop{};
};
class SubViewRearrangeHandle : public UIHandle
{
public:
// Make it somewhat wider than the close button
enum { HotZoneWidth = 3 * kTrackInfoBtnSize / 2 };
static UIHandlePtr HitTest( std::weak_ptr<UIHandle> &holder,
WaveTrackView &view, WaveTrackSubView &subView,
const TrackPanelMouseState &state )
{
if ( !view.GetMultiView() )
return {};
SubViewAdjuster adjuster{ view };
if ( adjuster.NVisible() < 2 )
return {};
auto relX = state.state.GetX() - state.rect.GetLeft();
if ( relX >= HotZoneWidth )
return {};
auto index = adjuster.FindIndex( subView );
// Hit on the rearrange cursor only in the top and bottom thirds of
// sub-view height, leaving the rest free to hit the selection cursor
// first.
// And also exclude the top third of the topmost sub-view and bottom
// third of bottommost.
auto relY = state.state.GetY() - state.rect.GetTop();
auto height = state.rect.GetHeight();
bool hit =
( ( 3 * relY < height ) && index > 0 ) // top hit
||
( ( 3 * relY > 2 * height ) &&
index < adjuster.mPermutation.size() - 1 ) // bottom
;
if ( ! hit )
return {};
UIHandlePtr result = std::make_shared< SubViewRearrangeHandle >(
std::move( adjuster ),
index, view.GetLastHeight()
);
result = AssignUIHandlePtr( holder, result );
return result;
}
SubViewRearrangeHandle(
SubViewAdjuster &&adjuster, size_t subViewIndex,
wxCoord viewHeight )
: mAdjuster{ std::move( adjuster ) }
, mMySubView{ subViewIndex }
, mViewHeight{ viewHeight }
{
}
Result Click(
const TrackPanelMouseEvent &event, AudacityProject *pProject ) override
{
using namespace RefreshCode;
const auto &permutation = mAdjuster.mPermutation;
const auto size = permutation.size();
if ( mMySubView >= size )
return Cancelled;
mHeights = mAdjuster.ComputeHeights( mViewHeight );
// Find y coordinate of first sub-view
wxCoord heightAbove = 0;
for (auto index = mAdjuster.mFirstSubView;
index != mMySubView; ++index)
heightAbove += mHeights[ index ];
mTopY = event.rect.GetTop() - heightAbove;
return RefreshNone;
}
bool Clicked() const { return !mHeights.empty(); }
enum DragChoice_t{ Upward, Downward, Neutral };
DragChoice_t DragChoice( const TrackPanelMouseEvent &event ) const
{
// Disregard x coordinate -- so the mouse need not be in any sub-view,
// just in the correct range of y coordinates
auto yy = event.event.GetY();
auto coord = mTopY;
size_t ii = mAdjuster.mFirstSubView;
if ( yy < mTopY )
return ( mMySubView == ii ) ? Neutral : Upward;
for ( auto nn = mHeights.size(); ii < nn; ++ii ) {
const auto height = mHeights[ ii ];
coord += height;
if ( yy < coord )
break;
}
if ( ii < mMySubView ) {
if ( yy < coord - mHeights[ ii ] + mHeights[ mMySubView ] )
return Upward;
}
if ( ii > mMySubView ) {
if( mMySubView < mHeights.size() - 1 &&
yy >= coord - mHeights[ mMySubView ] )
return Downward;
}
return Neutral;
}
Result Drag( const TrackPanelMouseEvent &event, AudacityProject * ) override
{
using namespace RefreshCode;
auto pView = mAdjuster.mwView.lock();
if ( !pView )
return Cancelled;
switch( DragChoice( event ) ) {
case Upward:
{
std::swap( mHeights[ mMySubView ], mHeights[ mMySubView - 1 ] );
std::swap(
mAdjuster.mNewPlacements[ mMySubView ].index,
mAdjuster.mNewPlacements[ mMySubView - 1 ].index
);
--mMySubView;
break;
}
case Downward:
{
std::swap( mHeights[ mMySubView ], mHeights[ mMySubView + 1 ] );
std::swap(
mAdjuster.mNewPlacements[ mMySubView ].index,
mAdjuster.mNewPlacements[ mMySubView + 1 ].index
);
++mMySubView;
break;
}
default:
return RefreshNone;
}
// Save adjustment to the track and request a redraw
mAdjuster.UpdateViews( false );
return RefreshAll;
}
HitTestPreview Preview(
const TrackPanelMouseState &state, AudacityProject * ) override
{
static auto hoverCursor =
::MakeCursor(wxCURSOR_HAND, RearrangeCursorXpm, 16, 16);
static auto clickedCursor =
::MakeCursor(wxCURSOR_HAND, RearrangingCursorXpm, 16, 16);
return {
XO("Click and drag to rearrange sub-views"),
Clicked() ? &*clickedCursor : &*hoverCursor,
XO("Rearrange sub-views")
};
}
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;
std::vector<wxCoord> mHeights;
wxCoord mTopY;
// An index into mAdjuster.mPermutation
size_t mMySubView{};
wxCoord mViewHeight{}; // Total height of all sub-views
};
class SubViewCloseHandle : public ButtonHandle
{
static wxRect GetButtonRect( const wxRect &rect )
{
return {
rect.GetLeft(),
rect.GetTop(),
kTrackInfoBtnSize,
kTrackInfoBtnSize
};
}
public:
static UIHandlePtr HitTest( std::weak_ptr<UIHandle> &holder,
WaveTrackView &view, WaveTrackSubView &subView,
const TrackPanelMouseState &state )
{
SubViewAdjuster adjuster{ view };
if ( adjuster.NVisible() < 2 )
return {};
const auto rect = GetButtonRect( state.rect );
if ( !rect.Contains( state.state.GetPosition() ) )
return {};
auto index = adjuster.FindIndex( subView );
UIHandlePtr result = std::make_shared<SubViewCloseHandle>(
std::move( adjuster ), index, view.FindTrack(), rect );
result = AssignUIHandlePtr( holder, result );
return result;
}
SubViewCloseHandle(
SubViewAdjuster &&adjuster, size_t index,
const std::shared_ptr<Track> &pTrack, const wxRect &rect )
: ButtonHandle{ pTrack, rect }
, mAdjuster{ std::move( adjuster ) }
, mMySubView{ index }
{
}
Result CommitChanges(
const wxMouseEvent &event, AudacityProject *pProject, wxWindow *pParent)
override
{
ProjectHistory::Get( *pProject ).ModifyState( false );
auto &myPlacement =
mAdjuster.mNewPlacements[ mAdjuster.mPermutation[ mMySubView ] ];
myPlacement.fraction = 0;
mAdjuster.UpdateViews( false );
return RefreshCode::RefreshAll;
}
TranslatableString Tip(
const wxMouseState &state, AudacityProject &project) const override
{
return XO("Close sub-view");
}
// TrackPanelDrawable implementation
void Draw(
TrackPanelDrawingContext &context, const wxRect &rect, unsigned iPass )
override
{
if ( iPass == TrackArtist::PassMargins ) { // after PassTracks
TrackInfo::DrawCloseButton(
context, GetButtonRect(rect), GetTrack().get(), this );
}
}
private:
SubViewAdjuster mAdjuster;
size_t mMySubView{};
};
}
std::pair<
bool, // if true, hit-testing is finished
std::vector<UIHandlePtr>
> WaveTrackSubView::DoDetailedHitTest(
const TrackPanelMouseState &state,
const AudacityProject *pProject, int currentTool, bool bMultiTool,
const std::shared_ptr<WaveTrack> &wt)
{
auto results = WaveTrackView::DoDetailedHitTest(
state, pProject, currentTool, bMultiTool, wt, *this );
if ( results.first )
return results;
auto pWaveTrackView = mwWaveTrackView.lock();
if ( pWaveTrackView && !state.state.HasModifiers() ) {
if ( auto pHandle = SubViewCloseHandle::HitTest(
mCloseHandle,
*pWaveTrackView, *this, state ) )
results.second.push_back( pHandle );
if ( auto pHandle = SubViewAdjustHandle::HitTest(
mAdjustHandle,
*pWaveTrackView, *this, state ) )
results.second.push_back( pHandle );
if ( auto pHandle = SubViewRearrangeHandle::HitTest(
mRearrangeHandle,
*pWaveTrackView, *this, state ) )
results.second.push_back( pHandle );
}
if (auto result = CutlineHandle::HitTest(
mCutlineHandle, state.state, state.rect,
pProject, wt ))
// This overriding test applies in all tools
results.second.push_back(result);
return results;
}
void WaveTrackSubView::DrawBoldBoundaries(
TrackPanelDrawingContext &context, const WaveTrack *track,
const wxRect &rect )
{
auto &dc = context.dc;
const auto artist = TrackArtist::Get( context );
const auto &zoomInfo = *artist->pZoomInfo;
#ifdef EXPERIMENTAL_TRACK_PANEL_HIGHLIGHTING
auto target2 = dynamic_cast<CutlineHandle*>(context.target.get());
#endif
for (const auto loc : track->GetCachedLocations()) {
bool highlightLoc = false;
#ifdef EXPERIMENTAL_TRACK_PANEL_HIGHLIGHTING
highlightLoc =
target2 && target2->GetTrack().get() == track &&
target2->GetLocation() == loc;
#endif
const int xx = zoomInfo.TimeToPosition(loc.pos);
if (xx >= 0 && xx < rect.width) {
dc.SetPen( highlightLoc ? AColor::uglyPen : *wxGREY_PEN );
AColor::Line(dc, (int) (rect.x + xx - 1), rect.y, (int) (rect.x + xx - 1), rect.y + rect.height);
if (loc.typ == WaveTrackLocation::locationCutLine) {
dc.SetPen( highlightLoc ? AColor::uglyPen : *wxRED_PEN );
}
else {
#ifdef EXPERIMENTAL_DA
// JKC Black does not show up enough.
dc.SetPen(highlightLoc ? AColor::uglyPen : *wxWHITE_PEN);
#else
dc.SetPen(highlightLoc ? AColor::uglyPen : *wxBLACK_PEN);
#endif
}
AColor::Line(dc, (int) (rect.x + xx), rect.y, (int) (rect.x + xx), rect.y + rect.height);
dc.SetPen( highlightLoc ? AColor::uglyPen : *wxGREY_PEN );
AColor::Line(dc, (int) (rect.x + xx + 1), rect.y, (int) (rect.x + xx + 1), rect.y + rect.height);
}
}
}
std::weak_ptr<WaveTrackView> WaveTrackSubView::GetWaveTrackView() const
{
return mwWaveTrackView;
}
WaveTrackView &WaveTrackView::Get( WaveTrack &track )
{
return static_cast< WaveTrackView& >( TrackView::Get( track ) );
}
const WaveTrackView &WaveTrackView::Get( const WaveTrack &track )
{
return Get( const_cast<WaveTrack&>( track ) );
}
WaveTrackView::WaveTrackView( const std::shared_ptr<Track> &pTrack )
: CommonTrackView{ pTrack }
{
}
WaveTrackSubView::WaveTrackSubView( WaveTrackView &waveTrackView )
: CommonTrackView( waveTrackView.FindTrack() )
{
mwWaveTrackView = std::static_pointer_cast<WaveTrackView>(
waveTrackView.shared_from_this() );
}
WaveTrackView::~WaveTrackView()
{
}
void WaveTrackView::CopyTo( Track &track ) const
{
TrackView::CopyTo( track );
auto &other = TrackView::Get( track );
if ( const auto pOther = dynamic_cast< WaveTrackView* >( &other ) ) {
// only these fields are important to preserve in undo/redo history
pOther->RestorePlacements( SavePlacements() );
pOther->mMultiView = mMultiView;
}
}
std::vector<UIHandlePtr> WaveTrackView::DetailedHitTest
(const TrackPanelMouseState &st,
const AudacityProject *pProject, int currentTool, bool bMultiTool)
{
// should not come here any more, delegation to sub-view instead
wxASSERT( false );
return {};
}
std::pair< bool, std::vector<UIHandlePtr> >
WaveTrackView::DoDetailedHitTest
(const TrackPanelMouseState &st,
const AudacityProject *pProject, int currentTool, bool bMultiTool,
const std::shared_ptr<WaveTrack> &pTrack,
CommonTrackView &view)
{
// common hit-testing for different sub-view types, to help implement their
// DetailedHitTest()
// This is the only override of Track::DetailedHitTest that still
// depends on the state of the Tools toolbar.
// If that toolbar were eliminated, this could simplify to a sequence of
// hit test routines describable by a table.
UIHandlePtr result;
std::vector<UIHandlePtr> results;
if (bMultiTool && st.state.CmdDown()) {
// Ctrl modifier key in multi-tool overrides everything else
// (But this does not do the time shift constrained to the vertical only,
// which is what happens when you hold Ctrl in the Time Shift tool mode)
result = TimeShiftHandle::HitAnywhere(
view.mTimeShiftHandle, pTrack, false);
if (result)
results.push_back(result);
return { true, results };
}
return { false, results };
}
auto WaveTrackView::GetDisplays() const
-> std::vector< WaveTrackSubView::Type >
{
BuildSubViews();
// Collect the display types of visible views and sort them by position
using Pair = std::pair< int, WaveTrackSubView::Type >;
std::vector< Pair > pairs;
size_t ii = 0;
WaveTrackSubViews::ForEach( [&]( const WaveTrackSubView &subView ){
auto &placement = mPlacements[ii];
if ( placement.fraction > 0 )
pairs.emplace_back( placement.index, subView.SubViewType() );
++ii;
} );
std::sort( pairs.begin(), pairs.end() );
std::vector< WaveTrackSubView::Type > results;
for ( const auto &pair : pairs )
results.push_back( pair.second );
return results;
}
void WaveTrackView::SetDisplay(Display display, bool exclusive)
{
BuildSubViews();
DoSetDisplay( display, exclusive );
}
bool WaveTrackView::ToggleSubView(Display display)
{
size_t ii = 0;
size_t found = 0;
if ( WaveTrackSubViews::FindIf( [&]( const WaveTrackSubView &subView ) {
if ( subView.SubViewType().id == display ) {
found = ii;
return true;
}
++ii;
return false;
} ) ) {
auto &foundPlacement = mPlacements[found];
if ( foundPlacement.fraction > 0.0 ) {
// Toggle off
if (GetDisplays().size() < 2)
// refuse to do it
return false;
auto index = foundPlacement.index;
foundPlacement = { -1, 0.0 };
if (index >= 0) {
for ( auto &placement : mPlacements ) {
if ( placement.index > index )
--placement.index;
}
}
return true;
}
else {
// Toggle on
float total = 0;
int greatest = -1;
unsigned nn = 0;
for ( const auto &placement : mPlacements ) {
if ( placement.fraction > 0.0 && placement.index >= 0 ) {
total += placement.fraction;
greatest = std::max( greatest, placement.index );
++nn;
}
}
// Turn on the sub-view, putting it lowest, and with average of the
// heights of the other sub-views
foundPlacement = { greatest + 1, total / nn };
return true;
}
}
else
// unknown sub-view
return false;
}
// If exclusive, make the chosen view take up all the height. Else,
// partition equally, putting the specified view on top.
// Be sure the sequence in which the other views appear is determinate.
void WaveTrackView::DoSetDisplay(Display display, bool exclusive)
{
// Some generality here anticipating more than two views.
// The order of sub-views in the array is not specified, so make it definite
// by sorting by the view type constants.
size_t ii = 0;
std::vector< std::pair< WaveTrackViewConstants::Display, size_t > > pairs;
WaveTrackSubViews::ForEach( [&pairs, &ii]( WaveTrackSubView &subView ){
pairs.push_back( { subView.SubViewType().id, ii++ } );
} );
std::sort( pairs.begin(), pairs.end() );
int jj = 1;
for ( const auto &pair : pairs ) {
auto &placement = mPlacements[ pair.second ];
if (pair.first == display) {
// 0 for first view
placement = { 0, 1.0 };
}
else if( exclusive )
// -1 for not displayed
placement = { -1, 0.0 };
else
// positions other than the first.
// (Note that the fractions in the placement don't need to be
// denominated to 1. Just make them all equal to get an equal
// partitioning of the sub-views.)
placement = { jj++, 1.0 };
}
}
auto WaveTrackView::GetSubViews( const wxRect &rect ) -> Refinement
{
BuildSubViews();
// Collect the visible views in the right sequence
struct Item {
int index; float fraction; std::shared_ptr< TrackView > pView;
};
std::vector< Item > items;
size_t ii = 0;
float total = 0;
WaveTrackSubViews::ForEach( [&]( WaveTrackSubView &subView ){
auto &placement = mPlacements[ii];
auto index = placement.index;
auto fraction = placement.fraction;
if ( index >= 0 && fraction > 0.0 )
total += fraction,
items.push_back( { index, fraction, subView.shared_from_this() } );
++ii;
} );
std::sort( items.begin(), items.end(), [](const Item &a, const Item &b){
return a.index < b.index;
} );
// Remove views we don't need
auto begin = items.begin(), end = items.end(),
newEnd = std::remove_if( begin, end,
[]( const Item &item ){ return !item.pView; } );
items.erase( newEnd, end );
// Assign coordinates, redenominating to the total height,
// storing integer values
Refinement results;
results.reserve( items.size() );
const auto top = rect.GetTop();
const auto height = rect.GetHeight();
float partial = 0;
wxCoord lastCoord = 0;
for ( const auto &item : items ) {
wxCoord newCoord = top + (partial / total) * height;
results.emplace_back( newCoord, item.pView );
partial += item.fraction;
}
// Cache for the use of sub-view dragging
mLastHeight = height;
return results;
}
std::vector< std::shared_ptr< WaveTrackSubView > >
WaveTrackView::GetAllSubViews()
{
BuildSubViews();
std::vector< std::shared_ptr< WaveTrackSubView > > results;
WaveTrackSubViews::ForEach( [&]( WaveTrackSubView &subView ){
results.push_back( std::static_pointer_cast<WaveTrackSubView>(
subView.shared_from_this() ) );
} );
return results;
}
std::shared_ptr<CommonTrackCell> WaveTrackView::DoGetAffordanceControls()
{
return std::make_shared<WaveTrackAffordanceControls>(FindTrack());
}
void WaveTrackView::DoSetMinimized( bool minimized )
{
BuildSubViews();
// May come here. Invoke also on sub-views.
TrackView::DoSetMinimized( minimized );
WaveTrackSubViews::ForEach( [minimized](WaveTrackSubView &subView){
subView.DoSetMinimized( minimized );
} );
}
using DoGetWaveTrackView = DoGetView::Override< WaveTrack >;
template<> template<> auto DoGetWaveTrackView::Implementation() -> Function {
return [](WaveTrack &track) {
return std::make_shared<WaveTrackView>( track.SharedPointer() );
};
}
static DoGetWaveTrackView registerDoGetWaveTrackView;
std::shared_ptr<TrackVRulerControls> WaveTrackView::DoGetVRulerControls()
{
// This should never be called because of delegation to the spectrum or
// waveform sub-view
wxASSERT( false );
return {};
}
#undef PROFILE_WAVEFORM
#ifdef PROFILE_WAVEFORM
#ifdef __WXMSW__
#include <time.h>
#else
#include <sys/time.h>
#endif
double gWaveformTimeTotal = 0;
int gWaveformTimeCount = 0;
namespace {
struct Profiler {
Profiler()
{
# ifdef __WXMSW__
_time64(&tv0);
# else
gettimeofday(&tv0, NULL);
# endif
}
~Profiler()
{
# ifdef __WXMSW__
_time64(&tv1);
double elapsed = _difftime64(tv1, tv0);
# else
gettimeofday(&tv1, NULL);
double elapsed =
(tv1.tv_sec + tv1.tv_usec*0.000001) -
(tv0.tv_sec + tv0.tv_usec*0.000001);
# endif
gWaveformTimeTotal += elapsed;
gWaveformTimeCount++;
wxPrintf(wxT("Avg waveform drawing time: %f\n"),
gWaveformTimeTotal / gWaveformTimeCount);
}
# ifdef __WXMSW__
__time64_t tv0, tv1;
#else
struct timeval tv0, tv1;
#endif
};
}
#endif
ClipParameters::ClipParameters
(bool spectrum, const WaveTrack *track, const WaveClip *clip, const wxRect &rect,
const SelectedRegion &selectedRegion, const ZoomInfo &zoomInfo)
{
tOffset = clip->GetOffset();
rate = clip->GetRate();
h = zoomInfo.PositionToTime(0, 0
, true
);
h1 = zoomInfo.PositionToTime(rect.width, 0
, true
);
double sel0 = selectedRegion.t0(); //left selection bound
double sel1 = selectedRegion.t1(); //right selection bound
//If the track isn't selected, make the selection empty
if (!track->GetSelected() &&
(spectrum || !track->IsSyncLockSelected())) { // PRL: why was there a difference for spectrum?
sel0 = sel1 = 0.0;
}
const double trackLen = clip->GetEndTime() - clip->GetStartTime();
tpre = h - tOffset; // offset corrected time of
// left edge of display
tpost = h1 - tOffset; // offset corrected time of
// right edge of display
const double sps = 1. / rate; //seconds-per-sample
// Determine whether we should show individual samples
// or draw circular points as well
averagePixelsPerSample = rect.width / (rate * (h1 - h));
showIndividualSamples = averagePixelsPerSample > 0.5;
// Calculate actual selection bounds so that t0 > 0 and t1 < the
// end of the track
t0 = (tpre >= 0.0 ? tpre : 0.0);
t1 = (tpost < trackLen - sps * .99 ? tpost : trackLen - sps * .99);
if (showIndividualSamples) {
// adjustment so that the last circular point doesn't appear
// to be hanging off the end
t1 += 2. / (averagePixelsPerSample * rate);
}
// Make sure t1 (the right bound) is greater than 0
if (t1 < 0.0) {
t1 = 0.0;
}
// Make sure t1 is greater than t0
if (t0 > t1) {
t0 = t1;
}
// Use the WaveTrack method to show what is selected and 'should' be copied, pasted etc.
ssel0 = std::max(sampleCount(0), spectrum
? sampleCount((sel0 - tOffset) * rate + .99) // PRL: why?
: track->TimeToLongSamples(sel0 - tOffset)
);
ssel1 = std::max(sampleCount(0), spectrum
? sampleCount((sel1 - tOffset) * rate + .99) // PRL: why?
: track->TimeToLongSamples(sel1 - tOffset)
);
//trim selection so that it only contains the actual samples
if (ssel0 != ssel1 && ssel1 > (sampleCount)(0.5 + trackLen * rate)) {
ssel1 = sampleCount( 0.5 + trackLen * rate );
}
// The variable "hiddenMid" will be the rectangle containing the
// actual waveform, as opposed to any blank area before
// or after the track, as it would appear without the fisheye.
hiddenMid = rect;
// If the left edge of the track is to the right of the left
// edge of the display, then there's some unused area to the
// left of the track. Reduce the "hiddenMid"
hiddenLeftOffset = 0;
if (tpre < 0) {
// Fix Bug #1296 caused by premature conversion to (int).
wxInt64 time64 = zoomInfo.TimeToPosition(tOffset, 0 , true);
if( time64 < 0 )
time64 = 0;
hiddenLeftOffset = (time64 < rect.width) ? (int)time64 : rect.width;
hiddenMid.x += hiddenLeftOffset;
hiddenMid.width -= hiddenLeftOffset;
}
// If the right edge of the track is to the left of the right
// edge of the display, then there's some unused area to the right
// of the track. Reduce the "hiddenMid" rect by the
// size of the blank area.
if (tpost > t1) {
wxInt64 time64 = zoomInfo.TimeToPosition(tOffset+t1, 0 , true);
if( time64 < 0 )
time64 = 0;
const int hiddenRightOffset = (time64 < rect.width) ? (int)time64 : rect.width;
hiddenMid.width = std::max(0, hiddenRightOffset - hiddenLeftOffset);
}
// The variable "mid" will be the rectangle containing the
// actual waveform, as distorted by the fisheye,
// as opposed to any blank area before or after the track.
mid = rect;
// If the left edge of the track is to the right of the left
// edge of the display, then there's some unused area to the
// left of the track. Reduce the "mid"
leftOffset = 0;
if (tpre < 0) {
wxInt64 time64 = zoomInfo.TimeToPosition(tOffset, 0 , false);
if( time64 < 0 )
time64 = 0;
leftOffset = (time64 < rect.width) ? (int)time64 : rect.width;
mid.x += leftOffset;
mid.width -= leftOffset;
}
// If the right edge of the track is to the left of the right
// edge of the display, then there's some unused area to the right
// of the track. Reduce the "mid" rect by the
// size of the blank area.
if (tpost > t1) {
wxInt64 time64 = zoomInfo.TimeToPosition(tOffset+t1, 0 , false);
if( time64 < 0 )
time64 = 0;
const int distortedRightOffset = (time64 < rect.width) ? (int)time64 : rect.width;
mid.width = std::max(0, distortedRightOffset - leftOffset);
}
}
wxRect ClipParameters::GetClipRect(const WaveClip& clip, const ZoomInfo& zoomInfo, const wxRect& viewRect, int clipOffsetX)
{
auto srs = 1. / static_cast<double>(clip.GetRate());
//to prevent overlap left and right most samples with frame border
auto margin = .25 * srs;
auto edgeLeft = static_cast<wxInt64>(viewRect.GetLeft());
auto edgeRight = static_cast<wxInt64>(viewRect.GetRight());
auto left = std::clamp(zoomInfo.TimeToPosition(clip.GetOffset() - margin, viewRect.x + clipOffsetX, true), edgeLeft, edgeRight);
auto right = std::clamp(zoomInfo.TimeToPosition(clip.GetEndTime() - srs + margin, viewRect.x + clipOffsetX, true), edgeLeft, edgeRight);
if (right - left > 0)
{
//after clamping we can expect that left and right
//are small enough to be put into int
return wxRect(static_cast<int>(left), viewRect.y, static_cast<int>(right - left), viewRect.height);
}
//off the screen
return wxRect();
}
void WaveTrackView::Reparent( const std::shared_ptr<Track> &parent )
{
// BuildSubViews(); // not really needed
CommonTrackView::Reparent( parent );
WaveTrackSubViews::ForEach( [&parent](WaveTrackSubView &subView){
subView.Reparent( parent );
} );
if (mpAffordanceCellControl)
mpAffordanceCellControl->Reparent(parent);
}
std::weak_ptr<WaveClip> WaveTrackView::GetSelectedClip()
{
if (auto affordance = std::dynamic_pointer_cast<WaveTrackAffordanceControls>(GetAffordanceControls()))
{
return affordance->GetSelectedClip();
}
return {};
}
void WaveTrackView::BuildSubViews() const
{
if ( WaveTrackSubViews::size() == 0) {
// On-demand steps that can't happen in the constructor
auto pThis = const_cast<WaveTrackView*>( this );
pThis->BuildAll();
bool minimized = GetMinimized();
pThis->WaveTrackSubViews::ForEach( [&]( WaveTrackSubView &subView ){
subView.DoSetMinimized( minimized );
} );
if ( pThis->mPlacements.empty() ) {
pThis->mPlacements.resize( WaveTrackSubViews::size() );
auto pTrack = pThis->FindTrack();
auto display = TracksPrefs::ViewModeChoice();
bool multi = (display == WaveTrackViewConstants::MultiView);
if ( multi ) {
pThis->SetMultiView( true );
display = WaveTrackSubViewType::Default();
}
pThis->DoSetDisplay( display, !multi );
}
}
}
void WaveTrackView::Draw(
TrackPanelDrawingContext &context,
const wxRect &rect, unsigned iPass )
{
// Should not come here, drawing is now delegated to sub-views
wxASSERT( false );
CommonTrackView::Draw( context, rect, iPass );
}