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