1 /**********************************************************************
2 
3 Audacity: A Digital Audio Editor
4 
5 WaveTrackView.cpp
6 
7 Paul Licameli split from TrackPanel.cpp
8 
9 **********************************************************************/
10 
11 #include "WaveTrackView.h"
12 
13 #include <unordered_set>
14 
15 #include "CutlineHandle.h"
16 
17 #include <numeric>
18 #include <wx/dc.h>
19 #include <wx/graphics.h>
20 
21 #include "AColor.h"
22 #include "../../../../WaveClip.h"
23 #include "../../../../WaveTrack.h"
24 
25 #include "../../../../../images/Cursors.h"
26 #include "AllThemeResources.h"
27 
28 #include "../../../../commands/CommandContext.h"
29 #include "../../../../HitTestResult.h"
30 #include "../../../../ProjectHistory.h"
31 #include "../../../../RefreshCode.h"
32 #include "../../../../TrackArtist.h"
33 #include "../../../../TrackPanel.h"
34 #include "../../../../TrackPanelAx.h"
35 #include "../../../../TrackPanelDrawingContext.h"
36 #include "../../../../TrackPanelMouseEvent.h"
37 #include "../../../../TrackPanelResizeHandle.h"
38 #include "ViewInfo.h"
39 #include "../../../../prefs/TracksPrefs.h"
40 
41 #include "../../../ui/TimeShiftHandle.h"
42 #include "../../../ui/ButtonHandle.h"
43 #include "../../../../TrackInfo.h"
44 
45 #include "../WaveTrackUtils.h"
46 
47 #include "WaveTrackAffordanceControls.h"
48 #include "WaveTrackAffordanceHandle.h"
49 #include "WaveClipTrimHandle.h"
50 
51 namespace {
52 
53 constexpr int kClipDetailedViewMinimumWidth{ 3 };
54 
55 using WaveTrackSubViewPtrs = std::vector< std::shared_ptr< WaveTrackSubView > >;
56 
57 // Structure that collects and modifies information on sub-view positions
58 // Written with great generality, allowing any number of sub-views
59 struct SubViewAdjuster
60 {
61    enum { HotZoneSize = 5 }; // so many pixels at top and bottom of each subview
62 
SubViewAdjuster__anondcb6d5860111::SubViewAdjuster63    SubViewAdjuster( WaveTrackView &view )
64       : mwView{
65          std::static_pointer_cast<WaveTrackView>( view.shared_from_this() ) }
66    {
67       mSubViews = view.GetAllSubViews();
68       mOrigPlacements = mNewPlacements = view.SavePlacements();
69       FindPermutation();
70    }
71 
FindPermutation__anondcb6d5860111::SubViewAdjuster72    void FindPermutation()
73    {
74       // Find a certain sort of the sub-views
75       auto size = mOrigPlacements.size();
76       wxASSERT( mSubViews.size() == size );
77       mPermutation.resize( size );
78       const auto begin = mPermutation.begin(), end = mPermutation.end();
79       std::iota( begin, end, 0 );
80       static auto invisible = []( const WaveTrackSubViewPlacement &placement ){
81          return placement.index < 0 || placement.fraction <= 0;
82       };
83       const auto comp = [this]( size_t ii, size_t jj ){
84          auto &pi = mOrigPlacements[ii];
85          bool iInvisible = invisible( pi );
86 
87          auto &pj = mOrigPlacements[jj];
88          bool jInvisible = invisible( pj );
89 
90          // Sort the invisibles to the front, rest by index
91          if ( iInvisible != jInvisible )
92             return iInvisible;
93          else if ( !iInvisible )
94             return pi.index < pj.index;
95          else
96             // Minor sort among the invisible views by their type
97             return mSubViews[ii]->SubViewType() < mSubViews[jj]->SubViewType();
98       };
99       std::sort( begin, end, comp );
100       // Find the start of visible sub-views
101       auto first = std::find_if( begin, end, [this](size_t ii){
102          return !invisible( mOrigPlacements[ii] );
103       } );
104       mFirstSubView = first - begin;
105    }
106 
NVisible__anondcb6d5860111::SubViewAdjuster107    size_t NVisible() const
108    { return mPermutation.size() - mFirstSubView; }
109 
ModifyPermutation__anondcb6d5860111::SubViewAdjuster110    bool ModifyPermutation( bool top )
111    {
112       bool rotated = false;
113       const auto pBegin = mPermutation.begin(), pEnd = mPermutation.end();
114       auto pFirst = pBegin + mFirstSubView;
115       if ( mFirstSubView > 0 ) {
116          // In case of dragging the top edge of the topmost view, or the
117          // bottom edge of the bottommost, decide which of the invisible
118          // views can become visible, and reassign the sequence.
119          // For definiteness, that choice depends on the subview type numbers;
120          // see the sorting criteria above.
121          --mFirstSubView;
122          --pFirst;
123          if ( top ) {
124             // If you drag down the top, the greatest-numbered invisible
125             // subview type will appear there.
126             mNewPlacements[ *pFirst ].fraction = 0;
127          }
128          else {
129             // If you drag up the bottom, let the least-numbered invisible
130             // subview type appear there.
131             mNewPlacements[ *pBegin ].fraction = 0;
132             std::rotate( pBegin, pBegin + 1, pEnd );
133             rotated = true;
134          }
135       }
136       // Reassign index numbers to all sub-views and 0 fraction to invisibles
137       for ( auto pIter = pBegin; pIter != pFirst; ++pIter ) {
138          auto &placement = mNewPlacements[ *pIter ];
139          placement.index = -1;
140          placement.fraction = 0;
141       }
142       size_t index = 0;
143       for ( auto pIter = pFirst; pIter != pEnd; ++pIter )
144          mNewPlacements[ *pIter ].index = index++;
145       return rotated;
146    }
147 
FindIndex__anondcb6d5860111::SubViewAdjuster148    size_t FindIndex( WaveTrackSubView &subView ) const
149    {
150       const auto begin = mPermutation.begin(), end = mPermutation.end();
151       auto iter = std::find_if( begin, end, [&](size_t ii){
152          return mSubViews[ ii ].get() == &subView;
153       } );
154       return iter - begin;
155    }
156 
157    std::pair< size_t, bool >
HitTest__anondcb6d5860111::SubViewAdjuster158    HitTest( WaveTrackSubView &subView,
159       wxCoord yy, wxCoord top, wxCoord height )
160    {
161       const auto index = FindIndex( subView );
162       auto size = mPermutation.size();
163       if ( index < (int)size ) {
164          yy -= top;
165          if ( yy >= 0 && yy < HotZoneSize && index > 0 )
166             return { index, true }; // top hit
167          if ( yy < height && yy >= height - HotZoneSize &&
168             // Have not yet called ModifyPermutation; dragging bottom of
169             // bottommost view allowed only if at least one view is invisible
170             ( index < (int)size - 1 || mFirstSubView > 0 ) )
171             return { index, false }; // bottom hit
172       }
173       return { size, false }; // not hit
174    }
175 
ComputeHeights__anondcb6d5860111::SubViewAdjuster176    std::vector<wxCoord> ComputeHeights( wxCoord totalHeight )
177    {
178       // Compute integer-valued heights
179       float total = 0;
180       for (const auto index : mPermutation ) {
181          const auto &placement = mOrigPlacements[ index ];
182          total += std::max( 0.f, placement.fraction );
183       }
184       float partial = 0;
185       wxCoord lastCoord = 0;
186       std::vector<wxCoord> result;
187       for (const auto index : mPermutation ) {
188          const auto &placement = mOrigPlacements[ index ];
189          auto fraction = std::max( 0.f, placement.fraction );
190          wxCoord coord = ( (partial + fraction ) / total ) * totalHeight;
191          auto height = coord - lastCoord;
192          result.emplace_back( height );
193          mNewPlacements[ index ].fraction = height;
194          lastCoord = coord;
195          partial += fraction;
196       }
197       return result;
198    }
199 
UpdateViews__anondcb6d5860111::SubViewAdjuster200    void UpdateViews( bool rollback )
201    {
202       auto pView = mwView.lock();
203       if ( pView ) {
204          auto pTrack = static_cast< WaveTrack* >( pView->FindTrack().get() );
205          for ( auto pChannel : TrackList::Channels<WaveTrack>( pTrack ) )
206             WaveTrackView::Get( *pChannel ).RestorePlacements(
207                rollback ? mOrigPlacements : mNewPlacements );
208       }
209    }
210 
211    std::weak_ptr< WaveTrackView > mwView;
212    WaveTrackSubViewPtrs mSubViews;
213    WaveTrackSubViewPlacements mOrigPlacements, mNewPlacements;
214    // Array mapping ordinal into the placement and subview arrays
215    std::vector< size_t > mPermutation;
216    // index into mPermutation
217    size_t mFirstSubView{};
218 };
219 
220 class SubViewAdjustHandle : public UIHandle
221 {
222 public:
223    enum { MinHeight = SubViewAdjuster::HotZoneSize };
224 
HitTest(std::weak_ptr<SubViewAdjustHandle> & holder,WaveTrackView & view,WaveTrackSubView & subView,const TrackPanelMouseState & state)225    static UIHandlePtr HitTest( std::weak_ptr<SubViewAdjustHandle> &holder,
226       WaveTrackView &view,
227       WaveTrackSubView &subView,
228       const TrackPanelMouseState &state )
229    {
230       if ( !view.GetMultiView() )
231          return {};
232 
233       SubViewAdjuster adjuster{ view };
234       auto hit = adjuster.HitTest( subView,
235          state.state.GetY(), state.rect.GetTop(), state.rect.GetHeight() );
236       auto index = hit.first;
237 
238       if ( index < adjuster.mPermutation.size() ) {
239          auto result = std::make_shared< SubViewAdjustHandle >(
240             std::move( adjuster ), index, view.GetLastHeight(), hit.second
241          );
242          result = AssignUIHandlePtr( holder, result );
243          return result;
244       }
245       else
246          return {};
247    }
248 
SubViewAdjustHandle(SubViewAdjuster && adjuster,size_t subViewIndex,wxCoord viewHeight,bool top)249    SubViewAdjustHandle(
250       SubViewAdjuster &&adjuster, size_t subViewIndex,
251          wxCoord viewHeight, bool top )
252       : mAdjuster{ std::move( adjuster ) }
253       , mMySubView{ subViewIndex }
254       , mViewHeight{ viewHeight }
255       , mTop{ top }
256    {
257       if ( mAdjuster.ModifyPermutation( top ) )
258          --mMySubView;
259    }
260 
Click(const TrackPanelMouseEvent & event,AudacityProject * pProject)261    Result Click(
262       const TrackPanelMouseEvent &event, AudacityProject *pProject ) override
263    {
264       using namespace RefreshCode;
265       const auto &permutation = mAdjuster.mPermutation;
266       const auto size = permutation.size();
267       if ( mMySubView >= size )
268          return Cancelled;
269 
270       if (event.event.LeftDClick()) {
271          for ( auto &placement : mAdjuster.mNewPlacements ) {
272             if ( placement.index >= 0 )
273                placement.fraction = 1.0f;
274             else
275                placement.fraction = 0.0f;
276          }
277          mAdjuster.UpdateViews( false );
278          ProjectHistory::Get( *pProject ).ModifyState( false );
279 
280          // Do not start a drag
281          return Cancelled | RefreshAll;
282       }
283 
284       const auto &rect = event.rect;
285       const auto height = rect.GetHeight();
286       mOrigHeight = height;
287 
288       mOrigHeights = mAdjuster.ComputeHeights( mViewHeight );
289 
290       // Find the total height of the sub-views that may resize
291       mTotalHeight = 0;
292       auto index = ( mTop ? mAdjuster.mFirstSubView : mMySubView );
293       const auto end = ( mTop ?  mMySubView + 1 : permutation.size() );
294       for (; index != end; ++index)
295          mTotalHeight += mOrigHeights[ index ];
296 
297       wxASSERT( height == mOrigHeights[ mMySubView ] );
298 
299       // Compute the maximum and minimum Y coordinates for drag effect
300       if ( mTop ) {
301          mOrigY = rect.GetTop();
302          mYMax = rect.GetBottom();
303          mYMin = mYMax - mTotalHeight + 1;
304       }
305       else {
306          mOrigY = rect.GetBottom();
307          mYMin = rect.GetTop();
308          mYMax = mYMin + mTotalHeight - 1;
309       }
310 
311       return RefreshNone;
312    }
313 
Drag(const TrackPanelMouseEvent & event,AudacityProject *)314    Result Drag( const TrackPanelMouseEvent &event, AudacityProject * ) override
315    {
316       using namespace RefreshCode;
317       auto pView = mAdjuster.mwView.lock();
318       if ( !pView )
319          return Cancelled;
320 
321       // Find new height for the dragged sub-view
322       auto newY = std::max( mYMin, std::min( mYMax, event.event.GetY() ) );
323       const auto delta = newY - mOrigY;
324       wxCoord newHeight = mTop
325          ? mOrigHeight - delta
326          : mOrigHeight + delta
327       ;
328       wxASSERT( newHeight >= 0 && newHeight <= mTotalHeight );
329       if ( newHeight < MinHeight )
330          // Snap the dragged sub-view to nothing
331          newHeight = 0;
332 
333       // Reassign height for the dragged sub-view
334       auto &myPlacement =
335          mAdjuster.mNewPlacements[ mAdjuster.mPermutation[ mMySubView ] ];
336       myPlacement.fraction = newHeight;
337 
338       // Grow or shrink other sub-views
339       auto excess = newHeight - mOrigHeight; // maybe negative
340       const auto adjustHeight = [&](size_t ii) {
341          if (excess == 0)
342             return true;
343 
344          const auto oldFraction = mOrigHeights[ ii ];
345 
346          auto index = mAdjuster.mPermutation[ ii ];
347          auto &placement = mAdjuster.mNewPlacements[ index ];
348          auto &fraction = placement.fraction;
349 
350          if (excess > oldFraction) {
351             excess -= oldFraction, fraction = 0;
352             return false;
353          }
354          else {
355             auto newFraction = oldFraction - excess;
356             if ( newFraction < MinHeight ) {
357                // This snaps very short sub-views to nothing
358                myPlacement.fraction += newFraction;
359                fraction = 0;
360             }
361             else
362                fraction = newFraction;
363             return true;
364          }
365       };
366       if ( mTop ) {
367          for ( size_t ii = mMySubView; ii > 0; ) {
368             --ii;
369             if ( adjustHeight( ii ) )
370                break;
371          }
372       }
373       else {
374          for ( size_t ii = mMySubView + 1, size = mAdjuster.mPermutation.size();
375             ii < size; ++ii
376          ) {
377             if ( adjustHeight( ii ) )
378                break;
379          }
380       }
381 
382       // Save adjustment to the track and request a redraw
383       mAdjuster.UpdateViews( false );
384       return RefreshAll;
385    }
386 
Preview(const TrackPanelMouseState & state,AudacityProject *)387    HitTestPreview Preview(
388       const TrackPanelMouseState &state, AudacityProject * ) override
389    {
390       static auto resizeCursor =
391          ::MakeCursor(wxCURSOR_ARROW, SubViewsCursorXpm, 16, 16);
392       return {
393          XO(
394 "Click and drag to adjust sizes of sub-views, double-click to split evenly"),
395          &*resizeCursor
396       };
397    }
398 
Release(const TrackPanelMouseEvent & event,AudacityProject * pProject,wxWindow * pParent)399    Result Release(
400       const TrackPanelMouseEvent &event, AudacityProject *pProject,
401       wxWindow *pParent) override
402    {
403       ProjectHistory::Get( *pProject ).ModifyState( false );
404       return RefreshCode::RefreshNone;
405    }
406 
Cancel(AudacityProject *)407    Result Cancel( AudacityProject * ) override
408    {
409       mAdjuster.UpdateViews( true );
410       return RefreshCode::RefreshAll;
411    }
412 
413 private:
414 
415    SubViewAdjuster mAdjuster;
416    std::vector<wxCoord> mOrigHeights;
417 
418    // An index into mAdjuster.mPermutation
419    size_t mMySubView{};
420 
421    wxCoord mYMin{}, mYMax{};
422    wxCoord mViewHeight{}; // Total height of all sub-views
423    wxCoord mTotalHeight{}; // Total height of adjusting sub-views only
424    wxCoord mOrigHeight{};
425    wxCoord mOrigY{};
426 
427    // Whether we drag the top or the bottom of the sub-view
428    bool mTop{};
429 };
430 
431 class SubViewRearrangeHandle : public UIHandle
432 {
433 public:
434    // Make it somewhat wider than the close button
435    enum { HotZoneWidth = 3 * kTrackInfoBtnSize / 2 };
436 
HitTest(std::weak_ptr<SubViewRearrangeHandle> & holder,WaveTrackView & view,WaveTrackSubView & subView,const TrackPanelMouseState & state)437    static UIHandlePtr HitTest(  std::weak_ptr<SubViewRearrangeHandle> &holder,
438       WaveTrackView &view, WaveTrackSubView &subView,
439       const TrackPanelMouseState &state )
440    {
441       if ( !view.GetMultiView() )
442          return {};
443 
444       SubViewAdjuster adjuster{ view };
445       if ( adjuster.NVisible() < 2 )
446          return {};
447 
448       auto relX = state.state.GetX() - state.rect.GetLeft();
449       if ( relX >= HotZoneWidth )
450          return {};
451 
452       auto index = adjuster.FindIndex( subView );
453 
454       // Hit on the rearrange cursor only in the top and bottom thirds of
455       // sub-view height, leaving the rest free to hit the selection cursor
456       // first.
457       // And also exclude the top third of the topmost sub-view and bottom
458       // third of bottommost.
459       auto relY = state.state.GetY() - state.rect.GetTop();
460       auto height = state.rect.GetHeight();
461       bool hit =
462          ( ( 3 * relY < height ) && index > 0 ) // top hit
463       ||
464          ( ( 3 * relY > 2 * height ) &&
465            index < adjuster.mPermutation.size() - 1 ) // bottom
466       ;
467       if ( ! hit )
468          return {};
469 
470       auto result = std::make_shared< SubViewRearrangeHandle >(
471          std::move( adjuster ),
472          index, view.GetLastHeight()
473       );
474       result = AssignUIHandlePtr( holder, result );
475       return result;
476    }
477 
SubViewRearrangeHandle(SubViewAdjuster && adjuster,size_t subViewIndex,wxCoord viewHeight)478    SubViewRearrangeHandle(
479       SubViewAdjuster &&adjuster, size_t subViewIndex,
480          wxCoord viewHeight )
481       : mAdjuster{ std::move( adjuster ) }
482       , mMySubView{ subViewIndex }
483       , mViewHeight{ viewHeight }
484    {
485    }
486 
Click(const TrackPanelMouseEvent & event,AudacityProject * pProject)487    Result Click(
488       const TrackPanelMouseEvent &event, AudacityProject *pProject ) override
489    {
490       using namespace RefreshCode;
491       const auto &permutation = mAdjuster.mPermutation;
492       const auto size = permutation.size();
493       if ( mMySubView >= size )
494          return Cancelled;
495 
496       mHeights = mAdjuster.ComputeHeights( mViewHeight );
497 
498       // Find y coordinate of first sub-view
499       wxCoord heightAbove = 0;
500       for (auto index = mAdjuster.mFirstSubView;
501          index != mMySubView; ++index)
502          heightAbove += mHeights[ index ];
503       mTopY = event.rect.GetTop() - heightAbove;
504 
505       return RefreshNone;
506    }
507 
Clicked() const508    bool Clicked() const { return !mHeights.empty(); }
509 
510    enum DragChoice_t{ Upward, Downward, Neutral };
511 
DragChoice(const TrackPanelMouseEvent & event) const512    DragChoice_t DragChoice( const TrackPanelMouseEvent &event ) const
513    {
514       // Disregard x coordinate -- so the mouse need not be in any sub-view,
515       // just in the correct range of y coordinates
516       auto yy = event.event.GetY();
517       auto coord = mTopY;
518       size_t ii = mAdjuster.mFirstSubView;
519       if ( yy < mTopY )
520          return ( mMySubView == ii ) ? Neutral : Upward;
521 
522       for ( auto nn = mHeights.size(); ii < nn; ++ii ) {
523          const auto height = mHeights[ ii ];
524          coord += height;
525          if ( yy < coord )
526             break;
527       }
528 
529       if ( ii < mMySubView ) {
530          if ( yy < coord - mHeights[ ii ] + mHeights[ mMySubView ] )
531             return Upward;
532       }
533 
534       if ( ii > mMySubView ) {
535          if( mMySubView < mHeights.size() - 1 &&
536             yy >= coord - mHeights[ mMySubView ] )
537          return Downward;
538       }
539 
540       return Neutral;
541    }
542 
Drag(const TrackPanelMouseEvent & event,AudacityProject *)543    Result Drag( const TrackPanelMouseEvent &event, AudacityProject * ) override
544    {
545       using namespace RefreshCode;
546       auto pView = mAdjuster.mwView.lock();
547       if ( !pView )
548          return Cancelled;
549 
550       switch( DragChoice( event ) ) {
551          case Upward:
552          {
553             std::swap( mHeights[ mMySubView ], mHeights[ mMySubView - 1 ] );
554             std::swap(
555                mAdjuster.mNewPlacements[ mMySubView ].index,
556                mAdjuster.mNewPlacements[ mMySubView - 1 ].index
557             );
558             --mMySubView;
559             break;
560          }
561          case Downward:
562          {
563             std::swap( mHeights[ mMySubView ], mHeights[ mMySubView + 1 ] );
564             std::swap(
565                mAdjuster.mNewPlacements[ mMySubView ].index,
566                mAdjuster.mNewPlacements[ mMySubView + 1 ].index
567             );
568             ++mMySubView;
569             break;
570          }
571          default:
572             return RefreshNone;
573       }
574 
575       // Save adjustment to the track and request a redraw
576       mAdjuster.UpdateViews( false );
577       return RefreshAll;
578    }
579 
Preview(const TrackPanelMouseState & state,AudacityProject *)580    HitTestPreview Preview(
581       const TrackPanelMouseState &state, AudacityProject * ) override
582    {
583       static auto hoverCursor =
584          ::MakeCursor(wxCURSOR_HAND, RearrangeCursorXpm, 16, 16);
585       static auto clickedCursor =
586          ::MakeCursor(wxCURSOR_HAND, RearrangingCursorXpm, 16, 16);
587       return {
588          XO("Click and drag to rearrange sub-views"),
589          Clicked() ? &*clickedCursor : &*hoverCursor,
590          XO("Rearrange sub-views")
591       };
592    }
593 
Release(const TrackPanelMouseEvent & event,AudacityProject * pProject,wxWindow * pParent)594    Result Release(
595       const TrackPanelMouseEvent &event, AudacityProject *pProject,
596       wxWindow *pParent) override
597    {
598       ProjectHistory::Get( *pProject ).ModifyState( false );
599       return RefreshCode::RefreshNone;
600    }
601 
Cancel(AudacityProject *)602    Result Cancel( AudacityProject * ) override
603    {
604       mAdjuster.UpdateViews( true );
605       return RefreshCode::RefreshAll;
606    }
607 
608 private:
609 
610    SubViewAdjuster mAdjuster;
611    std::vector<wxCoord> mHeights;
612    wxCoord mTopY;
613 
614    // An index into mAdjuster.mPermutation
615    size_t mMySubView{};
616 
617    wxCoord mViewHeight{}; // Total height of all sub-views
618 };
619 
620 class SubViewCloseHandle : public ButtonHandle
621 {
GetButtonRect(const wxRect & rect)622    static wxRect GetButtonRect( const wxRect &rect )
623    {
624       return {
625          rect.GetLeft(),
626          rect.GetTop(),
627          kTrackInfoBtnSize,
628          kTrackInfoBtnSize
629       };
630    }
631 
632 public:
HitTest(std::weak_ptr<SubViewCloseHandle> & holder,WaveTrackView & view,WaveTrackSubView & subView,const TrackPanelMouseState & state)633    static UIHandlePtr HitTest( std::weak_ptr<SubViewCloseHandle> &holder,
634       WaveTrackView &view, WaveTrackSubView &subView,
635       const TrackPanelMouseState &state )
636    {
637       SubViewAdjuster adjuster{ view };
638       if ( adjuster.NVisible() < 2 )
639          return {};
640 
641       const auto rect = GetButtonRect( state.rect );
642       if ( !rect.Contains( state.state.GetPosition() ) )
643          return {};
644       auto index = adjuster.FindIndex( subView );
645       auto result = std::make_shared<SubViewCloseHandle>(
646          std::move( adjuster ), index, view.FindTrack(), rect );
647       result = AssignUIHandlePtr( holder, result );
648       return result;
649    }
650 
SubViewCloseHandle(SubViewAdjuster && adjuster,size_t index,const std::shared_ptr<Track> & pTrack,const wxRect & rect)651    SubViewCloseHandle(
652       SubViewAdjuster &&adjuster, size_t index,
653       const std::shared_ptr<Track> &pTrack, const wxRect &rect )
654       : ButtonHandle{ pTrack, rect }
655       , mAdjuster{ std::move( adjuster ) }
656       , mMySubView{ index }
657    {
658    }
659 
CommitChanges(const wxMouseEvent & event,AudacityProject * pProject,wxWindow * pParent)660    Result CommitChanges(
661       const wxMouseEvent &event, AudacityProject *pProject, wxWindow *pParent)
662       override
663    {
664       ProjectHistory::Get( *pProject ).ModifyState( false );
665       auto &myPlacement =
666          mAdjuster.mNewPlacements[ mAdjuster.mPermutation[ mMySubView ] ];
667       myPlacement.fraction = 0;
668       mAdjuster.UpdateViews( false );
669       return RefreshCode::RefreshAll;
670    }
671 
Tip(const wxMouseState & state,AudacityProject & project) const672    TranslatableString Tip(
673       const wxMouseState &state, AudacityProject &project) const override
674    {
675       return XO("Close sub-view");
676    }
677 
678    // TrackPanelDrawable implementation
Draw(TrackPanelDrawingContext & context,const wxRect & rect,unsigned iPass)679    void Draw(
680       TrackPanelDrawingContext &context, const wxRect &rect, unsigned iPass )
681       override
682    {
683       if ( iPass == TrackArtist::PassMargins ) { // after PassTracks
684           TrackInfo::DrawCloseButton(
685              context, GetButtonRect(rect), GetTrack().get(), this );
686       }
687    }
688 
689 private:
690    SubViewAdjuster mAdjuster;
691    size_t mMySubView{};
692 };
693 
694 }
695 
696 std::pair<
697    bool, // if true, hit-testing is finished
698    std::vector<UIHandlePtr>
DoDetailedHitTest(const TrackPanelMouseState & state,const AudacityProject * pProject,int currentTool,bool bMultiTool,const std::shared_ptr<WaveTrack> & wt)699 > WaveTrackSubView::DoDetailedHitTest(
700    const TrackPanelMouseState &state,
701    const AudacityProject *pProject, int currentTool, bool bMultiTool,
702    const std::shared_ptr<WaveTrack> &wt)
703 {
704    auto results = WaveTrackView::DoDetailedHitTest(
705       state, pProject, currentTool, bMultiTool, wt, *this );
706    if ( results.first )
707       return results;
708 
709    auto pWaveTrackView = mwWaveTrackView.lock();
710    if ( pWaveTrackView && !state.state.HasModifiers() ) {
711       if ( auto pHandle = SubViewCloseHandle::HitTest(
712          mCloseHandle,
713          *pWaveTrackView, *this, state ) )
714          results.second.push_back( pHandle );
715 
716       auto channels = TrackList::Channels(wt.get());
717       if(channels.size() > 1) {
718          // Only one cell is tested and we need to know
719          // which one and it's relative location to the border.
720          auto subviews = pWaveTrackView->GetSubViews();
721          auto currentSubview = std::find_if(subviews.begin(), subviews.end(),
722             [self = shared_from_this()](const auto& p){
723                return self == p.second;
724          });
725          if (currentSubview != subviews.end())
726          {
727             auto currentSubviewIndex = std::distance(subviews.begin(), currentSubview);
728 
729             const auto py = state.state.GetY();
730             const auto topBorderHit = std::abs(py - state.rect.GetTop())
731                <= WaveTrackView::kChannelSeparatorThickness / 2;
732             const auto bottomBorderHit = std::abs(py - state.rect.GetBottom())
733                <= WaveTrackView::kChannelSeparatorThickness / 2;
734 
735             auto currentChannel = channels.find(wt.get());
736             auto currentChannelIndex = std::distance(channels.begin(), currentChannel);
737 
738             if (//for not-last-view check the bottom border hit
739                ((currentChannelIndex != channels.size() - 1)
740                   && (currentSubviewIndex == static_cast<int>(subviews.size()) - 1)
741                   && bottomBorderHit)
742                ||
743                //or for not-first-view check the top border hit
744                ((currentChannelIndex != 0) && currentSubviewIndex == 0 && topBorderHit))
745             {
746                //depending on which border hit test succeeded on we
747                //need to choose a proper target for resizing
748                auto it = bottomBorderHit ? currentChannel : currentChannel.advance(-1);
749                auto result = std::make_shared<TrackPanelResizeHandle>((*it)->shared_from_this(), py);
750                result = AssignUIHandlePtr(mResizeHandle, result);
751                results.second.push_back(result);
752             }
753          }
754       }
755 
756       if ( auto pHandle = SubViewAdjustHandle::HitTest(
757          mAdjustHandle,
758          *pWaveTrackView, *this, state ) )
759          results.second.push_back( pHandle );
760       if ( auto pHandle = SubViewRearrangeHandle::HitTest(
761          mRearrangeHandle,
762          *pWaveTrackView, *this, state ) )
763          results.second.push_back( pHandle );
764       if (auto pHandle = WaveClipTrimHandle::HitTest(
765           mClipTrimHandle,
766           *pWaveTrackView, pProject, state))
767           results.second.push_back(pHandle);
768    }
769    if (auto result = CutlineHandle::HitTest(
770       mCutlineHandle, state.state, state.rect,
771       pProject, wt ))
772       // This overriding test applies in all tools
773       results.second.push_back(result);
774 
775    return results;
776 }
777 
778 
DrawBoldBoundaries(TrackPanelDrawingContext & context,const WaveTrack * track,const wxRect & rect)779 void WaveTrackSubView::DrawBoldBoundaries(
780    TrackPanelDrawingContext &context, const WaveTrack *track,
781    const wxRect &rect )
782 {
783    auto &dc = context.dc;
784    const auto artist = TrackArtist::Get( context );
785 
786    const auto &zoomInfo = *artist->pZoomInfo;
787 
788 #ifdef EXPERIMENTAL_TRACK_PANEL_HIGHLIGHTING
789    auto target2 = dynamic_cast<CutlineHandle*>(context.target.get());
790 #endif
791    for (const auto loc : track->GetCachedLocations()) {
792       bool highlightLoc = false;
793 #ifdef EXPERIMENTAL_TRACK_PANEL_HIGHLIGHTING
794       highlightLoc =
795          target2 && target2->GetTrack().get() == track &&
796          target2->GetLocation() == loc;
797 #endif
798       const int xx = zoomInfo.TimeToPosition(loc.pos);
799       if (xx >= 0 && xx < rect.width) {
800          dc.SetPen( highlightLoc ? AColor::uglyPen : *wxGREY_PEN );
801          AColor::Line(dc, (int) (rect.x + xx - 1), rect.y, (int) (rect.x + xx - 1), rect.y + rect.height);
802          if (loc.typ == WaveTrackLocation::locationCutLine) {
803             dc.SetPen( highlightLoc ? AColor::uglyPen : *wxRED_PEN );
804          }
805          else {
806 #ifdef EXPERIMENTAL_DA
807             // JKC Black does not show up enough.
808             dc.SetPen(highlightLoc ? AColor::uglyPen : *wxWHITE_PEN);
809 #else
810             dc.SetPen(highlightLoc ? AColor::uglyPen : *wxBLACK_PEN);
811 #endif
812          }
813          AColor::Line(dc, (int) (rect.x + xx), rect.y, (int) (rect.x + xx), rect.y + rect.height);
814          dc.SetPen( highlightLoc ? AColor::uglyPen : *wxGREY_PEN );
815          AColor::Line(dc, (int) (rect.x + xx + 1), rect.y, (int) (rect.x + xx + 1), rect.y + rect.height);
816       }
817    }
818 }
819 
GetWaveTrackView() const820 std::weak_ptr<WaveTrackView> WaveTrackSubView::GetWaveTrackView() const
821 {
822    return mwWaveTrackView;
823 }
824 
GetMenuItems(const wxRect & rect,const wxPoint * pPosition,AudacityProject * pProject)825 auto WaveTrackSubView::GetMenuItems(
826    const wxRect &rect, const wxPoint *pPosition, AudacityProject *pProject )
827       -> std::vector<MenuItem>
828 {
829    const WaveClip *pClip = nullptr;
830    auto pTrack = static_cast<WaveTrack*>( FindTrack().get() );
831    double time = 0.0;
832    if ( pTrack && pPosition ) {
833       auto &viewInfo = ViewInfo::Get(*pProject);
834       time = viewInfo.PositionToTime( pPosition->x, rect.x );
835       pClip = pTrack->GetClipAtTime( time );
836    }
837 
838    if (pClip)
839       return {
840          { L"Cut", XO("Cut") },
841          { L"Copy", XO("Copy") },
842          { L"Paste", XO("Paste")  },
843          {},
844          { L"Split", XO("Split Clip") },
845          { L"TrackMute", XO("Mute/Unmute Track") },
846          {},
847          { L"RenameClip", XO("Rename clip...") },
848       };
849    else
850       return {
851          { L"Paste", XO("Paste")  },
852          {},
853          { L"TrackMute", XO("Mute/Unmute Track") },
854       };
855 }
856 
Get(WaveTrack & track)857 WaveTrackView &WaveTrackView::Get( WaveTrack &track )
858 {
859    return static_cast< WaveTrackView& >( TrackView::Get( track ) );
860 }
861 
Get(const WaveTrack & track)862 const WaveTrackView &WaveTrackView::Get( const WaveTrack &track )
863 {
864    return Get( const_cast<WaveTrack&>( track ) );
865 }
866 
WaveTrackView(const std::shared_ptr<Track> & pTrack)867 WaveTrackView::WaveTrackView( const std::shared_ptr<Track> &pTrack )
868    : CommonTrackView{ pTrack }
869 {
870 }
871 
WaveTrackSubView(WaveTrackView & waveTrackView)872 WaveTrackSubView::WaveTrackSubView( WaveTrackView &waveTrackView )
873    : CommonTrackView( waveTrackView.FindTrack() )
874 {
875    mwWaveTrackView = std::static_pointer_cast<WaveTrackView>(
876       waveTrackView.shared_from_this() );
877 }
878 
CopyToSubView(WaveTrackSubView * destSubView) const879 void WaveTrackSubView::CopyToSubView(WaveTrackSubView *destSubView) const {
880 
881 }
882 
~WaveTrackView()883 WaveTrackView::~WaveTrackView()
884 {
885 }
886 
CopyTo(Track & track) const887 void WaveTrackView::CopyTo( Track &track ) const
888 {
889    TrackView::CopyTo( track );
890    auto &other = TrackView::Get( track );
891 
892    if ( const auto pOther = dynamic_cast< WaveTrackView* >( &other ) ) {
893       // only these fields are important to preserve in undo/redo history
894       pOther->RestorePlacements( SavePlacements() );
895       pOther->mMultiView = mMultiView;
896 
897       auto srcSubViewsPtrs  = const_cast<WaveTrackView*>( this )->GetAllSubViews();
898       auto destSubViewsPtrs  = const_cast<WaveTrackView*>( pOther )->GetAllSubViews();
899       wxASSERT(srcSubViewsPtrs.size() == destSubViewsPtrs.size());
900 
901       for(auto i = 0; i != srcSubViewsPtrs.size(); i++){
902          srcSubViewsPtrs[i]->CopyToSubView(destSubViewsPtrs[i].get());
903       }
904    }
905 }
906 
DetailedHitTest(const TrackPanelMouseState & st,const AudacityProject * pProject,int currentTool,bool bMultiTool)907 std::vector<UIHandlePtr> WaveTrackView::DetailedHitTest
908 (const TrackPanelMouseState &st,
909  const AudacityProject *pProject, int currentTool, bool bMultiTool)
910 {
911    // should not come here any more, delegation to sub-view instead
912    wxASSERT( false );
913    return {};
914 }
915 
916 std::pair< bool, std::vector<UIHandlePtr> >
DoDetailedHitTest(const TrackPanelMouseState & st,const AudacityProject * pProject,int currentTool,bool bMultiTool,const std::shared_ptr<WaveTrack> & pTrack,CommonTrackView & view)917 WaveTrackView::DoDetailedHitTest
918 (const TrackPanelMouseState &st,
919  const AudacityProject *pProject, int currentTool, bool bMultiTool,
920  const std::shared_ptr<WaveTrack> &pTrack,
921  CommonTrackView &view)
922 {
923    // common hit-testing for different sub-view types, to help implement their
924    // DetailedHitTest()
925 
926    // This is the only override of Track::DetailedHitTest that still
927    // depends on the state of the Tools toolbar.
928    // If that toolbar were eliminated, this could simplify to a sequence of
929    // hit test routines describable by a table.
930 
931    std::vector<UIHandlePtr> results;
932 
933    const auto& viewInfo = ViewInfo::Get(*pProject);
934 
935    for (auto& clip : pTrack->GetClips())
936    {
937       if (!WaveTrackView::ClipDetailsVisible(*clip, viewInfo, st.rect)
938          && HitTest(*clip, viewInfo, st.rect, st.state.GetPosition()))
939       {
940          auto waveTrackView = std::static_pointer_cast<WaveTrackView>(pTrack->GetTrackView());
941          results.push_back(
942             AssignUIHandlePtr(
943                waveTrackView->mAffordanceHandle,
944                std::make_shared<WaveTrackAffordanceHandle>(pTrack, clip)
945             )
946          );
947       }
948    }
949 
950    if (bMultiTool && st.state.CmdDown()) {
951       // Ctrl modifier key in multi-tool overrides everything else
952       // (But this does not do the time shift constrained to the vertical only,
953       //  which is what happens when you hold Ctrl in the Time Shift tool mode)
954       auto result = TimeShiftHandle::HitAnywhere(
955          view.mTimeShiftHandle, pTrack, false);
956       if (result)
957          results.push_back(result);
958       return { true, results };
959    }
960 
961    return { false, results };
962 }
963 
GetDisplays() const964 auto WaveTrackView::GetDisplays() const
965    -> std::vector< WaveTrackSubView::Type >
966 {
967    BuildSubViews();
968 
969    // Collect the display types of visible views and sort them by position
970    using Pair = std::pair< int, WaveTrackSubView::Type >;
971    std::vector< Pair > pairs;
972    size_t ii = 0;
973    WaveTrackSubViews::ForEach( [&]( const WaveTrackSubView &subView ){
974       auto &placement = mPlacements[ii];
975       if ( placement.fraction > 0 )
976          pairs.emplace_back( placement.index, subView.SubViewType() );
977       ++ii;
978    } );
979    std::sort( pairs.begin(), pairs.end() );
980    std::vector< WaveTrackSubView::Type > results;
981    for ( const auto &pair : pairs )
982       results.push_back( pair.second );
983    return results;
984 }
985 
SetDisplay(Display display,bool exclusive)986 void WaveTrackView::SetDisplay(Display display, bool exclusive)
987 {
988    BuildSubViews();
989    DoSetDisplay( display, exclusive );
990 }
991 
ToggleSubView(Display display)992 bool WaveTrackView::ToggleSubView(Display display)
993 {
994    size_t ii = 0;
995    size_t found = 0;
996    if ( WaveTrackSubViews::FindIf( [&]( const WaveTrackSubView &subView ) {
997       if ( subView.SubViewType().id == display ) {
998          found = ii;
999          return true;
1000       }
1001       ++ii;
1002       return false;
1003    } ) ) {
1004       auto &foundPlacement = mPlacements[found];
1005       if ( foundPlacement.fraction > 0.0 ) {
1006          // Toggle off
1007 
1008          if (GetDisplays().size() < 2)
1009             // refuse to do it
1010             return false;
1011 
1012          auto index = foundPlacement.index;
1013          foundPlacement = { -1, 0.0 };
1014          if (index >= 0) {
1015             for ( auto &placement : mPlacements ) {
1016                if ( placement.index > index )
1017                   --placement.index;
1018             }
1019          }
1020 
1021          return true;
1022       }
1023       else {
1024          // Toggle on
1025          float total = 0;
1026          int greatest = -1;
1027          unsigned nn = 0;
1028          for ( const auto &placement : mPlacements ) {
1029             if ( placement.fraction > 0.0 && placement.index >= 0 ) {
1030                total += placement.fraction;
1031                greatest = std::max( greatest, placement.index );
1032                ++nn;
1033             }
1034          }
1035          // Turn on the sub-view, putting it lowest, and with average of the
1036          // heights of the other sub-views
1037          foundPlacement = { greatest + 1, total / nn };
1038 
1039          return true;
1040       }
1041    }
1042    else
1043       // unknown sub-view
1044       return false;
1045 }
1046 
1047 // If exclusive, make the chosen view take up all the height.  Else,
1048 // partition equally, putting the specified view on top.
1049 // Be sure the sequence in which the other views appear is determinate.
DoSetDisplay(Display display,bool exclusive)1050 void WaveTrackView::DoSetDisplay(Display display, bool exclusive)
1051 {
1052    // Some generality here anticipating more than two views.
1053    // The order of sub-views in the array is not specified, so make it definite
1054    // by sorting by the view type constants.
1055    size_t ii = 0;
1056    std::vector< std::pair< WaveTrackViewConstants::Display, size_t > > pairs;
1057    WaveTrackSubViews::ForEach( [&pairs, &ii]( WaveTrackSubView &subView ){
1058       pairs.push_back( { subView.SubViewType().id, ii++ } );
1059    } );
1060    std::sort( pairs.begin(), pairs.end() );
1061 
1062    int jj = 1;
1063    for ( const auto &pair : pairs ) {
1064       auto &placement = mPlacements[ pair.second ];
1065       if (pair.first == display) {
1066          // 0 for first view
1067          placement = { 0, 1.0 };
1068       }
1069       else if( exclusive )
1070          // -1 for not displayed
1071          placement = { -1, 0.0 };
1072       else
1073          // positions other than the first.
1074          // (Note that the fractions in the placement don't need to be
1075          // denominated to 1.  Just make them all equal to get an equal
1076          // partitioning of the sub-views.)
1077          placement = { jj++, 1.0 };
1078    }
1079 }
1080 
1081 namespace {
1082    template<typename Iter, typename Comp>
NextClipLooped(ViewInfo & viewInfo,Iter begin,Iter end,Comp comp)1083    const WaveClip* NextClipLooped(ViewInfo& viewInfo, Iter begin, Iter end, Comp comp)
1084    {
1085       auto it = WaveTrackUtils::SelectedClip(viewInfo, begin, end);
1086       if (it == end)
1087          it = std::find_if(begin, end, comp);
1088       else
1089          it = std::next(it);
1090 
1091       if (it == end)
1092          return *begin;
1093       return *it;
1094    }
1095 }
1096 
SelectNextClip(ViewInfo & viewInfo,AudacityProject * project,bool forward)1097 bool WaveTrackView::SelectNextClip(ViewInfo& viewInfo, AudacityProject* project, bool forward)
1098 {
1099    //Iterates through clips in a looped manner
1100    auto waveTrack = std::dynamic_pointer_cast<WaveTrack>(FindTrack());
1101    if (!waveTrack)
1102       return false;
1103    auto clips = waveTrack->SortedClipArray();
1104    if (clips.empty())
1105       return false;
1106 
1107    const WaveClip* clip{ };
1108    if (forward)
1109    {
1110       clip = NextClipLooped(viewInfo, clips.begin(), clips.end(), [&](const WaveClip* other) {
1111          return other->GetPlayStartTime() >= viewInfo.selectedRegion.t1();
1112       });
1113    }
1114    else
1115    {
1116       clip = NextClipLooped(viewInfo, clips.rbegin(), clips.rend(), [&](const WaveClip* other) {
1117          return other->GetPlayStartTime() <= viewInfo.selectedRegion.t0();
1118       });
1119    }
1120 
1121    viewInfo.selectedRegion.setTimes(clip->GetPlayStartTime(), clip->GetPlayEndTime());
1122    ProjectHistory::Get(*project).ModifyState(false);
1123 
1124    // create and send message to screen reader
1125    auto it = std::find(clips.begin(), clips.end(), clip);
1126    auto index = std::distance(clips.begin(), it);
1127 
1128    auto message = XP(
1129    /* i18n-hint:
1130        string is the name of a clip
1131        first number is the position of that clip in a sequence of clips,
1132        second number counts the clips */
1133        "%s, %d of %d clip",
1134        "%s, %d of %d clips",
1135        2
1136    )(
1137       clip->GetName(),
1138       static_cast<int>(index + 1),
1139       static_cast<int>(clips.size())
1140   );
1141 
1142    TrackFocus::Get(*project).MessageForScreenReader(message);
1143    return true;
1144 }
1145 
GetSubViews(const wxRect & rect)1146 auto WaveTrackView::GetSubViews( const wxRect &rect ) -> Refinement
1147 {
1148    return GetSubViews(&rect);
1149 }
1150 
GetSubViews(const wxRect * rect)1151 auto WaveTrackView::GetSubViews(const wxRect* rect) -> Refinement
1152 {
1153    BuildSubViews();
1154 
1155    // Collect the visible views in the right sequence
1156    struct Item {
1157       int index; float fraction; std::shared_ptr< TrackView > pView;
1158    };
1159    std::vector< Item > items;
1160    size_t ii = 0;
1161    float total = 0;
1162    WaveTrackSubViews::ForEach([&](WaveTrackSubView& subView) {
1163       auto& placement = mPlacements[ii];
1164       auto index = placement.index;
1165       auto fraction = placement.fraction;
1166       if (index >= 0 && fraction > 0.0)
1167          total += fraction,
1168          items.push_back({ index, fraction, subView.shared_from_this() });
1169       ++ii;
1170    });
1171    std::sort(items.begin(), items.end(), [](const Item& a, const Item& b) {
1172       return a.index < b.index;
1173    });
1174 
1175    // Remove views we don't need
1176    auto begin = items.begin(), end = items.end(),
1177         newEnd = std::remove_if(begin, end,
1178             [](const Item& item) { return !item.pView; });
1179    items.erase(newEnd, end);
1180 
1181    Refinement results;
1182 
1183    if (rect != nullptr)
1184    {
1185       // Assign coordinates, redenominating to the total height,
1186       // storing integer values
1187       results.reserve(items.size());
1188       const auto top = rect->GetTop();
1189       const auto height = rect->GetHeight();
1190       float partial = 0;
1191       wxCoord lastCoord = 0;
1192       for (const auto& item : items) {
1193          wxCoord newCoord = top + (partial / total) * height;
1194          results.emplace_back(newCoord, item.pView);
1195          partial += item.fraction;
1196       }
1197 
1198       // Cache for the use of sub-view dragging
1199       mLastHeight = height;
1200    }
1201    else
1202    {
1203       std::transform(items.begin(), items.end(), std::back_inserter(results), [](const auto& item) {
1204          return std::make_pair(0, item.pView);
1205       });
1206    }
1207 
1208    return results;
1209 }
1210 
1211 /*
1212  Note that the WaveTrackView isn't in the TrackPanel subdivision, but it is
1213  set sometimes as the focused cell, and therefore the following functions can
1214  be visited.  To visit their overrides in the sub-views and affordances,
1215  which are never focused, we must forward to them.  To do that properly, if
1216  any cell declines to handle the event by setting it as skipped, it must be
1217  set again to not-skipped before attempting the next call-through.
1218  */
CaptureKey(wxKeyEvent & event,ViewInfo & viewInfo,wxWindow * pParent,AudacityProject * project)1219 unsigned WaveTrackView::CaptureKey(wxKeyEvent& event, ViewInfo& viewInfo, wxWindow* pParent, AudacityProject* project)
1220 {
1221    unsigned result{ RefreshCode::RefreshNone };
1222    auto pTrack = static_cast<WaveTrack*>(FindTrack().get());
1223    for (auto pChannel : TrackList::Channels(pTrack)) {
1224       event.Skip(false);
1225       auto &waveTrackView = WaveTrackView::Get(*pChannel);
1226       // Give sub-views first chance to handle the event
1227       for (auto &subView : waveTrackView.GetSubViews()) {
1228          // Event defaults in skipped state which must be turned off explicitly
1229          wxASSERT(!event.GetSkipped());
1230          result |= subView.second->CaptureKey(event, viewInfo, pParent, project);
1231          if (!event.GetSkipped()) {
1232             // sub view wants it
1233             mKeyEventDelegate = subView.second;
1234             return result;
1235          }
1236          else
1237             event.Skip(false);
1238       }
1239 
1240       if (auto affordance = waveTrackView.GetAffordanceControls()) {
1241          result |= affordance->CaptureKey(event, viewInfo, pParent, project);
1242          if (!event.GetSkipped()) {
1243             mKeyEventDelegate = affordance;
1244             return result;
1245          }
1246       }
1247 
1248       event.Skip(false);
1249    }
1250    switch (event.GetKeyCode())
1251    {
1252    case WXK_TAB:
1253       break;
1254    default:
1255       result |= CommonTrackView::CaptureKey(
1256          event, viewInfo, pParent, project);
1257       break;
1258    };
1259    if (!event.GetSkipped()) {
1260       mKeyEventDelegate = shared_from_this();
1261    }
1262 
1263    return result;
1264 }
1265 
KeyDown(wxKeyEvent & event,ViewInfo & viewInfo,wxWindow * pParent,AudacityProject * project)1266 unsigned WaveTrackView::KeyDown(wxKeyEvent& event, ViewInfo& viewInfo, wxWindow* pParent, AudacityProject* project)
1267 {
1268    unsigned result{ RefreshCode::RefreshNone };
1269    if (auto delegate = mKeyEventDelegate.lock()) {
1270       if (auto pWaveTrackView = dynamic_cast<WaveTrackView*>(delegate.get()))
1271       {
1272          if (event.GetKeyCode() == WXK_TAB)
1273          {
1274             SelectNextClip(viewInfo, project, event.GetModifiers() != wxMOD_SHIFT);
1275             result |= RefreshCode::RefreshCell;
1276          }
1277          else
1278             result |= pWaveTrackView->CommonTrackView::KeyDown(
1279                event, viewInfo, pParent, project);
1280       }
1281       else
1282          result |= delegate->KeyDown(event, viewInfo, pParent, project);
1283    }
1284    else
1285       event.Skip();
1286 
1287    return result;
1288 }
1289 
Char(wxKeyEvent & event,ViewInfo & viewInfo,wxWindow * pParent,AudacityProject * project)1290 unsigned WaveTrackView::Char(wxKeyEvent& event, ViewInfo& viewInfo, wxWindow* pParent, AudacityProject* project)
1291 {
1292    unsigned result{ RefreshCode::RefreshNone };
1293    if (auto delegate = mKeyEventDelegate.lock()) {
1294       if (auto pWaveTrackView = dynamic_cast<WaveTrackView*>(delegate.get()))
1295          result |= pWaveTrackView->CommonTrackView::Char(
1296             event, viewInfo, pParent, project);
1297       else
1298          result |= delegate->Char(event, viewInfo, pParent, project);
1299    }
1300    else
1301       event.Skip();
1302 
1303    return result;
1304 }
1305 
LoseFocus(AudacityProject * project)1306 unsigned WaveTrackView::LoseFocus(AudacityProject *project)
1307 {
1308    unsigned result = RefreshCode::RefreshNone;
1309    if (auto delegate = mKeyEventDelegate.lock()) {
1310       if (auto waveTrackView = dynamic_cast<WaveTrackView*>(delegate.get()))
1311          result = waveTrackView->CommonTrackView::LoseFocus(project);
1312       else
1313          result = delegate->LoseFocus(project);
1314       mKeyEventDelegate.reset();
1315    }
1316    return result;
1317 }
1318 
CutSelectedText(AudacityProject & project)1319 bool WaveTrackView::CutSelectedText(AudacityProject& project)
1320 {
1321    for (auto channel : TrackList::Channels(FindTrack().get()))
1322    {
1323       auto& view = TrackView::Get(*channel);
1324       if (auto affordance
1325          = std::dynamic_pointer_cast<WaveTrackAffordanceControls>(view.GetAffordanceControls()))
1326       {
1327          if (affordance->OnTextCut(project))
1328             return true;
1329       }
1330    }
1331    return false;
1332 }
1333 
CopySelectedText(AudacityProject & project)1334 bool WaveTrackView::CopySelectedText(AudacityProject& project)
1335 {
1336    for (auto channel : TrackList::Channels(FindTrack().get()))
1337    {
1338       auto& view = TrackView::Get(*channel);
1339       if (auto affordance
1340          = std::dynamic_pointer_cast<WaveTrackAffordanceControls>(view.GetAffordanceControls()))
1341       {
1342          if (affordance->OnTextCopy(project))
1343             return true;
1344       }
1345    }
1346    return false;
1347 }
1348 
ClipDetailsVisible(const WaveClip & clip,const ZoomInfo & zoomInfo,const wxRect & viewRect)1349 bool WaveTrackView::ClipDetailsVisible(const WaveClip& clip, const ZoomInfo& zoomInfo, const wxRect& viewRect)
1350 {
1351    //Do not fold clips to line at sample zoom level, as
1352    //it may become impossible to 'unfold' it when clip is trimmed
1353    //to a single sample
1354    bool showSamples{ false };
1355    auto clipRect = ClipParameters::GetClipRect(clip, zoomInfo, viewRect, &showSamples);
1356    return showSamples || clipRect.width >= kClipDetailedViewMinimumWidth;
1357 }
1358 
ClipHitTestArea(const WaveClip & clip,const ZoomInfo & zoomInfo,const wxRect & viewRect)1359 wxRect WaveTrackView::ClipHitTestArea(const WaveClip& clip, const ZoomInfo& zoomInfo, const wxRect& viewRect)
1360 {
1361    bool showSamples{ false };
1362    auto clipRect = ClipParameters::GetClipRect(clip, zoomInfo, viewRect, &showSamples);
1363    if (showSamples || clipRect.width >= kClipDetailedViewMinimumWidth)
1364       return clipRect;
1365 
1366    return clipRect.Inflate(2, 0);
1367 }
1368 
HitTest(const WaveClip & clip,const ZoomInfo & viewInfo,const wxRect & viewRect,const wxPoint & pos)1369 bool WaveTrackView::HitTest(const WaveClip& clip, const ZoomInfo& viewInfo, const wxRect& viewRect, const wxPoint& pos)
1370 {
1371    return ClipHitTestArea(clip, viewInfo, viewRect).Contains(pos);
1372 }
1373 
PasteText(AudacityProject & project)1374 bool WaveTrackView::PasteText(AudacityProject& project)
1375 {
1376    for (auto channel : TrackList::Channels(FindTrack().get()))
1377    {
1378       auto& view = TrackView::Get(*channel);
1379       if (auto affordance
1380          = std::dynamic_pointer_cast<WaveTrackAffordanceControls>(view.GetAffordanceControls()))
1381       {
1382          if (affordance->OnTextPaste(project))
1383             return true;
1384       }
1385    }
1386    return false;
1387 }
1388 
SelectAllText(AudacityProject & project)1389 bool WaveTrackView::SelectAllText(AudacityProject& project)
1390 {
1391    for (auto channel : TrackList::Channels(FindTrack().get()))
1392    {
1393       auto& view = TrackView::Get(*channel);
1394       if (auto affordance
1395          = std::dynamic_pointer_cast<WaveTrackAffordanceControls>(view.GetAffordanceControls()))
1396       {
1397          if (affordance->OnTextSelect(project))
1398             return true;
1399       }
1400    }
1401    return false;
1402 }
1403 
1404 std::vector< std::shared_ptr< WaveTrackSubView > >
GetAllSubViews()1405 WaveTrackView::GetAllSubViews()
1406 {
1407    BuildSubViews();
1408 
1409    std::vector< std::shared_ptr< WaveTrackSubView > > results;
1410    WaveTrackSubViews::ForEach( [&]( WaveTrackSubView &subView ){
1411       results.push_back( std::static_pointer_cast<WaveTrackSubView>(
1412          subView.shared_from_this() ) );
1413    } );
1414    return results;
1415 }
1416 
GetAffordanceControls()1417 std::shared_ptr<CommonTrackCell> WaveTrackView::GetAffordanceControls()
1418 {
1419     auto track = FindTrack();
1420     if (!track->IsAlignedWithLeader())
1421     {
1422         return DoGetAffordance(track);
1423     }
1424     return {};
1425 }
1426 
DoSetMinimized(bool minimized)1427 void WaveTrackView::DoSetMinimized( bool minimized )
1428 {
1429    BuildSubViews();
1430 
1431    // May come here.  Invoke also on sub-views.
1432    TrackView::DoSetMinimized( minimized );
1433    WaveTrackSubViews::ForEach( [minimized](WaveTrackSubView &subView){
1434       subView.DoSetMinimized( minimized );
1435    } );
1436 }
1437 
DoGetAffordance(const std::shared_ptr<Track> & track)1438 std::shared_ptr<CommonTrackCell> WaveTrackView::DoGetAffordance(const std::shared_ptr<Track>& track)
1439 {
1440     if (mpAffordanceCellControl == nullptr)
1441         mpAffordanceCellControl = std::make_shared<WaveTrackAffordanceControls>(track);
1442     return mpAffordanceCellControl;
1443 }
1444 
1445 using DoGetWaveTrackView = DoGetView::Override< WaveTrack >;
DEFINE_ATTACHED_VIRTUAL_OVERRIDE(DoGetWaveTrackView)1446 DEFINE_ATTACHED_VIRTUAL_OVERRIDE(DoGetWaveTrackView) {
1447    return [](WaveTrack &track) {
1448       return std::make_shared<WaveTrackView>( track.SharedPointer() );
1449    };
1450 }
1451 
DoGetVRulerControls()1452 std::shared_ptr<TrackVRulerControls> WaveTrackView::DoGetVRulerControls()
1453 {
1454    // This should never be called because of delegation to the spectrum or
1455    // waveform sub-view
1456    wxASSERT( false );
1457    return {};
1458 }
1459 
1460 #undef PROFILE_WAVEFORM
1461 #ifdef PROFILE_WAVEFORM
1462 #ifdef __WXMSW__
1463 #include <time.h>
1464 #else
1465 #include <sys/time.h>
1466 #endif
1467 double gWaveformTimeTotal = 0;
1468 int gWaveformTimeCount = 0;
1469 
1470 namespace {
1471    struct Profiler {
Profiler__anondcb6d5861811::Profiler1472       Profiler()
1473       {
1474 #   ifdef __WXMSW__
1475          _time64(&tv0);
1476 #   else
1477          gettimeofday(&tv0, NULL);
1478 #   endif
1479       }
1480 
~Profiler__anondcb6d5861811::Profiler1481       ~Profiler()
1482       {
1483 #   ifdef __WXMSW__
1484          _time64(&tv1);
1485          double elapsed = _difftime64(tv1, tv0);
1486 #   else
1487          gettimeofday(&tv1, NULL);
1488          double elapsed =
1489          (tv1.tv_sec + tv1.tv_usec*0.000001) -
1490          (tv0.tv_sec + tv0.tv_usec*0.000001);
1491 #   endif
1492          gWaveformTimeTotal += elapsed;
1493          gWaveformTimeCount++;
1494          wxPrintf(wxT("Avg waveform drawing time: %f\n"),
1495                   gWaveformTimeTotal / gWaveformTimeCount);
1496       }
1497 
1498 #   ifdef __WXMSW__
1499       __time64_t tv0, tv1;
1500 #else
1501       struct timeval tv0, tv1;
1502 #endif
1503    };
1504 }
1505 #endif
1506 
1507 namespace
1508 {
1509    // Returns an offset in seconds to be applied to the right clip
1510    // boundary so that it does not overlap the last sample
CalculateAdjustmentForZoomLevel(const wxRect & viewRect,const ZoomInfo & zoomInfo,int rate,double & outAveragePPS,bool & outShowSamples)1511    double CalculateAdjustmentForZoomLevel(
1512       const wxRect& viewRect,
1513       const ZoomInfo& zoomInfo,
1514       int rate,
1515       double& outAveragePPS,
1516       //Is zoom level sufficient to show individual samples?
1517       bool& outShowSamples)
1518    {
1519       static constexpr double pixelsOffset{ 2 };//The desired offset in pixels
1520 
1521       auto h = zoomInfo.PositionToTime(0, 0, true);
1522       auto h1 = zoomInfo.PositionToTime(viewRect.width, 0, true);
1523 
1524       // Determine whether we should show individual samples
1525       // or draw circular points as well
1526       outAveragePPS = viewRect.width / (rate * (h1 - h));// pixels per sample
1527       outShowSamples = outAveragePPS > 0.5;
1528 
1529       if(outShowSamples)
1530          // adjustment so that the last circular point doesn't appear
1531          // to be hanging off the end
1532          return  pixelsOffset / (outAveragePPS * rate); // pixels / ( pixels / second ) = seconds
1533       return .0;
1534    }
1535 }
1536 
ClipParameters(bool spectrum,const WaveTrack * track,const WaveClip * clip,const wxRect & rect,const SelectedRegion & selectedRegion,const ZoomInfo & zoomInfo)1537 ClipParameters::ClipParameters
1538    (bool spectrum, const WaveTrack *track, const WaveClip *clip, const wxRect &rect,
1539    const SelectedRegion &selectedRegion, const ZoomInfo &zoomInfo)
1540 {
1541    tOffset = clip->GetPlayStartTime();
1542    rate = clip->GetRate();
1543 
1544    h = zoomInfo.PositionToTime(0, 0
1545       , true
1546    );
1547    h1 = zoomInfo.PositionToTime(rect.width, 0
1548       , true
1549    );
1550 
1551    double sel0 = selectedRegion.t0();    //left selection bound
1552    double sel1 = selectedRegion.t1();    //right selection bound
1553 
1554    //If the track isn't selected, make the selection empty
1555    if (!track->GetSelected() &&
1556       (spectrum || !track->IsSyncLockSelected())) { // PRL: why was there a difference for spectrum?
1557       sel0 = sel1 = 0.0;
1558    }
1559 
1560    const double trackLen = clip->GetPlayEndTime() - clip->GetPlayStartTime();
1561 
1562    tpre = h - tOffset;                 // offset corrected time of
1563    //  left edge of display
1564    tpost = h1 - tOffset;               // offset corrected time of
1565    //  right edge of display
1566 
1567    const double sps = 1. / rate;            //seconds-per-sample
1568 
1569    // Calculate actual selection bounds so that t0 > 0 and t1 < the
1570    // end of the track
1571    t0 = std::max(tpre, .0);
1572    t1 = std::min(tpost, trackLen - sps * .99)
1573       + CalculateAdjustmentForZoomLevel(rect, zoomInfo, rate, averagePixelsPerSample, showIndividualSamples);
1574 
1575    // Make sure t1 (the right bound) is greater than 0
1576    if (t1 < 0.0) {
1577       t1 = 0.0;
1578    }
1579 
1580    // Make sure t1 is greater than t0
1581    if (t0 > t1) {
1582       t0 = t1;
1583    }
1584 
1585    // Use the WaveTrack method to show what is selected and 'should' be copied, pasted etc.
1586    ssel0 = std::max(sampleCount(0), spectrum
1587       ? sampleCount((sel0 - tOffset) * rate + .99) // PRL: why?
1588       : track->TimeToLongSamples(sel0 - tOffset)
1589    );
1590    ssel1 = std::max(sampleCount(0), spectrum
1591       ? sampleCount((sel1 - tOffset) * rate + .99) // PRL: why?
1592       : track->TimeToLongSamples(sel1 - tOffset)
1593    );
1594 
1595    //trim selection so that it only contains the actual samples
1596    if (ssel0 != ssel1 && ssel1 > (sampleCount)(0.5 + trackLen * rate)) {
1597       ssel1 = sampleCount( 0.5 + trackLen * rate );
1598    }
1599 
1600    // The variable "hiddenMid" will be the rectangle containing the
1601    // actual waveform, as opposed to any blank area before
1602    // or after the track, as it would appear without the fisheye.
1603    hiddenMid = rect;
1604 
1605    // If the left edge of the track is to the right of the left
1606    // edge of the display, then there's some unused area to the
1607    // left of the track.  Reduce the "hiddenMid"
1608    hiddenLeftOffset = 0;
1609    if (tpre < 0) {
1610       // Fix Bug #1296 caused by premature conversion to (int).
1611       wxInt64 time64 = zoomInfo.TimeToPosition(tOffset, 0 , true);
1612       if( time64 < 0 )
1613          time64 = 0;
1614       hiddenLeftOffset = (time64 < rect.width) ? (int)time64 : rect.width;
1615 
1616       hiddenMid.x += hiddenLeftOffset;
1617       hiddenMid.width -= hiddenLeftOffset;
1618    }
1619 
1620    // If the right edge of the track is to the left of the right
1621    // edge of the display, then there's some unused area to the right
1622    // of the track.  Reduce the "hiddenMid" rect by the
1623    // size of the blank area.
1624    if (tpost > t1) {
1625       wxInt64 time64 = zoomInfo.TimeToPosition(tOffset+t1, 0 , true);
1626       if( time64 < 0 )
1627          time64 = 0;
1628       const int hiddenRightOffset = (time64 < rect.width) ? (int)time64 : rect.width;
1629 
1630       hiddenMid.width = std::max(0, hiddenRightOffset - hiddenLeftOffset);
1631    }
1632    // The variable "mid" will be the rectangle containing the
1633    // actual waveform, as distorted by the fisheye,
1634    // as opposed to any blank area before or after the track.
1635    mid = rect;
1636 
1637    // If the left edge of the track is to the right of the left
1638    // edge of the display, then there's some unused area to the
1639    // left of the track.  Reduce the "mid"
1640    leftOffset = 0;
1641    if (tpre < 0) {
1642       wxInt64 time64 = zoomInfo.TimeToPosition(tOffset, 0 , false);
1643       if( time64 < 0 )
1644          time64 = 0;
1645       leftOffset = (time64 < rect.width) ? (int)time64 : rect.width;
1646 
1647       mid.x += leftOffset;
1648       mid.width -= leftOffset;
1649    }
1650 
1651    // If the right edge of the track is to the left of the right
1652    // edge of the display, then there's some unused area to the right
1653    // of the track.  Reduce the "mid" rect by the
1654    // size of the blank area.
1655    if (tpost > t1) {
1656       wxInt64 time64 = zoomInfo.TimeToPosition(tOffset+t1, 0 , false);
1657       if( time64 < 0 )
1658          time64 = 0;
1659       const int distortedRightOffset = (time64 < rect.width) ? (int)time64 : rect.width;
1660 
1661       mid.width = std::max(0, distortedRightOffset - leftOffset);
1662    }
1663 }
1664 
GetClipRect(const WaveClip & clip,const ZoomInfo & zoomInfo,const wxRect & viewRect,bool * outShowSamples)1665 wxRect ClipParameters::GetClipRect(const WaveClip& clip, const ZoomInfo& zoomInfo, const wxRect& viewRect, bool* outShowSamples)
1666 {
1667     auto srs = 1. / static_cast<double>(clip.GetRate());
1668     double averagePixelsPerSample{};
1669     bool showIndividualSamples{};
1670     auto clipEndingAdjustemt
1671        = CalculateAdjustmentForZoomLevel(viewRect, zoomInfo, clip.GetRate(), averagePixelsPerSample, showIndividualSamples);
1672     if (outShowSamples != nullptr)
1673        *outShowSamples = showIndividualSamples;
1674     constexpr auto edgeLeft = static_cast<wxInt64>(std::numeric_limits<int>::min());
1675     constexpr auto edgeRight = static_cast<wxInt64>(std::numeric_limits<int>::max());
1676     auto left = std::clamp(
1677        zoomInfo.TimeToPosition(
1678           clip.GetPlayStartTime(), viewRect.x, true
1679        ), edgeLeft, edgeRight
1680     );
1681     auto right = std::clamp(
1682        zoomInfo.TimeToPosition(
1683           clip.GetPlayEndTime() - .99 * srs + clipEndingAdjustemt, viewRect.x, true
1684        ), edgeLeft, edgeRight
1685     );
1686     if (right >= left)
1687     {
1688         //after clamping we can expect that left and right
1689         //are small enough to be put into int
1690         return wxRect(
1691            static_cast<int>(left),
1692            viewRect.y,
1693            std::max(1, static_cast<int>(right - left)),
1694            viewRect.height
1695         );
1696     }
1697     return wxRect();
1698 }
1699 
Reparent(const std::shared_ptr<Track> & parent)1700 void WaveTrackView::Reparent( const std::shared_ptr<Track> &parent )
1701 {
1702    // BuildSubViews(); // not really needed
1703    CommonTrackView::Reparent( parent );
1704    WaveTrackSubViews::ForEach( [&parent](WaveTrackSubView &subView){
1705       subView.Reparent( parent );
1706    } );
1707    if (mpAffordanceCellControl)
1708       mpAffordanceCellControl->Reparent(parent);
1709 }
1710 
GetSelectedClip()1711 std::weak_ptr<WaveClip> WaveTrackView::GetSelectedClip()
1712 {
1713    if (auto affordance = std::dynamic_pointer_cast<WaveTrackAffordanceControls>(GetAffordanceControls()))
1714    {
1715       return affordance->GetSelectedClip();
1716    }
1717    return {};
1718 }
1719 
BuildSubViews() const1720 void WaveTrackView::BuildSubViews() const
1721 {
1722    if ( WaveTrackSubViews::size() == 0) {
1723       // On-demand steps that can't happen in the constructor
1724       auto pThis = const_cast<WaveTrackView*>( this );
1725       pThis->BuildAll();
1726       bool minimized = GetMinimized();
1727       pThis->WaveTrackSubViews::ForEach( [&]( WaveTrackSubView &subView ){
1728          subView.DoSetMinimized( minimized );
1729       } );
1730 
1731       if ( pThis->mPlacements.empty() ) {
1732          pThis->mPlacements.resize( WaveTrackSubViews::size() );
1733 
1734          auto pTrack = pThis->FindTrack();
1735          auto display = TracksPrefs::ViewModeChoice();
1736          bool multi = (display == WaveTrackViewConstants::MultiView);
1737          if ( multi ) {
1738             pThis->SetMultiView( true );
1739             display = WaveTrackSubViewType::Default();
1740          }
1741 
1742          pThis->DoSetDisplay( display, !multi );
1743       }
1744    }
1745 }
1746 
Draw(TrackPanelDrawingContext & context,const wxRect & rect,unsigned iPass)1747 void WaveTrackView::Draw(
1748    TrackPanelDrawingContext &context,
1749    const wxRect &rect, unsigned iPass )
1750 {
1751    // Should not come here, drawing is now delegated to sub-views
1752    wxASSERT( false );
1753 
1754    CommonTrackView::Draw( context, rect, iPass );
1755 }
1756