1 /**********************************************************************
2 
3   Audacity: A Digital Audio Editor
4 
5   TrackPanel.cpp
6 
7   Dominic Mazzoni
8   and lots of other contributors
9 
10   Implements TrackPanel.
11 
12 ********************************************************************//*!
13 
14 \file TrackPanel.cpp
15 \brief
16   Implements TrackPanel.
17 
18 *//***************************************************************//**
19 
20 \class TrackPanel
21 \brief
22   The TrackPanel class coordinates updates and operations on the
23   main part of the screen which contains multiple tracks.
24 
25   It uses many other classes, but in particular it uses the
26   TrackInfo class to draw the controls area on the left of a track,
27   and the TrackArtist class to draw the actual waveforms.
28 
29   Note that in some of the older code here,
30   "Label" means the TrackInfo plus the vertical ruler.
31   Confusing relative to LabelTrack labels.
32 
33   The TrackPanel manages multiple tracks and their TrackInfos.
34 
35   Note that with stereo tracks there will be one TrackInfo
36   being used by two wavetracks.
37 
38 *//*****************************************************************//**
39 
40 \class TrackPanel::AudacityTimer
41 \brief Timer class dedicated to informing the TrackPanel that it
42 is time to refresh some aspect of the screen.
43 
44 *//*****************************************************************/
45 
46 
47 #include "TrackPanel.h"
48 #include "TrackPanelConstants.h"
49 
50 #include <wx/app.h>
51 #include <wx/setup.h> // for wxUSE_* macros
52 
53 #include "AdornedRulerPanel.h"
54 #include "tracks/ui/CommonTrackPanelCell.h"
55 #include "KeyboardCapture.h"
56 #include "Project.h"
57 #include "ProjectAudioIO.h"
58 #include "ProjectAudioManager.h"
59 #include "ProjectHistory.h"
60 #include "ProjectWindows.h"
61 #include "ProjectSettings.h"
62 #include "ProjectStatus.h"
63 #include "ProjectWindow.h"
64 #include "Theme.h"
65 #include "TrackPanelMouseEvent.h"
66 #include "TrackPanelResizeHandle.h"
67 //#define DEBUG_DRAW_TIMING 1
68 
69 #include "UndoManager.h"
70 
71 #include "AColor.h"
72 #include "AllThemeResources.h"
73 #include "AudioIO.h"
74 #include "float_cast.h"
75 
76 #include "Prefs.h"
77 #include "RefreshCode.h"
78 #include "TrackArtist.h"
79 #include "TrackPanelAx.h"
80 #include "TrackPanelResizerCell.h"
81 #include "WaveTrack.h"
82 
83 #include "tracks/ui/TrackControls.h"
84 #include "tracks/ui/TrackView.h"
85 #include "tracks/ui/TrackVRulerControls.h"
86 
87 //This loads the appropriate set of cursors, depending on platform.
88 #include "../images/Cursors.h"
89 
90 #include <algorithm>
91 
92 #include <wx/dc.h>
93 #include <wx/dcclient.h>
94 #include <wx/graphics.h>
95 
96 static_assert( kVerticalPadding == kTopMargin + kBottomMargin );
97 static_assert( kTrackInfoBtnSize == kAffordancesAreaHeight, "Drag bar is misaligned with the menu button");
98 
99 /**
100 
101 \class TrackPanel
102 
103 This is a diagram of TrackPanel's division of one (non-stereo) track rectangle.
104 Total height equals TrackView::GetHeight()'s value.  Total width is the wxWindow's
105 width.  Each character that is not . represents one pixel.
106 
107 Inset space of this track, and top inset of the next track, are used to draw the
108 focus highlight.
109 
110 Top inset of the right channel of a stereo track, and bottom shadow line of the
111 left channel, are used for the channel separator.
112 
113 "Margin" is a term used for inset plus border (top and left) or inset plus
114 shadow plus border (right and bottom).
115 
116 GetVRulerOffset() counts columns from the left edge up to and including
117 controls, and is a constant.
118 
119 GetVRulerWidth() is variable -- all tracks have the same ruler width at any
120 time, but that width may be adjusted when tracks change their vertical scales.
121 
122 GetLeftOffset() counts columns up to and including the VRuler and one more,
123 the "one pixel" column.
124 
125 Cell for label has a rectangle that OMITS left, top, and bottom
126 margins
127 
128 Cell for vruler has a rectangle right of the label,
129 up to and including the One Pixel column, and OMITS top and bottom margins
130 
131 Cell() for track returns a rectangle with x == GetLeftOffset(), and OMITS
132 right, top, and bottom margins
133 
134 +--------------- ... ------ ... --------------------- ...       ... -------------+
135 | Top Inset                                                                      |
136 |                                                                                |
137 |  +------------ ... ------ ... --------------------- ...       ... ----------+  |
138 | L|+-Border---- ... ------ ... --------------------- ...       ... -Border-+ |R |
139 | e||+---------- ... -++--- ... -+++----------------- ...       ... -------+| |i |
140 | f|B|                ||         |||                                       |BS|g |
141 | t|o| Controls       || V       |O|  The good stuff                       |oh|h |
142 |  |r|                || R       |n|                                       |ra|t |
143 | I|d|                || u       |e|                                       |dd|  |
144 | n|e|                || l       | |                                       |eo|I |
145 | s|r|                || e       |P|                                       |rw|n |
146 | e|||                || r       |i|                                       ||||s |
147 | t|||                ||         |x|                                       ||||e |
148 |  |||                ||         |e|                                       ||||t |
149 |  |||                ||         |l|                                       ||||  |
150 |  |||                ||         |||                                       ||||  |
151 
152 .  ...                ..         ...                                       ....  .
153 .  ...                ..         ...                                       ....  .
154 .  ...                ..         ...                                       ....  .
155 
156 |  |||                ||         |||                                       ||||  |
157 |  ||+----------     -++--  ... -+++----------------- ...       ... -------+|||  |
158 |  |+-Border---- ... -----  ... --------------------- ...       ... -Border-+||  |
159 |  |  Shadow---- ... -----  ... --------------------- ...       ... --Shadow-+|  |
160 */
161 
162 // Is the distance between A and B less than D?
within(A a,B b,DIST d)163 template < class A, class B, class DIST > bool within(A a, B b, DIST d)
164 {
165    return (a > b - d) && (a < b + d);
166 }
167 
BEGIN_EVENT_TABLE(TrackPanel,CellularPanel)168 BEGIN_EVENT_TABLE(TrackPanel, CellularPanel)
169     EVT_MOUSE_EVENTS(TrackPanel::OnMouseEvent)
170     EVT_KEY_DOWN(TrackPanel::OnKeyDown)
171 
172     EVT_PAINT(TrackPanel::OnPaint)
173 
174     EVT_TIMER(wxID_ANY, TrackPanel::OnTimer)
175 
176     EVT_SIZE(TrackPanel::OnSize)
177 
178 END_EVENT_TABLE()
179 
180 /// Makes a cursor from an XPM, uses CursorId as a fallback.
181 /// TODO:  Move this function to some other source file for reuse elsewhere.
182 std::unique_ptr<wxCursor> MakeCursor( int WXUNUSED(CursorId), const char * const pXpm[36],  int HotX, int HotY )
183 {
184 #define CURSORS_SIZE32
185 #ifdef CURSORS_SIZE32
186    const int HotAdjust =0;
187 #else
188    const int HotAdjust =8;
189 #endif
190 
191    wxImage Image = wxImage(wxBitmap(pXpm).ConvertToImage());
192    Image.SetMaskColour(255,0,0);
193    Image.SetMask();// Enable mask.
194 
195    Image.SetOption( wxIMAGE_OPTION_CUR_HOTSPOT_X, HotX-HotAdjust );
196    Image.SetOption( wxIMAGE_OPTION_CUR_HOTSPOT_Y, HotY-HotAdjust );
197    return std::make_unique<wxCursor>( Image );
198 }
199 
200 
201 namespace{
202 
203 AttachedWindows::RegisteredFactory sKey{
__anon8ee6e9ff0202( ) 204    []( AudacityProject &project ) -> wxWeakRef< wxWindow > {
205       auto &ruler = AdornedRulerPanel::Get( project );
206       auto &viewInfo = ViewInfo::Get( project );
207       auto &window = ProjectWindow::Get( project );
208       auto mainPage = window.GetMainPage();
209       wxASSERT( mainPage ); // to justify safenew
210 
211       auto &tracks = TrackList::Get( project );
212       auto result = safenew TrackPanel(mainPage,
213          window.NextWindowID(),
214          wxDefaultPosition,
215          wxDefaultSize,
216          tracks.shared_from_this(),
217          &viewInfo,
218          &project,
219          &ruler);
220       SetProjectPanel( project, *result );
221       return result;
222    }
223 };
224 
225 }
226 
Get(AudacityProject & project)227 TrackPanel &TrackPanel::Get( AudacityProject &project )
228 {
229    return GetAttachedWindows(project).Get< TrackPanel >( sKey );
230 }
231 
Get(const AudacityProject & project)232 const TrackPanel &TrackPanel::Get( const AudacityProject &project )
233 {
234    return Get( const_cast< AudacityProject & >( project ) );
235 }
236 
Destroy(AudacityProject & project)237 void TrackPanel::Destroy( AudacityProject &project )
238 {
239    auto *pPanel = GetAttachedWindows(project).Find<TrackPanel>( sKey );
240    if (pPanel) {
241       pPanel->wxWindow::Destroy();
242       GetAttachedWindows(project).Assign(sKey, nullptr);
243    }
244 }
245 
246 // Don't warn us about using 'this' in the base member initializer list.
247 #ifndef __WXGTK__ //Get rid if this pragma for gtk
248 #pragma warning( disable: 4355 )
249 #endif
TrackPanel(wxWindow * parent,wxWindowID id,const wxPoint & pos,const wxSize & size,const std::shared_ptr<TrackList> & tracks,ViewInfo * viewInfo,AudacityProject * project,AdornedRulerPanel * ruler)250 TrackPanel::TrackPanel(wxWindow * parent, wxWindowID id,
251                        const wxPoint & pos,
252                        const wxSize & size,
253                        const std::shared_ptr<TrackList> &tracks,
254                        ViewInfo * viewInfo,
255                        AudacityProject * project,
256                        AdornedRulerPanel * ruler)
257    : CellularPanel(parent, id, pos, size, viewInfo,
258                    wxWANTS_CHARS | wxNO_BORDER),
259      mListener( &ProjectWindow::Get( *project ) ),
260      mTracks(tracks),
261      mRuler(ruler),
262      mTrackArtist(nullptr),
263      mRefreshBacking(false)
264 #ifndef __WXGTK__   //Get rid if this pragma for gtk
265 #pragma warning( default: 4355 )
266 #endif
267 {
268    SetLayoutDirection(wxLayout_LeftToRight);
269    SetLabel(XO("Track Panel"));
270    SetName(XO("Track Panel"));
271    SetBackgroundStyle(wxBG_STYLE_PAINT);
272 
273    {
274       auto pAx = std::make_unique <TrackPanelAx>( *project );
275       pAx->SetWindow( this );
276       wxWeakRef< TrackPanel > weakThis{ this };
277       pAx->SetFinder(
278          [weakThis]( const Track &track ) -> wxRect {
279             if (weakThis)
280                return weakThis->FindTrackRect( &track );
281             return {};
282          }
283       );
284       TrackFocus::Get( *GetProject() ).SetAccessible(
285          *this, std::move( pAx ) );
286    }
287 
288    mTrackArtist = std::make_unique<TrackArtist>( this );
289 
290    mTimeCount = 0;
291    mTimer.parent = this;
292    // Timer is started after the window is visible
293    ProjectWindow::Get( *GetProject() ).Bind(wxEVT_IDLE,
294       &TrackPanel::OnIdle, this);
295 
296    // Register for tracklist updates
297    mTracks->Bind(EVT_TRACKLIST_RESIZING,
298                     &TrackPanel::OnTrackListResizing,
299                     this);
300    mTracks->Bind(EVT_TRACKLIST_ADDITION,
301                     &TrackPanel::OnTrackListResizing,
302                     this);
303    mTracks->Bind(EVT_TRACKLIST_DELETION,
304                     &TrackPanel::OnTrackListDeletion,
305                     this);
306    mTracks->Bind(EVT_TRACKLIST_TRACK_REQUEST_VISIBLE,
307                     &TrackPanel::OnEnsureVisible,
308                     this);
309 
310    auto theProject = GetProject();
311    theProject->Bind(
312       EVT_PROJECT_SETTINGS_CHANGE, &TrackPanel::OnProjectSettingsChange, this);
313    theProject->Bind(
314       EVT_TRACK_FOCUS_CHANGE, &TrackPanel::OnTrackFocusChange, this );
315 
316    theProject->Bind(EVT_UNDO_RESET, &TrackPanel::OnUndoReset, this);
317 
318    wxTheApp->Bind(EVT_AUDIOIO_PLAYBACK,
319                      &TrackPanel::OnAudioIO,
320                      this);
321    wxTheApp->Bind(EVT_AUDIOIO_CAPTURE,
322                      &TrackPanel::OnAudioIO,
323                      this);
324    UpdatePrefs();
325 }
326 
327 
~TrackPanel()328 TrackPanel::~TrackPanel()
329 {
330    mTimer.Stop();
331 
332    // This can happen if a label is being edited and the user presses
333    // ALT+F4 or Command+Q
334    if (HasCapture())
335       ReleaseMouse();
336 }
337 
UpdatePrefs()338 void TrackPanel::UpdatePrefs()
339 {
340    // All vertical rulers must be recalculated since the minimum and maximum
341    // frequencies may have been changed.
342    UpdateVRulers();
343 
344    Refresh();
345 }
346 
347 /// Gets the pointer to the AudacityProject that
348 /// goes with this track panel.
GetProject() const349 AudacityProject * TrackPanel::GetProject() const
350 {
351    //JKC casting away constness here.
352    //Do it in two stages in case 'this' is not a wxWindow.
353    //when the compiler will flag the error.
354    wxWindow const * const pConstWind = this;
355    wxWindow * pWind=(wxWindow*)pConstWind;
356 #ifdef EXPERIMENTAL_NOTEBOOK
357    pWind = pWind->GetParent(); //Page
358    wxASSERT( pWind );
359    pWind = pWind->GetParent(); //Notebook
360    wxASSERT( pWind );
361 #endif
362    pWind = pWind->GetParent(); //MainPanel
363    wxASSERT( pWind );
364    pWind = pWind->GetParent(); //ProjectWindow
365    wxASSERT( pWind );
366    return &static_cast<ProjectWindow*>( pWind )->GetProject();
367 }
368 
OnSize(wxSizeEvent & evt)369 void TrackPanel::OnSize( wxSizeEvent &evt )
370 {
371    evt.Skip();
372    const auto &size = evt.GetSize();
373    mViewInfo->SetWidth( size.GetWidth() );
374    mViewInfo->SetHeight( size.GetHeight() );
375 }
376 
OnIdle(wxIdleEvent & event)377 void TrackPanel::OnIdle(wxIdleEvent& event)
378 {
379    event.Skip();
380    // The window must be ready when the timer fires (#1401)
381    if (IsShownOnScreen())
382    {
383       mTimer.Start(kTimerInterval, FALSE);
384 
385       // Timer is started, we don't need the event anymore
386       GetProjectFrame( *GetProject() ).Unbind(wxEVT_IDLE,
387          &TrackPanel::OnIdle, this);
388    }
389    else
390    {
391       // Get another idle event, wx only guarantees we get one
392       // event after "some other normal events occur"
393       event.RequestMore();
394    }
395 }
396 
397 /// AS: This gets called on our wx timer events.
OnTimer(wxTimerEvent &)398 void TrackPanel::OnTimer(wxTimerEvent& )
399 {
400    mTimeCount++;
401 
402    AudacityProject *const p = GetProject();
403    auto &window = ProjectWindow::Get( *p );
404 
405    auto &projectAudioIO = ProjectAudioIO::Get( *p );
406    auto gAudioIO = AudioIO::Get();
407 
408    // Check whether we were playing or recording, but the stream has stopped.
409    if (projectAudioIO.GetAudioIOToken()>0 && !IsAudioActive())
410    {
411       //the stream may have been started up after this one finished (by some other project)
412       //in that case reset the buttons don't stop the stream
413       auto &projectAudioManager = ProjectAudioManager::Get( *p );
414       projectAudioManager.Stop(!gAudioIO->IsStreamActive());
415    }
416 
417    // Next, check to see if we were playing or recording
418    // audio, but now Audio I/O is completely finished.
419    if (projectAudioIO.GetAudioIOToken()>0 &&
420          !gAudioIO->IsAudioTokenActive(projectAudioIO.GetAudioIOToken()))
421    {
422       projectAudioIO.SetAudioIOToken(0);
423       window.RedrawProject();
424    }
425    if (mLastDrawnSelectedRegion != mViewInfo->selectedRegion) {
426       UpdateSelectionDisplay();
427    }
428 
429    // Notify listeners for timer ticks
430    {
431       wxCommandEvent e(EVT_TRACK_PANEL_TIMER);
432       p->ProcessEvent(e);
433    }
434 
435    DrawOverlays(false);
436    mRuler->DrawOverlays(false);
437 
438    if(IsAudioActive() && gAudioIO->GetNumCaptureChannels()) {
439 
440       // Periodically update the display while recording
441 
442       if ((mTimeCount % 5) == 0) {
443          // Must tell OnPaint() to recreate the backing bitmap
444          // since we've not done a full refresh.
445          mRefreshBacking = true;
446          Refresh( false );
447       }
448    }
449    if(mTimeCount > 1000)
450       mTimeCount = 0;
451 }
452 
OnProjectSettingsChange(wxCommandEvent & event)453 void TrackPanel::OnProjectSettingsChange( wxCommandEvent &event )
454 {
455    event.Skip();
456    switch ( static_cast<ProjectSettings::EventCode>( event.GetInt() ) ) {
457    case ProjectSettings::ChangedSyncLock:
458       Refresh(false);
459       break;
460    default:
461       break;
462    }
463 }
464 
OnUndoReset(wxCommandEvent & event)465 void TrackPanel::OnUndoReset( wxCommandEvent &event )
466 {
467    event.Skip();
468    TrackFocus::Get( *GetProject() ).Set( nullptr );
469    Refresh( false );
470 }
471 
472 /// AS: OnPaint( ) is called during the normal course of
473 ///  completing a repaint operation.
OnPaint(wxPaintEvent &)474 void TrackPanel::OnPaint(wxPaintEvent & /* event */)
475 {
476    mLastDrawnSelectedRegion = mViewInfo->selectedRegion;
477 
478 #if DEBUG_DRAW_TIMING
479    wxStopWatch sw;
480 #endif
481 
482    {
483       wxPaintDC dc(this);
484 
485       // Retrieve the damage rectangle
486       wxRect box = GetUpdateRegion().GetBox();
487 
488       // Recreate the backing bitmap if we have a full refresh
489       // (See TrackPanel::Refresh())
490       if (mRefreshBacking || (box == GetRect()))
491       {
492          // Reset (should a mutex be used???)
493          mRefreshBacking = false;
494 
495          // Redraw the backing bitmap
496          DrawTracks(&GetBackingDCForRepaint());
497 
498          // Copy it to the display
499          DisplayBitmap(dc);
500       }
501       else
502       {
503          // Copy full, possibly clipped, damage rectangle
504          RepairBitmap(dc, box.x, box.y, box.width, box.height);
505       }
506 
507       // Done with the clipped DC
508 
509       // Drawing now goes directly to the client area.
510       // DrawOverlays() may need to draw outside the clipped region.
511       // (Used to make a NEW, separate wxClientDC, but that risks flashing
512       // problems on Mac.)
513       dc.DestroyClippingRegion();
514       DrawOverlays(true, &dc);
515    }
516 
517 #if DEBUG_DRAW_TIMING
518    sw.Pause();
519    wxLogDebug(wxT("Total: %ld milliseconds"), sw.Time());
520    wxPrintf(wxT("Total: %ld milliseconds\n"), sw.Time());
521 #endif
522 }
523 
MakeParentRedrawScrollbars()524 void TrackPanel::MakeParentRedrawScrollbars()
525 {
526    mListener->TP_RedrawScrollbars();
527 }
528 
529 namespace {
FindTrack(TrackPanelCell * pCell)530    std::shared_ptr<Track> FindTrack(TrackPanelCell *pCell )
531    {
532       if (pCell)
533          return static_cast<CommonTrackPanelCell*>( pCell )->FindTrack();
534       return {};
535    }
536 }
537 
ProcessUIHandleResult(TrackPanelCell * pClickedCell,TrackPanelCell * pLatestCell,UIHandle::Result refreshResult)538 void TrackPanel::ProcessUIHandleResult
539    (TrackPanelCell *pClickedCell, TrackPanelCell *pLatestCell,
540     UIHandle::Result refreshResult)
541 {
542    const auto panel = this;
543    auto pLatestTrack = FindTrack( pLatestCell ).get();
544 
545    // This precaution checks that the track is not only nonnull, but also
546    // really owned by the track list
547    auto pClickedTrack = GetTracks()->Lock(
548       std::weak_ptr<Track>{ FindTrack( pClickedCell ) }
549    ).get();
550 
551    // TODO:  make a finer distinction between refreshing the track control area,
552    // and the waveform area.  As it is, redraw both whenever you must redraw either.
553 
554    // Copy data from the underlying tracks to the pending tracks that are
555    // really displayed
556    TrackList::Get( *panel->GetProject() ).UpdatePendingTracks();
557 
558    using namespace RefreshCode;
559 
560    if (refreshResult & DestroyedCell) {
561       panel->UpdateViewIfNoTracks();
562       // Beware stale pointer!
563       if (pLatestTrack == pClickedTrack)
564          pLatestTrack = NULL;
565       pClickedTrack = NULL;
566    }
567 
568    if (pClickedTrack && (refreshResult & RefreshCode::UpdateVRuler))
569       panel->UpdateVRuler(pClickedTrack);
570 
571    if (refreshResult & RefreshCode::DrawOverlays) {
572       panel->DrawOverlays(false);
573       mRuler->DrawOverlays(false);
574    }
575 
576    // Refresh all if told to do so, or if told to refresh a track that
577    // is not known.
578    const bool refreshAll =
579       (    (refreshResult & RefreshAll)
580        || ((refreshResult & RefreshCell) && !pClickedTrack)
581        || ((refreshResult & RefreshLatestCell) && !pLatestTrack));
582 
583    if (refreshAll)
584       panel->Refresh(false);
585    else {
586       if (refreshResult & RefreshCell)
587          panel->RefreshTrack(pClickedTrack);
588       if (refreshResult & RefreshLatestCell)
589          panel->RefreshTrack(pLatestTrack);
590    }
591 
592    if (refreshResult & FixScrollbars)
593       panel->MakeParentRedrawScrollbars();
594 
595    if (refreshResult & Resize)
596       panel->GetListener()->TP_HandleResize();
597 
598    if ((refreshResult & RefreshCode::EnsureVisible) && pClickedTrack) {
599       TrackFocus::Get(*GetProject()).Set(pClickedTrack);
600       pClickedTrack->EnsureVisible();
601    }
602 }
603 
HandlePageUpKey()604 void TrackPanel::HandlePageUpKey()
605 {
606    mListener->TP_ScrollWindow(2 * mViewInfo->h - mViewInfo->GetScreenEndTime());
607 }
608 
HandlePageDownKey()609 void TrackPanel::HandlePageDownKey()
610 {
611    mListener->TP_ScrollWindow(mViewInfo->GetScreenEndTime());
612 }
613 
IsAudioActive()614 bool TrackPanel::IsAudioActive()
615 {
616    AudacityProject *p = GetProject();
617    return ProjectAudioIO::Get( *p ).IsAudioActive();
618 }
619 
UpdateStatusMessage(const TranslatableString & st)620 void TrackPanel::UpdateStatusMessage( const TranslatableString &st )
621 {
622    auto status = st;
623    if (HasEscape())
624       /* i18n-hint Esc is a key on the keyboard */
625       status.Join( XO("(Esc to cancel)"), " " );
626    ProjectStatus::Get( *GetProject() ).Set( status );
627 }
628 
UpdateSelectionDisplay()629 void TrackPanel::UpdateSelectionDisplay()
630 {
631    // Full refresh since the label area may need to indicate
632    // newly selected tracks.
633    Refresh(false);
634 
635    // Make sure the ruler follows suit.
636    mRuler->DrawSelection();
637 }
638 
639 // Counts selected tracks, counting stereo tracks as one track.
GetSelectedTrackCount() const640 size_t TrackPanel::GetSelectedTrackCount() const
641 {
642    return GetTracks()->SelectedLeaders().size();
643 }
644 
UpdateViewIfNoTracks()645 void TrackPanel::UpdateViewIfNoTracks()
646 {
647    if (mTracks->empty())
648    {
649       // BG: There are no more tracks on screen
650       //BG: Set zoom to normal
651       mViewInfo->SetZoom(ZoomInfo::GetDefaultZoom());
652 
653       //STM: Set selection to 0,0
654       //PRL: and default the rest of the selection information
655       mViewInfo->selectedRegion = SelectedRegion();
656 
657       // PRL:  Following causes the time ruler to align 0 with left edge.
658       // Bug 972
659       mViewInfo->h = 0;
660 
661       mListener->TP_HandleResize();
662       //STM: Clear message if all tracks are removed
663       ProjectStatus::Get( *GetProject() ).Set({});
664    }
665 }
666 
667 // The tracks positions within the list have changed, so update the vertical
668 // ruler size for the track that triggered the event.
OnTrackListResizing(TrackListEvent & e)669 void TrackPanel::OnTrackListResizing(TrackListEvent & e)
670 {
671    auto t = e.mpTrack.lock();
672    // A deleted track can trigger the event.  In which case do nothing here.
673    // A deleted track can have a valid pointer but no owner, bug 2060
674    if( t && t->HasOwner() )
675       UpdateVRuler(t.get());
676    e.Skip();
677 
678    // fix for bug 2477
679    mListener->TP_RedrawScrollbars();
680 }
681 
682 // Tracks have been removed from the list.
OnTrackListDeletion(wxEvent & e)683 void TrackPanel::OnTrackListDeletion(wxEvent & e)
684 {
685    // copy shared_ptr for safety, as in HandleClick
686    auto handle = Target();
687    if (handle) {
688       handle->OnProjectChange(GetProject());
689    }
690 
691    // If the focused track disappeared but there are still other tracks,
692    // this reassigns focus.
693    TrackFocus( *GetProject() ).Get();
694 
695    UpdateVRulerSize();
696 
697    e.Skip();
698 }
699 
OnKeyDown(wxKeyEvent & event)700 void TrackPanel::OnKeyDown(wxKeyEvent & event)
701 {
702    switch (event.GetKeyCode())
703    {
704       // Allow PageUp and PageDown keys to
705       //scroll the Track Panel left and right
706    case WXK_PAGEUP:
707       HandlePageUpKey();
708       return;
709 
710    case WXK_PAGEDOWN:
711       HandlePageDownKey();
712       return;
713 
714    default:
715       // fall through to base class handler
716       event.Skip();
717    }
718 }
719 
OnMouseEvent(wxMouseEvent & event)720 void TrackPanel::OnMouseEvent(wxMouseEvent & event)
721 {
722    if (event.LeftDown()) {
723       // wxTimers seem to be a little unreliable, so this
724       // "primes" it to make sure it keeps going for a while...
725 
726       // When this timer fires, we call TrackPanel::OnTimer and
727       // possibly update the screen for offscreen scrolling.
728       mTimer.Stop();
729       mTimer.Start(kTimerInterval, FALSE);
730    }
731 
732 
733    if (event.ButtonUp()) {
734       //EnsureVisible should be called after processing the up-click.
735       this->CallAfter( [this, event]{
736          const auto foundCell = FindCell(event.m_x, event.m_y);
737          const auto t = FindTrack( foundCell.pCell.get() );
738          if ( t ) {
739             TrackFocus::Get(*GetProject()).Set(t.get());
740             t->EnsureVisible();
741          }
742       } );
743    }
744 
745    // Must also fall through to base class handler
746    event.Skip();
747 }
748 
GetMostRecentXPos()749 double TrackPanel::GetMostRecentXPos()
750 {
751    return mViewInfo->PositionToTime(
752       MostRecentXCoord(), mViewInfo->GetLeftOffset());
753 }
754 
RefreshTrack(Track * trk,bool refreshbacking)755 void TrackPanel::RefreshTrack(Track *trk, bool refreshbacking)
756 {
757    if (!trk)
758       return;
759 
760    // Always move to the first channel of the group, and use only
761    // the sum of channel heights, not the height of any channel alone!
762    trk = *GetTracks()->FindLeader(trk);
763    auto &view = TrackView::Get( *trk );
764    auto height =
765       TrackList::Channels(trk).sum( TrackView::GetTrackHeight );
766 
767    // Set rectangle top according to the scrolling position, `vpos`
768    // Subtract the inset (above) and shadow (below) from the height of the
769    // rectangle, but not the border
770    // This matters because some separators do paint over the border
771    const auto top =
772       -mViewInfo->vpos + view.GetCumulativeHeightBefore() + kTopInset;
773    height -= (kTopInset + kShadowThickness);
774 
775    // Width also subtracts insets (left and right) plus shadow (right)
776    const auto left = kLeftInset;
777    const auto width = GetRect().GetWidth()
778       - (kLeftInset + kRightInset + kShadowThickness);
779 
780    wxRect rect(left, top, width, height);
781 
782    if( refreshbacking )
783       mRefreshBacking = true;
784 
785    Refresh( false, &rect );
786 }
787 
788 
789 /// This method overrides Refresh() of wxWindow so that the
790 /// boolean play indicator can be set to false, so that an old play indicator that is
791 /// no longer there won't get  XORed (to erase it), thus redrawing it on the
792 /// TrackPanel
Refresh(bool eraseBackground,const wxRect * rect)793 void TrackPanel::Refresh(bool eraseBackground /* = TRUE */,
794                          const wxRect *rect /* = NULL */)
795 {
796    // Tell OnPaint() to refresh the backing bitmap.
797    //
798    // Originally I had the check within the OnPaint() routine and it
799    // was working fine.  That was until I found that, even though a full
800    // refresh was requested, Windows only set the onscreen portion of a
801    // window as damaged.
802    //
803    // So, if any part of the trackpanel was off the screen, full refreshes
804    // didn't work and the display got corrupted.
805    if( !rect || ( *rect == GetRect() ) )
806    {
807       mRefreshBacking = true;
808    }
809    wxWindow::Refresh(eraseBackground, rect);
810 
811    CallAfter([this]{ CellularPanel::HandleCursorForPresentMouseState(); } );
812 }
813 
OnAudioIO(wxCommandEvent & evt)814 void TrackPanel::OnAudioIO(wxCommandEvent & evt)
815 {
816    evt.Skip();
817    // Some hit tests want to change their cursor to and from the ban symbol
818    CallAfter( [this]{ CellularPanel::HandleCursorForPresentMouseState(); } );
819 }
820 
821 #include "TrackPanelDrawingContext.h"
822 
823 /// Draw the actual track areas.  We only draw the borders
824 /// and the little buttons and menues and whatnot here, the
825 /// actual contents of each track are drawn by the TrackArtist.
DrawTracks(wxDC * dc)826 void TrackPanel::DrawTracks(wxDC * dc)
827 {
828    wxRegion region = GetUpdateRegion();
829 
830    const wxRect clip = GetRect();
831 
832    const SelectedRegion &sr = mViewInfo->selectedRegion;
833    mTrackArtist->pSelectedRegion = &sr;
834    mTrackArtist->pZoomInfo = mViewInfo;
835    TrackPanelDrawingContext context {
836       *dc, Target(), mLastMouseState, mTrackArtist.get()
837    };
838 
839    // Don't draw a bottom margin here.
840 
841    const auto &settings = ProjectSettings::Get( *GetProject() );
842    bool bMultiToolDown =
843       (ToolCodes::multiTool == settings.GetTool());
844    bool envelopeFlag   =
845       bMultiToolDown || (ToolCodes::envelopeTool == settings.GetTool());
846    bool bigPointsFlag  =
847       bMultiToolDown || (ToolCodes::drawTool == settings.GetTool());
848    bool sliderFlag     = bMultiToolDown;
849    bool brushFlag   = false;
850 #ifdef EXPERIMENTAL_BRUSH_TOOL
851    brushFlag   = (ToolCodes::brushTool == settings.GetTool());
852 #endif
853 
854    const bool hasSolo = GetTracks()->Any< PlayableTrack >()
855       .any_of( []( const PlayableTrack *pt ) {
856          pt = static_cast< const PlayableTrack * >(
857             pt->SubstitutePendingChangedTrack().get() );
858          return (pt && pt->GetSolo());
859       } );
860 
861    mTrackArtist->drawEnvelope = envelopeFlag;
862    mTrackArtist->bigPoints = bigPointsFlag;
863    mTrackArtist->drawSliders = sliderFlag;
864    mTrackArtist->onBrushTool = brushFlag;
865    mTrackArtist->hasSolo = hasSolo;
866 
867    this->CellularPanel::Draw( context, TrackArtist::NPasses );
868 }
869 
SetBackgroundCell(const std::shared_ptr<CommonTrackPanelCell> & pCell)870 void TrackPanel::SetBackgroundCell
871 (const std::shared_ptr< CommonTrackPanelCell > &pCell)
872 {
873    mpBackground = pCell;
874 }
875 
GetBackgroundCell()876 std::shared_ptr< CommonTrackPanelCell > TrackPanel::GetBackgroundCell()
877 {
878    return mpBackground;
879 }
880 
881 namespace {
FindAdjustedChannelHeights(Track & t)882 std::vector<int> FindAdjustedChannelHeights( Track &t )
883 {
884    auto channels = TrackList::Channels(&t);
885    wxASSERT(!channels.empty());
886 
887    // Collect heights, and count affordances
888    int nAffordances = 0;
889    int totalHeight = 0;
890    std::vector<int> oldHeights;
891    for (auto channel : channels) {
892       auto &view = TrackView::Get( *channel );
893       const auto height = view.GetHeight();
894       totalHeight += height;
895       oldHeights.push_back( height );
896       if (view.GetAffordanceControls())
897          ++nAffordances;
898    }
899 
900    // Allocate results
901    auto nChannels = static_cast<int>(oldHeights.size());
902    std::vector<int> results;
903    results.reserve(nChannels);
904 
905    // Now reallocate the channel heights for the presence of affordances
906    // and separators
907    auto availableHeight = totalHeight
908       - nAffordances * kAffordancesAreaHeight
909       - (nChannels - 1) * kChannelSeparatorThickness
910       - kTrackSeparatorThickness;
911    int cumulativeOldHeight = 0;
912    int cumulativeNewHeight = 0;
913    for (const auto &oldHeight : oldHeights) {
914       // Preserve the porportions among the stored heights
915       cumulativeOldHeight += oldHeight;
916       const auto newHeight =
917          cumulativeOldHeight * availableHeight / totalHeight
918             - cumulativeNewHeight;
919       cumulativeNewHeight += newHeight;
920       results.push_back(newHeight);
921    }
922 
923    return results;
924 }
925 }
926 
UpdateVRulers()927 void TrackPanel::UpdateVRulers()
928 {
929    for (auto t : GetTracks()->Any< WaveTrack >())
930       UpdateTrackVRuler(t);
931 
932    UpdateVRulerSize();
933 }
934 
UpdateVRuler(Track * t)935 void TrackPanel::UpdateVRuler(Track *t)
936 {
937    if (t)
938       UpdateTrackVRuler(t);
939 
940    UpdateVRulerSize();
941 }
942 
UpdateTrackVRuler(Track * t)943 void TrackPanel::UpdateTrackVRuler(Track *t)
944 {
945    wxASSERT(t);
946    if (!t)
947       return;
948 
949    auto heights = FindAdjustedChannelHeights(*t);
950 
951    wxRect rect(mViewInfo->GetVRulerOffset(),
952             0,
953             mViewInfo->GetVRulerWidth(),
954             0);
955 
956    auto pHeight = heights.begin();
957    for (auto channel : TrackList::Channels(t)) {
958       auto &view = TrackView::Get( *channel );
959       const auto height = *pHeight++;
960       rect.SetHeight( height );
961       const auto subViews = view.GetSubViews( rect );
962       if (subViews.empty())
963          continue;
964 
965       auto iter = subViews.begin(), end = subViews.end(), next = iter;
966       auto yy = iter->first;
967       wxSize vRulerSize{ 0, 0 };
968       for ( ; iter != end; iter = next ) {
969          ++next;
970          auto nextY = ( next == end )
971             ? height
972             : next->first;
973          rect.SetHeight( nextY - yy );
974          // This causes ruler size in the track to be reassigned:
975          TrackVRulerControls::Get( *iter->second ).UpdateRuler( rect );
976          // But we want to know the maximum width and height over all sub-views:
977          vRulerSize.IncTo( t->vrulerSize );
978          yy = nextY;
979       }
980       t->vrulerSize = vRulerSize;
981    }
982 }
983 
UpdateVRulerSize()984 void TrackPanel::UpdateVRulerSize()
985 {
986    auto trackRange = GetTracks()->Any();
987    if (trackRange) {
988       wxSize s { 0, 0 };
989       for (auto t : trackRange)
990          s.IncTo(t->vrulerSize);
991 
992       if (mViewInfo->GetVRulerWidth() != s.GetWidth()) {
993          mViewInfo->SetVRulerWidth( s.GetWidth() );
994          mRuler->SetLeftOffset(
995             mViewInfo->GetLeftOffset());  // bevel on AdornedRuler
996          mRuler->Refresh();
997       }
998    }
999    Refresh(false);
1000 }
1001 
OnTrackMenu(Track * t)1002 void TrackPanel::OnTrackMenu(Track *t)
1003 {
1004    CellularPanel::DoContextMenu( t ? &TrackView::Get( *t ) : nullptr );
1005 }
1006 
1007 // Tracks have been removed from the list.
OnEnsureVisible(TrackListEvent & e)1008 void TrackPanel::OnEnsureVisible(TrackListEvent & e)
1009 {
1010    e.Skip();
1011    bool modifyState = e.GetInt();
1012 
1013    auto pTrack = e.mpTrack.lock();
1014    auto t = pTrack.get();
1015 
1016    int trackTop = 0;
1017    int trackHeight =0;
1018 
1019    for (auto it : GetTracks()->Leaders()) {
1020       trackTop += trackHeight;
1021 
1022       auto channels = TrackList::Channels(it);
1023       trackHeight = channels.sum( TrackView::GetTrackHeight );
1024 
1025       //We have found the track we want to ensure is visible.
1026       if (channels.contains(t)) {
1027 
1028          //Get the size of the trackpanel.
1029          int width, height;
1030          GetSize(&width, &height);
1031 
1032          if (trackTop < mViewInfo->vpos) {
1033             height = mViewInfo->vpos - trackTop + mViewInfo->scrollStep;
1034             height /= mViewInfo->scrollStep;
1035             mListener->TP_ScrollUpDown(-height);
1036          }
1037          else if (trackTop + trackHeight > mViewInfo->vpos + height) {
1038             height = (trackTop + trackHeight) - (mViewInfo->vpos + height);
1039             height = (height + mViewInfo->scrollStep + 1) / mViewInfo->scrollStep;
1040             mListener->TP_ScrollUpDown(height);
1041          }
1042 
1043          break;
1044       }
1045    }
1046    Refresh(false);
1047 
1048    if ( modifyState )
1049       ProjectHistory::Get( *GetProject() ).ModifyState( false );
1050 }
1051 
1052 // 0.0 scrolls to top
1053 // 1.0 scrolls to bottom.
VerticalScroll(float fracPosition)1054 void TrackPanel::VerticalScroll( float fracPosition){
1055 
1056    int trackTop = 0;
1057    int trackHeight = 0;
1058 
1059    auto tracks = GetTracks();
1060 
1061    auto range = tracks->Leaders();
1062    if (!range.empty()) {
1063       trackHeight = TrackView::GetChannelGroupHeight( *range.rbegin() );
1064       --range.second;
1065    }
1066    trackTop = range.sum( TrackView::GetChannelGroupHeight );
1067 
1068    int delta;
1069 
1070    //Get the size of the trackpanel.
1071    int width, height;
1072    GetSize(&width, &height);
1073 
1074    delta = (fracPosition * (trackTop + trackHeight - height)) - mViewInfo->vpos + mViewInfo->scrollStep;
1075    //wxLogDebug( "Scroll down by %i pixels", delta );
1076    delta /= mViewInfo->scrollStep;
1077    mListener->TP_ScrollUpDown(delta);
1078    Refresh(false);
1079 }
1080 
1081 
1082 namespace {
1083    // Drawing constants
1084    // DisplaceX and MarginX are large enough to avoid overwriting <- symbol
1085    // See TrackArt::DrawNegativeOffsetTrackArrows
1086    enum : int {
1087       // Displacement of the rectangle from upper left corner
1088       DisplaceX = 7, DisplaceY = 1,
1089       // Size of margins about the text extent that determine the rectangle size
1090       MarginX = 8, MarginY = 2,
1091       // Derived constants
1092       MarginsX = 2 * MarginX, MarginsY = 2 * MarginY,
1093    };
1094 
GetTrackNameExtent(wxDC & dc,const Track * t,wxCoord * pW,wxCoord * pH)1095 void GetTrackNameExtent(
1096    wxDC &dc, const Track *t, wxCoord *pW, wxCoord *pH )
1097 {
1098    wxFont labelFont(12, wxFONTFAMILY_SWISS, wxFONTSTYLE_NORMAL, wxFONTWEIGHT_NORMAL);
1099    dc.SetFont(labelFont);
1100    dc.GetTextExtent( t->GetName(), pW, pH );
1101 }
1102 
GetTrackNameRect(int leftOffset,const wxRect & trackRect,wxCoord textWidth,wxCoord textHeight)1103 wxRect GetTrackNameRect(
1104    int leftOffset,
1105    const wxRect &trackRect, wxCoord textWidth, wxCoord textHeight )
1106 {
1107    return {
1108       leftOffset + DisplaceX,
1109       trackRect.y + DisplaceY,
1110       textWidth + MarginsX,
1111       textHeight + MarginsY
1112    };
1113 }
1114 
1115 // Draws the track name on the track, if it is needed.
DrawTrackName(int leftOffset,TrackPanelDrawingContext & context,const Track * t,const wxRect & rect)1116 void DrawTrackName(
1117    int leftOffset,
1118    TrackPanelDrawingContext &context, const Track * t, const wxRect & rect )
1119 {
1120    if( !TrackArtist::Get( context )->mbShowTrackNameInTrack )
1121       return;
1122    auto name = t->GetName();
1123    if( name.IsEmpty())
1124       return;
1125    if( !t->IsLeader())
1126       return;
1127    auto &dc = context.dc;
1128    wxBrush Brush;
1129    wxCoord textWidth, textHeight;
1130    GetTrackNameExtent( dc, t, &textWidth, &textHeight );
1131 
1132    // Logic for name background translucency (aka 'shields')
1133    // Tracks less than kOpaqueHeight high will have opaque shields.
1134    // Tracks more than kTranslucentHeight will have maximum translucency for shields.
1135    const int kOpaqueHeight = 44;
1136    const int kTranslucentHeight = 124;
1137 
1138    // PRL:  to do:  reexamine this strange use of TrackView::GetHeight,
1139    // ultimately to compute an opacity
1140    int h = TrackView::Get( *t ).GetHeight();
1141 
1142    // f codes the opacity as a number between 0.0 and 1.0
1143    float f = wxClip((h-kOpaqueHeight)/(float)(kTranslucentHeight-kOpaqueHeight),0.0,1.0);
1144    // kOpaque is the shield's alpha for tracks that are not tall
1145    // kTranslucent is the shield's alpha for tracks that are tall.
1146    const int kOpaque = 255;
1147    const int kTranslucent = 140;
1148    // 0.0 maps to full opacity, 1.0 maps to full translucency.
1149    int opacity = 255 - (255-140)*f;
1150 
1151    const auto nameRect =
1152       GetTrackNameRect( leftOffset, rect, textWidth, textHeight );
1153 
1154 #ifdef __WXMAC__
1155    // Mac dc is a graphics dc already.
1156    AColor::UseThemeColour( &dc, clrTrackInfoSelected, clrTrackPanelText, opacity );
1157    dc.DrawRoundedRectangle( nameRect, 8.0 );
1158 #else
1159    // This little dance with wxImage in order to draw to a graphic dc
1160    // which we can then paste as a translucent bitmap onto the real dc.
1161    enum : int {
1162       SecondMarginX = 1, SecondMarginY = 1,
1163       SecondMarginsX = 2 * SecondMarginX, SecondMarginsY = 2 * SecondMarginY,
1164    };
1165    wxImage image(
1166       textWidth + MarginsX + SecondMarginsX,
1167       textHeight + MarginsY + SecondMarginsY );
1168    image.InitAlpha();
1169    unsigned char *alpha=image.GetAlpha();
1170    memset(alpha, wxIMAGE_ALPHA_TRANSPARENT, image.GetWidth()*image.GetHeight());
1171 
1172    {
1173       std::unique_ptr< wxGraphicsContext >
1174          pGc{ wxGraphicsContext::Create(image) };
1175       auto &gc = *pGc;
1176       // This is to a gc, not a dc.
1177       AColor::UseThemeColour( &gc, clrTrackInfoSelected, clrTrackPanelText, opacity );
1178       // Draw at 1,1, not at 0,0 to avoid clipping of the antialiasing.
1179       gc.DrawRoundedRectangle(
1180          SecondMarginX, SecondMarginY,
1181          textWidth + MarginsX, textHeight + MarginsY, 8.0 );
1182       // destructor of gc updates the wxImage.
1183    }
1184    wxBitmap bitmap( image );
1185    dc.DrawBitmap( bitmap,
1186       nameRect.x - SecondMarginX, nameRect.y - SecondMarginY );
1187 #endif
1188    dc.SetTextForeground(theTheme.Colour( clrTrackPanelText ));
1189    dc.DrawText(t->GetName(),
1190       nameRect.x + MarginX,
1191       nameRect.y + MarginY);
1192 }
1193 
1194 /*
1195 
1196   The following classes define the subdivision of the area of the TrackPanel
1197   into cells with differing responses to mouse, keyboard, and scroll wheel
1198   events.
1199 
1200   The classes defining the less inclusive areas are earlier, while those
1201   defining ever larger hierarchical groupings of cells are later.
1202 
1203   To describe that subdivision again, informally, and top-down:
1204 
1205   Firstly subtract margin areas, on the left and right, that do not interact.
1206 
1207   Secondly subtract a noninterative margin above the first track, and an area
1208   below all tracks that causes deselection of all tracks if you click it.
1209   (One or both of those areas might be vertically scrolled off-screen, though.)
1210   Divide what remains into areas corresponding to the several tracks.
1211 
1212   Thirdly, for each track, subtract an area below, which you can click and drag
1213   to resize the track vertically.
1214 
1215   Fourthly, subtract an area at the left, which contains the track controls,
1216   such as the menu and delete and minimize buttons, and others appropriate
1217   to the track subtype.
1218 
1219   Fifthly, divide what remains into the vertically stacked channels, if there
1220   are more than one, alternating with separators, which can be clicked to
1221   resize the channel views.
1222 
1223   Sixthly, divide each channel into one or more vertically stacked sub-views.
1224 
1225   Lastly, split the area for each sub-view into a vertical ruler, and an area
1226   that displays the channel's own contents.
1227 
1228 */
1229 
1230 struct EmptyCell final : CommonTrackPanelCell {
HitTest__anon8ee6e9ff0a11::EmptyCell1231    std::vector< UIHandlePtr > HitTest(
1232       const TrackPanelMouseState &, const AudacityProject *) override
1233    { return {}; }
DoFindTrack__anon8ee6e9ff0a11::EmptyCell1234    virtual std::shared_ptr< Track > DoFindTrack() override { return {}; }
Instance__anon8ee6e9ff0a11::EmptyCell1235    static std::shared_ptr<EmptyCell> Instance()
1236    {
1237       static auto instance = std::make_shared< EmptyCell >();
1238       return instance;
1239    }
1240 
1241    // TrackPanelDrawable implementation
Draw__anon8ee6e9ff0a11::EmptyCell1242    void Draw(
1243       TrackPanelDrawingContext &context,
1244       const wxRect &rect, unsigned iPass ) override
1245    {
1246       if ( iPass == TrackArtist::PassMargins ) {
1247          // Draw a margin area of TrackPanel
1248          auto dc = &context.dc;
1249 
1250          AColor::TrackPanelBackground( dc, false );
1251          dc->DrawRectangle( rect );
1252       }
1253    }
1254 };
1255 
1256 // A vertical ruler left of a channel
1257 struct VRulerAndChannel final : TrackPanelGroup {
VRulerAndChannel__anon8ee6e9ff0a11::VRulerAndChannel1258    VRulerAndChannel(
1259       const std::shared_ptr< TrackView > &pView, wxCoord leftOffset )
1260          : mpView{ pView }, mLeftOffset{ leftOffset } {}
Children__anon8ee6e9ff0a11::VRulerAndChannel1261    Subdivision Children( const wxRect &rect ) override
1262    {
1263       return { Axis::X, Refinement{
1264          { rect.GetLeft(),
1265            TrackVRulerControls::Get( *mpView ).shared_from_this() },
1266          { mLeftOffset, mpView }
1267       } };
1268    }
1269    std::shared_ptr< TrackView > mpView;
1270    wxCoord mLeftOffset;
1271 };
1272 
1273 // One or more sub-views of one channel, stacked vertically, each containing
1274 // a vertical ruler and a channel
1275 struct VRulersAndChannels final : TrackPanelGroup {
VRulersAndChannels__anon8ee6e9ff0a11::VRulersAndChannels1276    VRulersAndChannels(
1277       const std::shared_ptr<Track> &pTrack,
1278       TrackView::Refinement refinement, wxCoord leftOffset )
1279          : mpTrack{ pTrack }
1280          , mRefinement{ std::move( refinement ) }
1281          , mLeftOffset{ leftOffset } {}
Children__anon8ee6e9ff0a11::VRulersAndChannels1282    Subdivision Children( const wxRect &rect ) override
1283    {
1284       Refinement refinement;
1285       auto y1 = rect.GetTop();
1286       for ( const auto &subView : mRefinement ) {
1287          y1 = std::max( y1, subView.first );
1288          refinement.emplace_back( y1,
1289             std::make_shared< VRulerAndChannel >(
1290                subView.second, mLeftOffset ) );
1291       }
1292       return { Axis::Y, std::move( refinement ) };
1293    }
1294 
1295    // TrackPanelDrawable implementation
Draw__anon8ee6e9ff0a11::VRulersAndChannels1296    void Draw(
1297       TrackPanelDrawingContext &context,
1298       const wxRect &rect, unsigned iPass ) override
1299    {
1300       // This overpaints the track area, but sometimes too the stereo channel
1301       // separator, so draw at least later than that
1302       if ( iPass == TrackArtist::PassBorders ) {
1303          DrawTrackName( mLeftOffset,
1304             context, mpTrack->SubstitutePendingChangedTrack().get(), rect );
1305       }
1306       if ( iPass == TrackArtist::PassControls ) {
1307          if (mRefinement.size() > 1) {
1308             // Draw lines separating sub-views
1309             auto &dc = context.dc;
1310             AColor::CursorColor( &dc );
1311             auto iter = mRefinement.begin() + 1, end = mRefinement.end();
1312             for ( ; iter != end; ++iter ) {
1313                auto yy = iter->first;
1314                AColor::Line( dc, mLeftOffset, yy, rect.GetRight(), yy );
1315             }
1316          }
1317       }
1318    }
1319 
DrawingArea__anon8ee6e9ff0a11::VRulersAndChannels1320    wxRect DrawingArea(
1321       TrackPanelDrawingContext &context,
1322       const wxRect &rect, const wxRect &panelRect, unsigned iPass ) override
1323    {
1324       auto result = rect;
1325       if ( iPass == TrackArtist::PassBorders ) {
1326          if ( true ) {
1327             wxCoord textWidth, textHeight;
1328             GetTrackNameExtent( context.dc, mpTrack.get(),
1329                &textWidth, &textHeight );
1330             result =
1331                GetTrackNameRect( mLeftOffset, rect, textWidth, textHeight );
1332          }
1333       }
1334       return result;
1335    }
1336 
1337    std::shared_ptr< Track > mpTrack;
1338    TrackView::Refinement mRefinement;
1339    wxCoord mLeftOffset;
1340 };
1341 
1342 //Simply fills area using specified brush and outlines borders
1343 class EmptyPanelRect final : public CommonTrackPanelCell
1344 {
1345    //Required to keep selection behaviour similar to others
1346    std::shared_ptr<Track> mTrack;
1347    int mFillBrushName;
1348 public:
EmptyPanelRect(const std::shared_ptr<Track> & track,int fillBrushName)1349    explicit EmptyPanelRect(const std::shared_ptr<Track>& track, int fillBrushName)
1350       : mTrack(track), mFillBrushName(fillBrushName)
1351    {
1352    }
1353 
~EmptyPanelRect()1354    ~EmptyPanelRect() { }
1355 
Draw(TrackPanelDrawingContext & context,const wxRect & rect,unsigned iPass)1356    void Draw(TrackPanelDrawingContext& context,
1357       const wxRect& rect, unsigned iPass) override
1358    {
1359       if (iPass == TrackArtist::PassBackground)
1360       {
1361          context.dc.SetPen(*wxTRANSPARENT_PEN);
1362          AColor::UseThemeColour(&context.dc, mFillBrushName);
1363          context.dc.DrawRectangle(rect);
1364          wxRect bevel(rect.x, rect.y, rect.width - 1, rect.height - 1);
1365          AColor::BevelTrackInfo(context.dc, true, bevel, false);
1366       }
1367    }
1368 
DoFindTrack()1369    std::shared_ptr<Track> DoFindTrack() override
1370    {
1371        return mTrack;
1372    }
1373 
HitTest(const TrackPanelMouseState & state,const AudacityProject * pProject)1374    std::vector<UIHandlePtr> HitTest(const TrackPanelMouseState& state,
1375       const AudacityProject* pProject) override
1376    {
1377       return {};
1378    }
1379 };
1380 
1381 //Simply place children one after another horizontally, without any specific logic
1382 struct HorizontalGroup final : TrackPanelGroup {
1383 
1384    Refinement mRefinement;
1385 
HorizontalGroup__anon8ee6e9ff0a11::HorizontalGroup1386    HorizontalGroup(Refinement refinement)
1387       : mRefinement(std::move(refinement))
1388    {
1389    }
1390 
Children__anon8ee6e9ff0a11::HorizontalGroup1391    Subdivision Children(const wxRect& /*rect*/) override
1392    {
1393       return { Axis::X, mRefinement };
1394    }
1395 
1396 };
1397 
1398 
1399 // optional affordance areas, and n channels with vertical rulers,
1400 // alternating with n - 1 resizers;
1401 // each channel-ruler pair might be divided into multiple views
1402 struct ChannelGroup final : TrackPanelGroup {
ChannelGroup__anon8ee6e9ff0a11::ChannelGroup1403    ChannelGroup( const std::shared_ptr< Track > &pTrack, wxCoord leftOffset )
1404       : mpTrack{ pTrack }, mLeftOffset{ leftOffset } {}
Children__anon8ee6e9ff0a11::ChannelGroup1405    Subdivision Children( const wxRect &rect_ ) override
1406    {
1407       auto rect = rect_;
1408       Refinement refinement;
1409 
1410       const auto channels = TrackList::Channels( mpTrack.get() );
1411       const auto pLast = *channels.rbegin();
1412       wxCoord yy = rect.GetTop();
1413       auto heights = FindAdjustedChannelHeights(*mpTrack);
1414       auto pHeight = heights.begin();
1415       for ( auto channel : channels )
1416       {
1417          auto &view = TrackView::Get( *channel );
1418          if (auto affordance = view.GetAffordanceControls())
1419          {
1420             auto panelRect = std::make_shared<EmptyPanelRect>(
1421                channel->shared_from_this(),
1422                channel->GetSelected() ? clrTrackInfoSelected : clrTrackInfo);
1423             Refinement hgroup {
1424                std::make_pair(rect.GetLeft() + 1, panelRect),
1425                std::make_pair(mLeftOffset, affordance)
1426             };
1427             refinement.emplace_back(yy, std::make_shared<HorizontalGroup>(hgroup));
1428             yy += kAffordancesAreaHeight;
1429          }
1430 
1431          auto height = *pHeight++;
1432          rect.SetTop( yy );
1433          rect.SetHeight( height - kChannelSeparatorThickness );
1434          refinement.emplace_back( yy,
1435             std::make_shared< VRulersAndChannels >(
1436                channel->shared_from_this(),
1437                TrackView::Get( *channel ).GetSubViews( rect ),
1438                mLeftOffset ) );
1439          if ( channel != pLast ) {
1440             yy += height;
1441             refinement.emplace_back(
1442                yy - kChannelSeparatorThickness,
1443                TrackPanelResizerCell::Get( *channel ).shared_from_this() );
1444          }
1445       }
1446 
1447       return { Axis::Y, std::move( refinement ) };
1448    }
1449 
Draw__anon8ee6e9ff0a11::ChannelGroup1450    void Draw(TrackPanelDrawingContext& context, const wxRect& rect, unsigned iPass) override
1451    {
1452       TrackPanelGroup::Draw(context, rect, iPass);
1453       if (iPass == TrackArtist::PassFocus && mpTrack->IsSelected())
1454       {
1455          const auto channels = TrackList::Channels(mpTrack.get());
1456          const auto pLast = *channels.rbegin();
1457          wxCoord yy = rect.GetTop();
1458          auto heights = FindAdjustedChannelHeights(*mpTrack);
1459          auto pHeight = heights.begin();
1460          for (auto channel : channels)
1461          {
1462             auto& view = TrackView::Get(*channel);
1463             auto height = *pHeight++;
1464             if (auto affordance = view.GetAffordanceControls())
1465             {
1466                height += kAffordancesAreaHeight;
1467             }
1468             auto trackRect = wxRect(
1469                mLeftOffset,
1470                yy,
1471                rect.GetRight() - mLeftOffset,
1472                height - kChannelSeparatorThickness);
1473             TrackArt::DrawCursor(context, trackRect, mpTrack.get());
1474             yy += height;
1475          }
1476       }
1477    }
1478 
1479    std::shared_ptr< Track > mpTrack;
1480    wxCoord mLeftOffset;
1481 };
1482 
1483 // A track control panel, left of n vertical rulers and n channels
1484 // alternating with n - 1 resizers
1485 struct LabeledChannelGroup final : TrackPanelGroup {
LabeledChannelGroup__anon8ee6e9ff0a11::LabeledChannelGroup1486    LabeledChannelGroup(
1487       const std::shared_ptr< Track > &pTrack, wxCoord leftOffset )
1488          : mpTrack{ pTrack }, mLeftOffset{ leftOffset } {}
Children__anon8ee6e9ff0a11::LabeledChannelGroup1489    Subdivision Children( const wxRect &rect ) override
1490    { return { Axis::X, Refinement{
1491       { rect.GetLeft(),
1492          TrackControls::Get( *mpTrack ).shared_from_this() },
1493       { rect.GetLeft() + kTrackInfoWidth,
1494         std::make_shared< ChannelGroup >( mpTrack, mLeftOffset ) }
1495    } }; }
1496 
1497    // TrackPanelDrawable implementation
Draw__anon8ee6e9ff0a11::LabeledChannelGroup1498    void Draw( TrackPanelDrawingContext &context,
1499       const wxRect &rect, unsigned iPass ) override
1500    {
1501       if ( iPass == TrackArtist::PassBorders ) {
1502          auto &dc = context.dc;
1503          dc.SetBrush(*wxTRANSPARENT_BRUSH);
1504          dc.SetPen(*wxBLACK_PEN);
1505 
1506          // border
1507          dc.DrawRectangle(
1508             rect.x, rect.y,
1509             rect.width - kShadowThickness, rect.height - kShadowThickness
1510          );
1511 
1512          // shadow
1513          // Stroke lines along bottom and right, which are slightly short at
1514          // bottom-left and top-right
1515          const auto right = rect.GetRight();
1516          const auto bottom = rect.GetBottom();
1517 
1518          // bottom
1519          AColor::Line(dc, rect.x + 2, bottom, right, bottom);
1520          // right
1521          AColor::Line(dc, right, rect.y + 2, right, bottom);
1522       }
1523       if ( iPass == TrackArtist::PassFocus ) {
1524          // Sometimes highlight is not drawn on backing bitmap. I thought
1525          // it was because FindFocus did not return the TrackPanel on Mac, but
1526          // when I removed that test, yielding this condition:
1527          //     if (GetFocusedTrack() != NULL) {
1528          // the highlight was reportedly drawn even when something else
1529          // was the focus and no highlight should be drawn. -RBD
1530          const auto artist = TrackArtist::Get( context );
1531          auto &trackPanel = *artist->parent;
1532          auto &trackFocus = TrackFocus::Get( *trackPanel.GetProject() );
1533          if (trackFocus.Get() == mpTrack.get() &&
1534              wxWindow::FindFocus() == &trackPanel ) {
1535             /// Draw a three-level highlight gradient around the focused track.
1536             wxRect theRect = rect;
1537             auto &dc = context.dc;
1538             dc.SetBrush(*wxTRANSPARENT_BRUSH);
1539 
1540             AColor::TrackFocusPen( &dc, 2 );
1541             dc.DrawRectangle(theRect);
1542             theRect.Deflate(1);
1543 
1544             AColor::TrackFocusPen( &dc, 1 );
1545             dc.DrawRectangle(theRect);
1546             theRect.Deflate(1);
1547 
1548             AColor::TrackFocusPen( &dc, 0 );
1549             dc.DrawRectangle(theRect);
1550          }
1551       }
1552    }
1553 
DrawingArea__anon8ee6e9ff0a11::LabeledChannelGroup1554    wxRect DrawingArea(
1555       TrackPanelDrawingContext &,
1556       const wxRect &rect, const wxRect &, unsigned iPass ) override
1557    {
1558       if ( iPass == TrackArtist::PassBorders )
1559          return {
1560             rect.x - kBorderThickness,
1561             rect.y - kBorderThickness,
1562             rect.width + 2 * kBorderThickness + kShadowThickness,
1563             rect.height + 2 * kBorderThickness + kShadowThickness
1564          };
1565       else if ( iPass == TrackArtist::PassFocus ) {
1566          constexpr auto extra = kBorderThickness + 3;
1567          return {
1568             rect.x - extra,
1569             rect.y - extra,
1570             rect.width + 2 * extra + kShadowThickness,
1571             rect.height + 2 * extra + kShadowThickness
1572          };
1573       }
1574       else
1575          return rect;
1576    }
1577 
1578    std::shared_ptr< Track > mpTrack;
1579    wxCoord mLeftOffset;
1580 };
1581 
1582 // Stacks a label and a single or multi-channel track on a resizer below,
1583 // which is associated with the last channel
1584 struct ResizingChannelGroup final : TrackPanelGroup {
ResizingChannelGroup__anon8ee6e9ff0a11::ResizingChannelGroup1585    ResizingChannelGroup(
1586       const std::shared_ptr< Track > &pTrack, wxCoord leftOffset )
1587          : mpTrack{ pTrack }, mLeftOffset{ leftOffset } {}
Children__anon8ee6e9ff0a11::ResizingChannelGroup1588    Subdivision Children( const wxRect &rect ) override
1589    { return { Axis::Y, Refinement{
1590       { rect.GetTop(),
1591          std::make_shared< LabeledChannelGroup >( mpTrack, mLeftOffset ) },
1592       { rect.GetTop() + rect.GetHeight() - kTrackSeparatorThickness,
1593          TrackPanelResizerCell::Get(
1594             **TrackList::Channels( mpTrack.get() ).rbegin() ).shared_from_this()
1595       }
1596    } }; }
1597    std::shared_ptr< Track > mpTrack;
1598    wxCoord mLeftOffset;
1599 };
1600 
1601 // Stacks a dead area at top, the tracks, and the click-to-deselect area below
1602 struct Subgroup final : TrackPanelGroup {
Subgroup__anon8ee6e9ff0a11::Subgroup1603    explicit Subgroup( TrackPanel &panel ) : mPanel{ panel } {}
Children__anon8ee6e9ff0a11::Subgroup1604    Subdivision Children( const wxRect &rect ) override
1605    {
1606       const auto &viewInfo = *mPanel.GetViewInfo();
1607       wxCoord yy = -viewInfo.vpos;
1608       Refinement refinement;
1609 
1610       auto &tracks = *mPanel.GetTracks();
1611       if ( tracks.Any() )
1612          refinement.emplace_back( yy, EmptyCell::Instance() ),
1613          yy += kTopMargin;
1614 
1615       for ( const auto leader : tracks.Leaders() ) {
1616          wxCoord height = 0;
1617          for ( auto channel : TrackList::Channels( leader ) ) {
1618             auto &view = TrackView::Get( *channel );
1619             height += view.GetHeight();
1620          }
1621          refinement.emplace_back( yy,
1622             std::make_shared< ResizingChannelGroup >(
1623                leader->SharedPointer(), viewInfo.GetLeftOffset() )
1624          );
1625          yy += height;
1626       }
1627 
1628       refinement.emplace_back( std::max( 0, yy ), mPanel.GetBackgroundCell() );
1629 
1630       return { Axis::Y, std::move( refinement ) };
1631    }
1632    TrackPanel &mPanel;
1633 };
1634 
1635 // Main group shaves off the left and right margins
1636 struct MainGroup final : TrackPanelGroup {
MainGroup__anon8ee6e9ff0a11::MainGroup1637    explicit MainGroup( TrackPanel &panel ) : mPanel{ panel } {}
Children__anon8ee6e9ff0a11::MainGroup1638    Subdivision Children( const wxRect &rect ) override
1639    { return { Axis::X, Refinement{
1640       { 0, EmptyCell::Instance() },
1641       { kLeftMargin, std::make_shared< Subgroup >( mPanel ) },
1642       { rect.GetRight() + 1 - kRightMargin, EmptyCell::Instance() }
1643    } }; }
1644    TrackPanel &mPanel;
1645 };
1646 
1647 }
1648 
Root()1649 std::shared_ptr<TrackPanelNode> TrackPanel::Root()
1650 {
1651    // Root and other subgroup objects are throwaways.
1652    // They might instead be cached to avoid repeated allocation.
1653    // That cache would need invalidation when there is addition, deletion, or
1654    // permutation of tracks, or change of width of the vertical rulers.
1655    return std::make_shared< MainGroup >( *this );
1656 }
1657 
1658 // This finds the rectangle of a given track (including all channels),
1659 // either that of the label 'adornment' or the track itself
1660 // The given track is assumed to be the first channel
FindTrackRect(const Track * target)1661 wxRect TrackPanel::FindTrackRect( const Track * target )
1662 {
1663    auto leader = *GetTracks()->FindLeader( target );
1664    if (!leader) {
1665       return {};
1666    }
1667 
1668    return CellularPanel::FindRect( [&] ( TrackPanelNode &node ) {
1669       if (auto pGroup = dynamic_cast<const LabeledChannelGroup*>( &node ))
1670          return pGroup->mpTrack.get() == leader;
1671       return false;
1672    } );
1673 }
1674 
FindFocusedTrackRect(const Track * target)1675 wxRect TrackPanel::FindFocusedTrackRect( const Track * target )
1676 {
1677    auto rect = FindTrackRect(target);
1678    if (rect != wxRect{}) {
1679       // Enlarge horizontally.
1680       // PRL:  perhaps it's one pixel too much each side, including some gray
1681       // beyond the yellow?
1682       rect.x = 0;
1683       GetClientSize(&rect.width, nullptr);
1684 
1685       // Enlarge vertically, enough to enclose the yellow focus border pixels
1686       // The the outermost ring of gray pixels is included on three sides
1687       // but not the top (should that be fixed?)
1688 
1689       // (Note that TrackPanel paints its focus over the "top margin" of the
1690       // rectangle allotted to the track, according to TrackView::GetY() and
1691       // TrackView::GetHeight(), but also over the margin of the next track.)
1692 
1693       rect.height += kBottomMargin;
1694       int dy = kTopMargin - 1;
1695       rect.Inflate( 0, dy );
1696 
1697       // Note that this rectangle does not coincide with any one of
1698       // the nodes in the subdivision.
1699    }
1700    return rect;
1701 }
1702 
FindRulerRects(const Track * target)1703 std::vector<wxRect> TrackPanel::FindRulerRects( const Track *target )
1704 {
1705    std::vector<wxRect> results;
1706    if (target)
1707       VisitCells( [&]( const wxRect &rect, TrackPanelCell &visited ) {
1708          if (auto pRuler = dynamic_cast<const TrackVRulerControls*>(&visited);
1709              pRuler && pRuler->FindTrack().get() == target)
1710             results.push_back(rect);
1711       } );
1712    return results;
1713 }
1714 
GetFocusedCell()1715 TrackPanelCell *TrackPanel::GetFocusedCell()
1716 {
1717    auto pTrack = TrackFocus::Get( *GetProject() ).Get();
1718    return pTrack ? &TrackView::Get( *pTrack ) : GetBackgroundCell().get();
1719 }
1720 
SetFocusedCell()1721 void TrackPanel::SetFocusedCell()
1722 {
1723    // This may have a side-effect of assigning a focus if there was none
1724    auto& trackFocus = TrackFocus::Get(*GetProject());
1725    trackFocus.Set(trackFocus.Get());
1726    KeyboardCapture::Capture(this);
1727 }
1728 
OnTrackFocusChange(wxCommandEvent & event)1729 void TrackPanel::OnTrackFocusChange( wxCommandEvent &event )
1730 {
1731    event.Skip();
1732    auto cell = GetFocusedCell();
1733 
1734    if (cell) {
1735       Refresh( false );
1736    }
1737 }
1738