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