mirror of
https://github.com/cookiengineer/audacity
synced 2025-06-15 15:49:36 +02:00
... Preserving existing behavior, but maybe these cases should be reconsidered and made more uniform. (e.g. should hit test on NoteTrack sometimes miss, as with WaveTrack?) Also made details of WaveTrack hit testing consistent with what ClipMenus does
1343 lines
41 KiB
C++
1343 lines
41 KiB
C++
/**********************************************************************
|
|
|
|
Audacity: A Digital Audio Editor
|
|
|
|
WaveTrackView.cpp
|
|
|
|
Paul Licameli split from TrackPanel.cpp
|
|
|
|
**********************************************************************/
|
|
|
|
#include "WaveTrackView.h"
|
|
|
|
#include "CutlineHandle.h"
|
|
|
|
#include "../../../../Experimental.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"
|
|
|
|
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 );
|
|
|
|
// Update cache for locations, e.g. cutlines and merge points
|
|
track->UpdateLocationsCache();
|
|
|
|
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);
|
|
}
|
|
}
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
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);
|
|
}
|
|
}
|
|
|
|
void ClipParameters::DrawClipEdges( wxDC &dc, const wxRect &rect ) const
|
|
{
|
|
// Draw clip edges
|
|
dc.SetPen(*wxGREY_PEN);
|
|
if (tpre < 0) {
|
|
AColor::Line(dc,
|
|
mid.x - 1, mid.y,
|
|
mid.x - 1, mid.y + rect.height);
|
|
}
|
|
if (tpost > t1) {
|
|
AColor::Line(dc,
|
|
mid.x + mid.width, mid.y,
|
|
mid.x + mid.width, mid.y + rect.height);
|
|
}
|
|
}
|
|
|
|
void WaveTrackView::Reparent( const std::shared_ptr<Track> &parent )
|
|
{
|
|
// BuildSubViews(); // not really needed
|
|
CommonTrackView::Reparent( parent );
|
|
WaveTrackSubViews::ForEach( [&parent](WaveTrackSubView &subView){
|
|
subView.Reparent( parent );
|
|
} );
|
|
}
|
|
|
|
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();
|
|
}
|
|
|
|
// Force creation always:
|
|
WaveformSettings &settings = static_cast< WaveTrack* >( pTrack.get() )
|
|
->GetIndependentWaveformSettings();
|
|
|
|
// Set the default scale type to linear or log, even if we are showing
|
|
// spectrogram
|
|
settings.scaleType = TracksPrefs::WaveformScaleChoice();
|
|
|
|
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 );
|
|
}
|
|
|
|
class WaveTrackShifter final : public TrackShifter {
|
|
public:
|
|
WaveTrackShifter( WaveTrack &track )
|
|
: mpTrack{ track.SharedPointer<WaveTrack>() }
|
|
{
|
|
InitIntervals();
|
|
}
|
|
~WaveTrackShifter() override {}
|
|
Track &GetTrack() const override { return *mpTrack; }
|
|
|
|
HitTestResult HitTest( double time ) override
|
|
{
|
|
auto pClip = mpTrack->GetClipAtTime( time );
|
|
|
|
if (!pClip)
|
|
return HitTestResult::Miss;
|
|
|
|
// Make a side-effect on our intervals
|
|
UnfixIntervals( [&](const auto &interval){
|
|
return
|
|
static_cast<WaveTrack::IntervalData*>(interval.Extra())
|
|
->GetClip().get() == pClip;
|
|
} );
|
|
|
|
return HitTestResult::Intervals;
|
|
}
|
|
|
|
private:
|
|
std::shared_ptr<WaveTrack> mpTrack;
|
|
};
|
|
|
|
using MakeWaveTrackShifter = MakeTrackShifter::Override<WaveTrack>;
|
|
template<> template<> auto MakeWaveTrackShifter::Implementation() -> Function {
|
|
return [](WaveTrack &track) {
|
|
return std::make_unique<WaveTrackShifter>(track);
|
|
};
|
|
}
|
|
static MakeWaveTrackShifter registerMakeWaveTrackShifter;
|