1 /**********************************************************************
2 
3 Audacity: A Digital Audio Editor
4 
5 ProjectWindow.cpp
6 
7 Paul Licameli split from AudacityProject.cpp
8 
9 **********************************************************************/
10 
11 #include "ProjectWindow.h"
12 
13 
14 
15 #include "ActiveProject.h"
16 #include "AllThemeResources.h"
17 #include "AudioIO.h"
18 #include "Menus.h"
19 #include "Project.h"
20 #include "ProjectAudioIO.h"
21 #include "ProjectWindows.h"
22 #include "ProjectStatus.h"
23 #include "RefreshCode.h"
24 #include "TrackPanelMouseEvent.h"
25 #include "TrackPanelAx.h"
26 #include "UndoManager.h"
27 #include "ViewInfo.h"
28 #include "WaveClip.h"
29 #include "WaveTrack.h"
30 #include "prefs/ThemePrefs.h"
31 #include "prefs/TracksPrefs.h"
32 #include "toolbars/ToolManager.h"
33 #include "tracks/ui/Scrubbing.h"
34 #include "tracks/ui/TrackView.h"
35 #include "widgets/wxPanelWrapper.h"
36 #include "widgets/WindowAccessible.h"
37 
38 #include <wx/app.h>
39 #include <wx/display.h>
40 #include <wx/scrolbar.h>
41 #include <wx/sizer.h>
42 
43 // Returns the screen containing a rectangle, or -1 if none does.
ScreenContaining(wxRect & r)44 int ScreenContaining( wxRect & r ){
45    unsigned int n = wxDisplay::GetCount();
46    for(unsigned int i = 0;i<n;i++){
47       wxDisplay d(i);
48       wxRect scr = d.GetClientArea();
49       if( scr.Contains( r ) )
50          return (int)i;
51    }
52    return -1;
53 }
54 
55 // true IFF TL and BR corners are on a connected display.
56 // Does not need to check all four.  We just need to check that
57 // the window probably is straddling screens in a sensible way.
58 // If the user wants to use mixed landscape and portrait, they can.
CornersOnScreen(wxRect & r)59 bool CornersOnScreen( wxRect & r ){
60    if( wxDisplay::GetFromPoint( r.GetTopLeft()  ) == wxNOT_FOUND) return false;
61    if( wxDisplay::GetFromPoint( r.GetBottomRight()  ) == wxNOT_FOUND) return false;
62    return true;
63 }
64 
65 // true iff we have enough of the top bar to be able to reposition the window.
IsWindowAccessible(wxRect * requestedRect)66 bool IsWindowAccessible(wxRect *requestedRect)
67 {
68    wxDisplay display;
69    wxRect targetTitleRect(requestedRect->GetLeftTop(), requestedRect->GetBottomRight());
70    // Hackery to approximate a window top bar size from a window size.
71    // and exclude the open/close and borders.
72    targetTitleRect.x += 15;
73    targetTitleRect.width -= 100;
74    if (targetTitleRect.width <  165) targetTitleRect.width = 165;
75    targetTitleRect.height = 15;
76    int targetBottom = targetTitleRect.GetBottom();
77    int targetRight = targetTitleRect.GetRight();
78    // This looks like overkill to check each and every pixel in the ranges.
79    // and decide that if any is visible on screen we are OK.
80    for (int i =  targetTitleRect.GetLeft(); i < targetRight; i++) {
81       for (int j = targetTitleRect.GetTop(); j < targetBottom; j++) {
82          int monitor = display.GetFromPoint(wxPoint(i, j));
83          if (monitor != wxNOT_FOUND) {
84             return TRUE;
85          }
86       }
87    }
88    return FALSE;
89 }
90 
91 // BG: The default size and position of the first window
GetDefaultWindowRect(wxRect * defRect)92 void GetDefaultWindowRect(wxRect *defRect)
93 {
94    *defRect = wxGetClientDisplayRect();
95 
96    int width = 940;
97    int height = 674;
98 
99    //These conditional values assist in improving placement and size
100    //of NEW windows on different platforms.
101 #ifdef __WXGTK__
102    height += 20;
103 #endif
104 
105 #ifdef __WXMSW__
106    height += 40;
107 #endif
108 
109 #ifdef __WXMAC__
110    height += 55;
111 #endif
112 
113    // Use screen size where it is smaller than the values we would like.
114    // Otherwise use the values we would like, and centred.
115    if (width < defRect->width)
116    {
117       defRect->x = (defRect->width - width)/2;
118       defRect->width = width;
119    }
120 
121    if (height < defRect->height)
122    {
123       defRect->y = (defRect->height - height)/2;
124       // Bug 1119 workaround
125       // Small adjustment for very small Mac screens.
126       // If there is only a tiny space at the top
127       // then instead of vertical centre, align to bottom.
128       const int pixelsFormenu = 60;
129       if( defRect->y < pixelsFormenu )
130          defRect->y *=2;
131       defRect->height = height;
132    }
133 }
134 
135 // BG: Calculate where to place the next window (could be the first window)
136 // BG: Does not store X and Y in prefs. This is intentional.
137 //
138 // LL: This should NOT need to be this complicated...FIXME
GetNextWindowPlacement(wxRect * nextRect,bool * pMaximized,bool * pIconized)139 void GetNextWindowPlacement(wxRect *nextRect, bool *pMaximized, bool *pIconized)
140 {
141    int inc = 25;
142 
143    wxRect defaultRect;
144    GetDefaultWindowRect(&defaultRect);
145 
146    gPrefs->Read(wxT("/Window/Maximized"), pMaximized, false);
147    gPrefs->Read(wxT("/Window/Iconized"), pIconized, false);
148 
149    wxRect windowRect;
150    gPrefs->Read(wxT("/Window/X"), &windowRect.x, defaultRect.x);
151    gPrefs->Read(wxT("/Window/Y"), &windowRect.y, defaultRect.y);
152    gPrefs->Read(wxT("/Window/Width"), &windowRect.width, defaultRect.width);
153    gPrefs->Read(wxT("/Window/Height"), &windowRect.height, defaultRect.height);
154 
155    wxRect normalRect;
156    gPrefs->Read(wxT("/Window/Normal_X"), &normalRect.x, defaultRect.x);
157    gPrefs->Read(wxT("/Window/Normal_Y"), &normalRect.y, defaultRect.y);
158    gPrefs->Read(wxT("/Window/Normal_Width"), &normalRect.width, defaultRect.width);
159    gPrefs->Read(wxT("/Window/Normal_Height"), &normalRect.height, defaultRect.height);
160 
161    // Workaround 2.1.1 and earlier bug on OSX...affects only normalRect, but let's just
162    // validate for all rects and plats
163    if (normalRect.width == 0 || normalRect.height == 0) {
164       normalRect = defaultRect;
165    }
166    if (windowRect.width == 0 || windowRect.height == 0) {
167       windowRect = defaultRect;
168    }
169 
170 
171    wxRect screenRect( wxGetClientDisplayRect());
172 #if defined(__WXMAC__)
173 
174    // On OSX, the top of the window should never be less than the menu height,
175    // so something is amiss if it is
176    if (normalRect.y < screenRect.y) {
177       normalRect = defaultRect;
178    }
179    if (windowRect.y < screenRect.y) {
180       windowRect = defaultRect;
181    }
182 #endif
183 
184    // IF projects empty, THEN it's the first window.
185    // It lands where the config says it should, and can straddle screen.
186    if (AllProjects{}.empty()) {
187       if (*pMaximized || *pIconized) {
188          *nextRect = normalRect;
189       }
190       else {
191          *nextRect = windowRect;
192       }
193       // Resize, for example if one monitor that was on is now off.
194       if (!CornersOnScreen( wxRect(*nextRect).Deflate( 32, 32 ))) {
195          *nextRect = defaultRect;
196       }
197       if (!IsWindowAccessible(nextRect)) {
198          *nextRect = defaultRect;
199       }
200       // Do not trim the first project window down.
201       // All corners are on screen (or almost so), and
202       // the rect may straddle screens.
203       return;
204    }
205 
206 
207    // ELSE a subsequent NEW window.  It will NOT straddle screens.
208 
209    // We don't mind being 32 pixels off the screen in any direction.
210    // Make sure initial sizes (pretty much) fit within the display bounds
211    // We used to trim the sizes which could result in ridiculously small windows.
212    // contributing to bug 1243.
213    // Now instead if the window significantly doesn't fit the screen, we use the default
214    // window instead, which we know does.
215    if (ScreenContaining( wxRect(normalRect).Deflate( 32, 32 ))<0) {
216       normalRect = defaultRect;
217    }
218    if (ScreenContaining( wxRect(windowRect).Deflate( 32, 32 ) )<0) {
219       windowRect = defaultRect;
220    }
221 
222    bool validWindowSize = false;
223    ProjectWindow * validProject = NULL;
224    for ( auto iter = AllProjects{}.rbegin(), end = AllProjects{}.rend();
225       iter != end; ++iter
226    ) {
227       auto pProject = *iter;
228       if (!GetProjectFrame( *pProject ).IsIconized()) {
229          validWindowSize = true;
230          validProject = &ProjectWindow::Get( *pProject );
231          break;
232       }
233    }
234    if (validWindowSize) {
235       *nextRect = validProject->GetRect();
236       *pMaximized = validProject->IsMaximized();
237       *pIconized = validProject->IsIconized();
238       // Do not straddle screens.
239       if (ScreenContaining( wxRect(*nextRect).Deflate( 32, 32 ) )<0) {
240          *nextRect = defaultRect;
241       }
242    }
243    else {
244       *nextRect = normalRect;
245    }
246 
247    //Placement depends on the increments
248    nextRect->x += inc;
249    nextRect->y += inc;
250 
251    // defaultrect is a rectangle on the first screen.  It's the right fallback to
252    // use most of the time if things are not working out right with sizing.
253    // windowRect is a saved rectangle size.
254    // normalRect seems to be a substitute for windowRect when iconized or maximised.
255 
256    // Windows can say that we are off screen when actually we are not.
257    // On Windows 10 I am seeing miscalculation by about 6 pixels.
258    // To fix this we allow some sloppiness on the edge being counted as off screen.
259    // This matters most when restoring very carefully sized windows that are maximised
260    // in one dimension (height or width) but not both.
261    const int edgeSlop = 10;
262 
263    // Next four lines are getting the rectangle for the screen that contains the
264    // top left corner of nextRect (and defaulting to rect of screen 0 otherwise).
265    wxPoint p = nextRect->GetLeftTop();
266    int scr = std::max( 0, wxDisplay::GetFromPoint( p ));
267    wxDisplay d( scr );
268    screenRect = d.GetClientArea();
269 
270    // Now we (possibly) start trimming our rectangle down.
271    // Have we hit the right side of the screen?
272    wxPoint bottomRight = nextRect->GetBottomRight();
273    if (bottomRight.x > (screenRect.GetRight()+edgeSlop)) {
274       int newWidth = screenRect.GetWidth() - nextRect->GetLeft();
275       if (newWidth < defaultRect.GetWidth()) {
276          nextRect->x = windowRect.x;
277          nextRect->y = windowRect.y;
278          nextRect->width = windowRect.width;
279       }
280       else {
281          nextRect->width = newWidth;
282       }
283    }
284 
285    // Have we hit the bottom of the screen?
286    bottomRight = nextRect->GetBottomRight();
287    if (bottomRight.y > (screenRect.GetBottom()+edgeSlop)) {
288       nextRect->y -= inc;
289       bottomRight = nextRect->GetBottomRight();
290       if (bottomRight.y > (screenRect.GetBottom()+edgeSlop)) {
291          nextRect->SetBottom(screenRect.GetBottom());
292       }
293    }
294 
295    // After all that we could have a window that does not have a visible
296    // top bar.  [It is unlikely, but something might have gone wrong]
297    // If so, use the safe fallback size.
298    if (!IsWindowAccessible(nextRect)) {
299       *nextRect = defaultRect;
300    }
301 }
302 
303 namespace {
304 
305 // This wrapper prevents the scrollbars from retaining focus after being
306 // used.  Otherwise, the only way back to the track panel is to click it
307 // and that causes your original location to be lost.
308 class ScrollBar final : public wxScrollBar
309 {
310 public:
ScrollBar(wxWindow * parent,wxWindowID id,long style)311    ScrollBar(wxWindow* parent, wxWindowID id, long style)
312    :   wxScrollBar(parent, id, wxDefaultPosition, wxDefaultSize, style)
313    {
314    }
315 
OnSetFocus(wxFocusEvent & e)316    void OnSetFocus(wxFocusEvent & e)
317    {
318       wxWindow *w = e.GetWindow();
319       if (w != NULL) {
320          w->SetFocus();
321       }
322    }
323 
324    void SetScrollbar(int position, int thumbSize,
325                      int range, int pageSize,
326                      bool refresh = true) override;
327 
328 private:
329    DECLARE_EVENT_TABLE()
330 };
331 
SetScrollbar(int position,int thumbSize,int range,int pageSize,bool refresh)332 void ScrollBar::SetScrollbar(int position, int thumbSize,
333                              int range, int pageSize,
334                              bool refresh)
335 {
336    // Mitigate flashing of scrollbars by refreshing only when something really changes.
337 
338    // PRL:  This may have been made unnecessary by other fixes for flashing, see
339    // commit ac05b190bee7dd0000bce56edb0e5e26185c972f
340 
341    auto changed =
342       position != GetThumbPosition() ||
343       thumbSize != GetThumbSize() ||
344       range != GetRange() ||
345       pageSize != GetPageSize();
346    if (!changed)
347       return;
348 
349    wxScrollBar::SetScrollbar(position, thumbSize, range, pageSize, refresh);
350 }
351 
352 BEGIN_EVENT_TABLE(ScrollBar, wxScrollBar)
353    EVT_SET_FOCUS(ScrollBar::OnSetFocus)
354 END_EVENT_TABLE()
355 
356 // Common mouse wheel handling in track panel cells, moved here to avoid
357 // compilation dependencies on Track, TrackPanel, and Scrubbing at low levels
358 // which made cycles
359 static struct MouseWheelHandler {
360 
MouseWheelHandler__anon01149d290111::MouseWheelHandler361 MouseWheelHandler()
362 {
363    CommonTrackPanelCell::InstallMouseWheelHook( *this );
364 }
365 
366 // Need a bit of memory from one call to the next
367 mutable double mVertScrollRemainder = 0.0;
368 
operator ()__anon01149d290111::MouseWheelHandler369 unsigned operator()
370    ( const TrackPanelMouseEvent &evt, AudacityProject *pProject ) const
371 {
372    using namespace RefreshCode;
373 
374    if ( TrackList::Get( *pProject ).empty() )
375       // Scrolling and Zoom in and out commands are disabled when there are no tracks.
376       // This should be disabled too for consistency.  Otherwise
377       // you do see changes in the time ruler.
378       return Cancelled;
379 
380    unsigned result = RefreshAll;
381    const wxMouseEvent &event = evt.event;
382    auto &viewInfo = ViewInfo::Get( *pProject );
383    Scrubber &scrubber = Scrubber::Get( *pProject );
384    auto &window = ProjectWindow::Get( *pProject );
385    const auto steps = evt.steps;
386 
387    if (event.ShiftDown()
388        // Don't pan during smooth scrolling.  That would conflict with keeping
389        // the play indicator centered.
390        && !scrubber.IsScrollScrubbing()
391       )
392    {
393       // MM: Scroll left/right when used with Shift key down
394       window.TP_ScrollWindow(
395          viewInfo.OffsetTimeByPixels(
396             viewInfo.PositionToTime(0), 50.0 * -steps));
397    }
398    else if (event.CmdDown())
399    {
400 #if 0
401          // JKC: Alternative scroll wheel zooming code
402          // using AudacityProject zooming, which is smarter,
403          // it keeps selections on screen and centred if it can,
404          // also this ensures mousewheel and zoom buttons give same result.
405          double ZoomFactor = pow(2.0, steps);
406          AudacityProject *p = GetProject();
407          if( steps > 0 )
408             // PRL:  Track panel refresh may be needed if you reenable this
409             // code, but we don't want this file dependent on TrackPanel.cpp
410             p->ZoomInByFactor( ZoomFactor );
411          else
412             p->ZoomOutByFactor( ZoomFactor );
413 #endif
414       // MM: Zoom in/out when used with Control key down
415       // We're converting pixel positions to times,
416       // counting pixels from the left edge of the track.
417       int trackLeftEdge = viewInfo.GetLeftOffset();
418 
419       // Time corresponding to mouse position
420       wxCoord xx;
421       double center_h;
422       double mouse_h = viewInfo.PositionToTime(event.m_x, trackLeftEdge);
423 
424       // Scrubbing? Expand or contract about the center, ignoring mouse position
425       if (scrubber.IsScrollScrubbing())
426          center_h = viewInfo.h +
427             (viewInfo.GetScreenEndTime() - viewInfo.h) / 2.0;
428       // Zooming out? Focus on mouse.
429       else if( steps <= 0 )
430          center_h = mouse_h;
431       // No Selection? Focus on mouse.
432       else if((viewInfo.selectedRegion.t1() - viewInfo.selectedRegion.t0() ) < 0.00001  )
433          center_h = mouse_h;
434       // Before Selection? Focus on left
435       else if( mouse_h < viewInfo.selectedRegion.t0() )
436          center_h = viewInfo.selectedRegion.t0();
437       // After Selection? Focus on right
438       else if( mouse_h > viewInfo.selectedRegion.t1() )
439          center_h = viewInfo.selectedRegion.t1();
440       // Inside Selection? Focus on mouse
441       else
442          center_h = mouse_h;
443 
444       xx = viewInfo.TimeToPosition(center_h, trackLeftEdge);
445 
446       // Time corresponding to last (most far right) audio.
447       double audioEndTime = TrackList::Get( *pProject ).GetEndTime();
448 
449 // Disabled this code to fix Bug 1923 (tricky to wheel-zoom right of waveform).
450 #if 0
451       // When zooming in empty space, it's easy to 'lose' the waveform.
452       // This prevents it.
453       // IF zooming in
454       if (steps > 0)
455       {
456          // IF mouse is to right of audio
457          if (center_h > audioEndTime)
458             // Zooming brings far right of audio to mouse.
459             center_h = audioEndTime;
460       }
461 #endif
462 
463       wxCoord xTrackEnd = viewInfo.TimeToPosition( audioEndTime );
464       viewInfo.ZoomBy(pow(2.0, steps));
465 
466       double new_center_h = viewInfo.PositionToTime(xx, trackLeftEdge);
467       viewInfo.h += (center_h - new_center_h);
468 
469       // If wave has gone off screen, bring it back.
470       // This means that the end of the track stays where it was.
471       if( viewInfo.h > audioEndTime )
472          viewInfo.h += audioEndTime - viewInfo.PositionToTime( xTrackEnd );
473 
474 
475       result |= FixScrollbars;
476    }
477    else
478    {
479 #ifdef EXPERIMENTAL_SCRUBBING_SCROLL_WHEEL
480       if (scrubber.IsScrubbing()) {
481          scrubber.HandleScrollWheel(steps);
482          evt.event.Skip(false);
483       }
484       else
485 #endif
486       {
487          // MM: Scroll up/down when used without modifier keys
488          double lines = steps * 4 + mVertScrollRemainder;
489          mVertScrollRemainder = lines - floor(lines);
490          lines = floor(lines);
491          auto didSomething = window.TP_ScrollUpDown((int)-lines);
492          if (!didSomething)
493             result |= Cancelled;
494       }
495    }
496 
497    return result;
498 }
499 
500 } sMouseWheelHandler;
501 
502 AttachedWindows::RegisteredFactory sProjectWindowKey{
__anon01149d290202( ) 503    []( AudacityProject &parent ) -> wxWeakRef< wxWindow > {
504       wxRect wndRect;
505       bool bMaximized = false;
506       bool bIconized = false;
507       GetNextWindowPlacement(&wndRect, &bMaximized, &bIconized);
508 
509       auto pWindow = safenew ProjectWindow(
510          nullptr, -1,
511          wxDefaultPosition,
512          wxSize(wndRect.width, wndRect.height),
513          parent
514       );
515 
516       auto &window = *pWindow;
517       // wxGTK3 seems to need to require creating the window using default position
518       // and then manually positioning it.
519       window.SetPosition(wndRect.GetPosition());
520 
521       if(bMaximized) {
522          window.Maximize(true);
523       }
524       else if (bIconized) {
525          // if the user close down and iconized state we could start back up and iconized state
526          // window.Iconize(TRUE);
527       }
528 
529       return pWindow;
530    }
531 };
532 
533 }
534 
Get(AudacityProject & project)535 ProjectWindow &ProjectWindow::Get( AudacityProject &project )
536 {
537    return GetAttachedWindows(project).Get< ProjectWindow >(sProjectWindowKey);
538 }
539 
Get(const AudacityProject & project)540 const ProjectWindow &ProjectWindow::Get( const AudacityProject &project )
541 {
542    return Get( const_cast< AudacityProject & >( project ) );
543 }
544 
Find(AudacityProject * pProject)545 ProjectWindow *ProjectWindow::Find( AudacityProject *pProject )
546 {
547    return pProject
548       ? GetAttachedWindows(*pProject).Find< ProjectWindow >(sProjectWindowKey)
549       : nullptr;
550 }
551 
Find(const AudacityProject * pProject)552 const ProjectWindow *ProjectWindow::Find( const AudacityProject *pProject )
553 {
554    return Find( const_cast< AudacityProject * >( pProject ) );
555 }
556 
NextWindowID()557 int ProjectWindow::NextWindowID()
558 {
559    return mNextWindowID++;
560 }
561 
562 enum {
563    FirstID = 1000,
564 
565    // Window controls
566 
567    HSBarID,
568    VSBarID,
569 
570    NextID,
571 };
572 
573 //If you want any of these files, ask JKC.  They are not
574 //yet checked in to Audacity SVN as of 12-Feb-2010
575 #ifdef EXPERIMENTAL_NOTEBOOK
576    #include "GuiFactory.h"
577    #include "APanel.h"
578 #endif
579 
ProjectWindow(wxWindow * parent,wxWindowID id,const wxPoint & pos,const wxSize & size,AudacityProject & project)580 ProjectWindow::ProjectWindow(wxWindow * parent, wxWindowID id,
581                                  const wxPoint & pos,
582                                  const wxSize & size, AudacityProject &project)
583    : ProjectWindowBase{ parent, id, pos, size, project }
584 {
585    mNextWindowID = NextID;
586 
587    // Two sub-windows need to be made before Init(),
588    // so that this constructor can complete, and then TrackPanel and
589    // AdornedRulerPanel can retrieve those windows from this in their
590    // factory functions
591 
592    // PRL:  this panel groups the top tool dock and the ruler into one
593    // tab cycle.
594    // Must create it with non-default width equal to the main window width,
595    // or else the device toolbar doesn't make initial widths of the choice
596    // controls correct.
597    mTopPanel = safenew wxPanelWrapper {
598       this, wxID_ANY, wxDefaultPosition,
599       wxSize{ this->GetSize().GetWidth(), -1 }
600    };
601    mTopPanel->SetLabel( "Top Panel" );// Not localised
602    mTopPanel->SetLayoutDirection(wxLayout_LeftToRight);
603    mTopPanel->SetAutoLayout(true);
604 #ifdef EXPERIMENTAL_DA2
605    mTopPanel->SetBackgroundColour(theTheme.Colour( clrMedium ));
606 #endif
607 
608    wxWindow    * pPage;
609 
610 #ifdef EXPERIMENTAL_NOTEBOOK
611    // We are using a notebook (tabbed panel), so we create the notebook and add pages.
612    GuiFactory Factory;
613    wxNotebook  * pNotebook;
614    mMainPanel = Factory.AddPanel(
615       this, wxPoint( left, top ), wxSize( width, height ) );
616    pNotebook  = Factory.AddNotebook( mMainPanel );
617    /* i18n-hint: This is an experimental feature where the main panel in
618       Audacity is put on a notebook tab, and this is the name on that tab.
619       Other tabs in that notebook may have instruments, patch panels etc.*/
620    pPage = Factory.AddPage( pNotebook, _("Main Mix"));
621 #else
622    // Not using a notebook, so we place the track panel inside another panel,
623    // this keeps the notebook code and normal code consistent and also
624    // paves the way for adding additional windows inside the track panel.
625    mMainPanel = safenew wxPanelWrapper(this, -1,
626       wxDefaultPosition,
627       wxDefaultSize,
628       wxNO_BORDER);
629    mMainPanel->SetSizer( safenew wxBoxSizer(wxVERTICAL) );
630    mMainPanel->SetLabel("Main Panel");// Not localised.
631    pPage = mMainPanel;
632    // Set the colour here to the track panel background to avoid
633    // flicker when Audacity starts up.
634    // However, that leads to areas next to the horizontal scroller
635    // being painted in background colour and not scroller background
636    // colour, so suppress this for now.
637    //pPage->SetBackgroundColour( theTheme.Colour( clrDark ));
638 #endif
639    pPage->SetLayoutDirection(wxLayout_LeftToRight);
640 
641 #ifdef EXPERIMENTAL_DA2
642    pPage->SetBackgroundColour(theTheme.Colour( clrMedium ));
643 #endif
644 
645    mMainPage = pPage;
646 
647    mPlaybackScroller = std::make_unique<PlaybackScroller>( &project );
648 
649    // PRL: Old comments below.  No longer observing the ordering that it
650    //      recommends.  ProjectWindow::OnActivate puts the focus directly into
651    //      the TrackPanel, which avoids the problems.
652    // LLL: When Audacity starts or becomes active after returning from
653    //      another application, the first window that can accept focus
654    //      will be given the focus even if we try to SetFocus().  By
655    //      creating the scrollbars after the TrackPanel, we resolve
656    //      several focus problems.
657 
658    mHsbar = safenew ScrollBar(pPage, HSBarID, wxSB_HORIZONTAL);
659    mVsbar = safenew ScrollBar(pPage, VSBarID, wxSB_VERTICAL);
660 #if wxUSE_ACCESSIBILITY
661    // so that name can be set on a standard control
662    mHsbar->SetAccessible(safenew WindowAccessible(mHsbar));
663    mVsbar->SetAccessible(safenew WindowAccessible(mVsbar));
664 #endif
665    mHsbar->SetLayoutDirection(wxLayout_LeftToRight);
666    mHsbar->SetName(_("Horizontal Scrollbar"));
667    mVsbar->SetName(_("Vertical Scrollbar"));
668 
669    project.Bind( EVT_UNDO_MODIFIED, &ProjectWindow::OnUndoPushedModified, this );
670    project.Bind( EVT_UNDO_PUSHED, &ProjectWindow::OnUndoPushedModified, this );
671    project.Bind( EVT_UNDO_OR_REDO, &ProjectWindow::OnUndoRedo, this );
672    project.Bind( EVT_UNDO_RESET, &ProjectWindow::OnUndoReset, this );
673 
674    wxTheApp->Bind(EVT_THEME_CHANGE, &ProjectWindow::OnThemeChange, this);
675 }
676 
~ProjectWindow()677 ProjectWindow::~ProjectWindow()
678 {
679    // Tool manager gives us capture sometimes
680    if(HasCapture())
681       ReleaseMouse();
682 }
683 
BEGIN_EVENT_TABLE(ProjectWindow,wxFrame)684 BEGIN_EVENT_TABLE(ProjectWindow, wxFrame)
685    EVT_MENU(wxID_ANY, ProjectWindow::OnMenu)
686    EVT_MOUSE_EVENTS(ProjectWindow::OnMouseEvent)
687    EVT_CLOSE(ProjectWindow::OnCloseWindow)
688    EVT_SIZE(ProjectWindow::OnSize)
689    EVT_SHOW(ProjectWindow::OnShow)
690    EVT_ICONIZE(ProjectWindow::OnIconize)
691    EVT_MOVE(ProjectWindow::OnMove)
692    EVT_ACTIVATE(ProjectWindow::OnActivate)
693    EVT_COMMAND_SCROLL_LINEUP(HSBarID, ProjectWindow::OnScrollLeftButton)
694    EVT_COMMAND_SCROLL_LINEDOWN(HSBarID, ProjectWindow::OnScrollRightButton)
695    EVT_COMMAND_SCROLL(HSBarID, ProjectWindow::OnScroll)
696    EVT_COMMAND_SCROLL(VSBarID, ProjectWindow::OnScroll)
697    // Fires for menu with ID #1...first menu defined
698    EVT_UPDATE_UI(1, ProjectWindow::OnUpdateUI)
699    EVT_COMMAND(wxID_ANY, EVT_TOOLBAR_UPDATED, ProjectWindow::OnToolBarUpdate)
700    //mchinen:multithreaded calls - may not be threadsafe with CommandEvent: may have to change.
701 END_EVENT_TABLE()
702 
703 void ProjectWindow::ApplyUpdatedTheme()
704 {
705    auto &project = mProject;
706    SetBackgroundColour(theTheme.Colour( clrMedium ));
707    ClearBackground();// For wxGTK.
708 }
709 
RedrawProject(const bool bForceWaveTracks)710 void ProjectWindow::RedrawProject(const bool bForceWaveTracks /*= false*/)
711 {
712    auto pThis = wxWeakRef<ProjectWindow>(this);
713    CallAfter( [pThis, bForceWaveTracks]{
714 
715    if (!pThis)
716       return;
717    if (pThis->IsBeingDeleted())
718       return;
719 
720    auto &project = pThis->mProject ;
721    auto &tracks = TrackList::Get( project );
722    auto &trackPanel = GetProjectPanel( project );
723    pThis->FixScrollbars();
724    if (bForceWaveTracks)
725    {
726       for ( auto pWaveTrack : tracks.Any< WaveTrack >() )
727          for (const auto &clip: pWaveTrack->GetClips())
728             clip->MarkChanged();
729    }
730    trackPanel.Refresh(false);
731 
732    });
733 }
734 
OnThemeChange(wxCommandEvent & evt)735 void ProjectWindow::OnThemeChange(wxCommandEvent& evt)
736 {
737    evt.Skip();
738    auto &project = mProject;
739    this->ApplyUpdatedTheme();
740    auto &toolManager = ToolManager::Get( project );
741    for( int ii = 0; ii < ToolBarCount; ++ii )
742    {
743       ToolBar *pToolBar = toolManager.GetToolBar(ii);
744       if( pToolBar )
745          pToolBar->ReCreateButtons();
746    }
747 }
748 
UpdatePrefs()749 void ProjectWindow::UpdatePrefs()
750 {
751    // Update status bar widths in case of language change
752    UpdateStatusWidths();
753 }
754 
FinishAutoScroll()755 void ProjectWindow::FinishAutoScroll()
756 {
757    // Set a flag so we don't have to generate two update events
758    mAutoScrolling = true;
759 
760    // Call our Scroll method which updates our ViewInfo variables
761    // to reflect the positions of the scrollbars
762    DoScroll();
763 
764    mAutoScrolling = false;
765 }
766 
767 #if defined(__WXMAC__)
768 // const int sbarSpaceWidth = 15;
769 // const int sbarControlWidth = 16;
770 // const int sbarExtraLen = 1;
771 const int sbarHjump = 30;       //STM: This is how far the thumb jumps when the l/r buttons are pressed, or auto-scrolling occurs -- in pixels
772 #elif defined(__WXMSW__)
773 const int sbarSpaceWidth = 16;
774 const int sbarControlWidth = 16;
775 const int sbarExtraLen = 0;
776 const int sbarHjump = 30;       //STM: This is how far the thumb jumps when the l/r buttons are pressed, or auto-scrolling occurs -- in pixels
777 #else // wxGTK, wxMOTIF, wxX11
778 const int sbarSpaceWidth = 15;
779 const int sbarControlWidth = 15;
780 const int sbarExtraLen = 0;
781 const int sbarHjump = 30;       //STM: This is how far the thumb jumps when the l/r buttons are pressed, or auto-scrolling occurs -- in pixels
782 #include "AllThemeResources.h"
783 #endif
784 
785 // Make sure selection edge is in view
ScrollIntoView(double pos)786 void ProjectWindow::ScrollIntoView(double pos)
787 {
788    auto &trackPanel = GetProjectPanel( mProject );
789    auto &viewInfo = ViewInfo::Get( mProject );
790    auto w = viewInfo.GetTracksUsableWidth();
791 
792    int pixel = viewInfo.TimeToPosition(pos);
793    if (pixel < 0 || pixel >= w)
794    {
795       TP_ScrollWindow
796          (viewInfo.OffsetTimeByPixels(pos, -(w / 2)));
797       trackPanel.Refresh(false);
798    }
799 }
800 
ScrollIntoView(int x)801 void ProjectWindow::ScrollIntoView(int x)
802 {
803    auto &viewInfo = ViewInfo::Get( mProject );
804    ScrollIntoView(viewInfo.PositionToTime(x, viewInfo.GetLeftOffset()));
805 }
806 
807 ///
808 /// This method handles general left-scrolling, either for drag-scrolling
809 /// or when the scrollbar is clicked to the left of the thumb
810 ///
OnScrollLeft()811 void ProjectWindow::OnScrollLeft()
812 {
813    auto &project = mProject;
814    auto &viewInfo = ViewInfo::Get( project );
815    wxInt64 pos = mHsbar->GetThumbPosition();
816    // move at least one scroll increment
817    pos -= wxMax((wxInt64)(sbarHjump * viewInfo.sbarScale), 1);
818    pos = wxMax(pos, 0);
819    viewInfo.sbarH -= sbarHjump;
820    viewInfo.sbarH = std::max(viewInfo.sbarH,
821       -(wxInt64) PixelWidthBeforeTime(0.0));
822 
823 
824    if (pos != mHsbar->GetThumbPosition()) {
825       mHsbar->SetThumbPosition((int)pos);
826       FinishAutoScroll();
827    }
828 }
829 ///
830 /// This method handles general right-scrolling, either for drag-scrolling
831 /// or when the scrollbar is clicked to the right of the thumb
832 ///
833 
OnScrollRight()834 void ProjectWindow::OnScrollRight()
835 {
836    auto &project = mProject;
837    auto &viewInfo = ViewInfo::Get( project );
838    wxInt64 pos = mHsbar->GetThumbPosition();
839    // move at least one scroll increment
840    // use wxInt64 for calculation to prevent temporary overflow
841    pos += wxMax((wxInt64)(sbarHjump * viewInfo.sbarScale), 1);
842    wxInt64 max = mHsbar->GetRange() - mHsbar->GetThumbSize();
843    pos = wxMin(pos, max);
844    viewInfo.sbarH += sbarHjump;
845    viewInfo.sbarH = std::min(viewInfo.sbarH,
846       viewInfo.sbarTotal
847          - (wxInt64) PixelWidthBeforeTime(0.0) - viewInfo.sbarScreen);
848 
849    if (pos != mHsbar->GetThumbPosition()) {
850       mHsbar->SetThumbPosition((int)pos);
851       FinishAutoScroll();
852    }
853 }
854 
855 
856 ///
857 ///  This handles the event when the left direction button on the scrollbar is depressed
858 ///
OnScrollLeftButton(wxScrollEvent &)859 void ProjectWindow::OnScrollLeftButton(wxScrollEvent & /*event*/)
860 {
861    auto &project = mProject;
862    auto &viewInfo = ViewInfo::Get( project );
863    wxInt64 pos = mHsbar->GetThumbPosition();
864    // move at least one scroll increment
865    pos -= wxMax((wxInt64)(sbarHjump * viewInfo.sbarScale), 1);
866    pos = wxMax(pos, 0);
867    viewInfo.sbarH -= sbarHjump;
868    viewInfo.sbarH = std::max(viewInfo.sbarH,
869       - (wxInt64) PixelWidthBeforeTime(0.0));
870 
871    if (pos != mHsbar->GetThumbPosition()) {
872       mHsbar->SetThumbPosition((int)pos);
873       DoScroll();
874    }
875 }
876 
877 ///
878 ///  This handles  the event when the right direction button on the scrollbar is depressed
879 ///
OnScrollRightButton(wxScrollEvent &)880 void ProjectWindow::OnScrollRightButton(wxScrollEvent & /*event*/)
881 {
882    auto &project = mProject;
883    auto &viewInfo = ViewInfo::Get( project );
884    wxInt64 pos = mHsbar->GetThumbPosition();
885    // move at least one scroll increment
886    // use wxInt64 for calculation to prevent temporary overflow
887    pos += wxMax((wxInt64)(sbarHjump * viewInfo.sbarScale), 1);
888    wxInt64 max = mHsbar->GetRange() - mHsbar->GetThumbSize();
889    pos = wxMin(pos, max);
890    viewInfo.sbarH += sbarHjump;
891    viewInfo.sbarH = std::min(viewInfo.sbarH,
892       viewInfo.sbarTotal
893          - (wxInt64) PixelWidthBeforeTime(0.0) - viewInfo.sbarScreen);
894 
895    if (pos != mHsbar->GetThumbPosition()) {
896       mHsbar->SetThumbPosition((int)pos);
897       DoScroll();
898    }
899 }
900 
901 
MayScrollBeyondZero() const902 bool ProjectWindow::MayScrollBeyondZero() const
903 {
904    auto &project = mProject;
905    auto &scrubber = Scrubber::Get( project );
906    auto &viewInfo = ViewInfo::Get( project );
907    if (viewInfo.bScrollBeyondZero)
908       return true;
909 
910    if (scrubber.HasMark() ||
911        ProjectAudioIO::Get( project ).IsAudioActive()) {
912       if (mPlaybackScroller) {
913          auto mode = mPlaybackScroller->GetMode();
914          if (mode == PlaybackScroller::Mode::Pinned ||
915              mode == PlaybackScroller::Mode::Right)
916             return true;
917       }
918    }
919 
920    return false;
921 }
922 
ScrollingLowerBoundTime() const923 double ProjectWindow::ScrollingLowerBoundTime() const
924 {
925    auto &project = mProject;
926    auto &tracks = TrackList::Get( project );
927    auto &viewInfo = ViewInfo::Get( project );
928    if (!MayScrollBeyondZero())
929       return 0;
930    const double screen = viewInfo.GetScreenEndTime() - viewInfo.h;
931    return std::min(tracks.GetStartTime(), -screen);
932 }
933 
934 // PRL: Bug1197: we seem to need to compute all in double, to avoid differing results on Mac
935 // That's why ViewInfo::TimeRangeToPixelWidth was defined, with some regret.
PixelWidthBeforeTime(double scrollto) const936 double ProjectWindow::PixelWidthBeforeTime(double scrollto) const
937 {
938    auto &project = mProject;
939    auto &viewInfo = ViewInfo::Get( project );
940    const double lowerBound = ScrollingLowerBoundTime();
941    return
942       // Ignoring fisheye is correct here
943       viewInfo.TimeRangeToPixelWidth(scrollto - lowerBound);
944 }
945 
SetHorizontalThumb(double scrollto)946 void ProjectWindow::SetHorizontalThumb(double scrollto)
947 {
948    auto &project = mProject;
949    auto &viewInfo = ViewInfo::Get( project );
950    const auto unscaled = PixelWidthBeforeTime(scrollto);
951    const int max = mHsbar->GetRange() - mHsbar->GetThumbSize();
952    const int pos =
953       std::min(max,
954          std::max(0,
955             (int)(floor(0.5 + unscaled * viewInfo.sbarScale))));
956    mHsbar->SetThumbPosition(pos);
957    viewInfo.sbarH = floor(0.5 + unscaled - PixelWidthBeforeTime(0.0));
958    viewInfo.sbarH = std::max(viewInfo.sbarH,
959       - (wxInt64) PixelWidthBeforeTime(0.0));
960    viewInfo.sbarH = std::min(viewInfo.sbarH,
961       viewInfo.sbarTotal
962          - (wxInt64) PixelWidthBeforeTime(0.0) - viewInfo.sbarScreen);
963 }
964 
965 //
966 // This method, like the other methods prefaced with TP, handles TrackPanel
967 // 'callback'.
968 //
TP_ScrollWindow(double scrollto)969 void ProjectWindow::TP_ScrollWindow(double scrollto)
970 {
971    SetHorizontalThumb(scrollto);
972 
973    // Call our Scroll method which updates our ViewInfo variables
974    // to reflect the positions of the scrollbars
975    DoScroll();
976 }
977 
978 //
979 // Scroll vertically. This is called for example by the mouse wheel
980 // handler in Track Panel. A positive argument makes the window
981 // scroll down, while a negative argument scrolls up.
982 //
TP_ScrollUpDown(int delta)983 bool ProjectWindow::TP_ScrollUpDown(int delta)
984 {
985    int oldPos = mVsbar->GetThumbPosition();
986    int pos = oldPos + delta;
987    int max = mVsbar->GetRange() - mVsbar->GetThumbSize();
988 
989    // Can be negative in case of only one track
990    if (max < 0)
991       max = 0;
992 
993    if (pos > max)
994       pos = max;
995    else if (pos < 0)
996       pos = 0;
997 
998    if (pos != oldPos)
999    {
1000       mVsbar->SetThumbPosition(pos);
1001 
1002       DoScroll();
1003       return true;
1004    }
1005    else
1006       return false;
1007 }
1008 
FixScrollbars()1009 void ProjectWindow::FixScrollbars()
1010 {
1011    auto &project = mProject;
1012    auto &tracks = TrackList::Get( project );
1013    auto &trackPanel = GetProjectPanel( project );
1014    auto &viewInfo = ViewInfo::Get( project );
1015 
1016    bool refresh = false;
1017    bool rescroll = false;
1018 
1019    int totalHeight = TrackView::GetTotalHeight( tracks ) + 32;
1020 
1021    auto panelWidth = viewInfo.GetTracksUsableWidth();
1022    auto panelHeight = viewInfo.GetHeight();
1023 
1024    // (From Debian...at least I think this is the change corresponding
1025    // to this comment)
1026    //
1027    // (2.) GTK critical warning "IA__gtk_range_set_range: assertion
1028    // 'min < max' failed" because of negative numbers as result of window
1029    // size checking. Added a sanity check that straightens up the numbers
1030    // in edge cases.
1031    if (panelWidth < 0) {
1032       panelWidth = 0;
1033    }
1034    if (panelHeight < 0) {
1035       panelHeight = 0;
1036    }
1037 
1038    auto LastTime = std::numeric_limits<double>::lowest();
1039    for (const Track *track : tracks) {
1040       // Iterate over pending changed tracks if present.
1041       track = track->SubstitutePendingChangedTrack().get();
1042       LastTime = std::max( LastTime, track->GetEndTime() );
1043    }
1044    LastTime =
1045       std::max(LastTime, viewInfo.selectedRegion.t1());
1046 
1047    const double screen =
1048       viewInfo.GetScreenEndTime() - viewInfo.h;
1049    const double halfScreen = screen / 2.0;
1050 
1051    // If we can scroll beyond zero,
1052    // Add 1/2 of a screen of blank space to the end
1053    // and another 1/2 screen before the beginning
1054    // so that any point within the union of the selection and the track duration
1055    // may be scrolled to the midline.
1056    // May add even more to the end, so that you can always scroll the starting time to zero.
1057    const double lowerBound = ScrollingLowerBoundTime();
1058    const double additional = MayScrollBeyondZero()
1059       ? -lowerBound + std::max(halfScreen, screen - LastTime)
1060       : screen / 4.0;
1061 
1062    viewInfo.total = LastTime + additional;
1063 
1064    // Don't remove time from total that's still on the screen
1065    viewInfo.total = std::max(viewInfo.total, viewInfo.h + screen);
1066 
1067    if (viewInfo.h < lowerBound) {
1068       viewInfo.h = lowerBound;
1069       rescroll = true;
1070    }
1071 
1072    viewInfo.sbarTotal = (wxInt64) (viewInfo.GetTotalWidth());
1073    viewInfo.sbarScreen = (wxInt64)(panelWidth);
1074    viewInfo.sbarH = (wxInt64) (viewInfo.GetBeforeScreenWidth());
1075 
1076    // PRL:  Can someone else find a more elegant solution to bug 812, than
1077    // introducing this boolean member variable?
1078    // Setting mVSbar earlier, int HandlXMLTag, didn't succeed in restoring
1079    // the vertical scrollbar to its saved position.  So defer that till now.
1080    // mbInitializingScrollbar should be true only at the start of the life
1081    // of an AudacityProject reopened from disk.
1082    if (!mbInitializingScrollbar) {
1083       viewInfo.vpos = mVsbar->GetThumbPosition() * viewInfo.scrollStep;
1084    }
1085    mbInitializingScrollbar = false;
1086 
1087    if (viewInfo.vpos >= totalHeight)
1088       viewInfo.vpos = totalHeight - 1;
1089    if (viewInfo.vpos < 0)
1090       viewInfo.vpos = 0;
1091 
1092    bool oldhstate;
1093    bool oldvstate;
1094    bool newhstate =
1095       (viewInfo.GetScreenEndTime() - viewInfo.h) < viewInfo.total;
1096    bool newvstate = panelHeight < totalHeight;
1097 
1098 #ifdef __WXGTK__
1099    oldhstate = mHsbar->IsShown();
1100    oldvstate = mVsbar->IsShown();
1101    mHsbar->Show(newhstate);
1102    mVsbar->Show(panelHeight < totalHeight);
1103 #else
1104    oldhstate = mHsbar->IsEnabled();
1105    oldvstate = mVsbar->IsEnabled();
1106    mHsbar->Enable(newhstate);
1107    mVsbar->Enable(panelHeight < totalHeight);
1108 #endif
1109 
1110    if (panelHeight >= totalHeight && viewInfo.vpos != 0) {
1111       viewInfo.vpos = 0;
1112 
1113       refresh = true;
1114       rescroll = false;
1115    }
1116    if (!newhstate && viewInfo.sbarH != 0) {
1117       viewInfo.sbarH = 0;
1118 
1119       refresh = true;
1120       rescroll = false;
1121    }
1122 
1123    // wxScrollbar only supports int values but we need a greater range, so
1124    // we scale the scrollbar coordinates on demand. We only do this if we
1125    // would exceed the int range, so we can always use the maximum resolution
1126    // available.
1127 
1128    // Don't use the full 2^31 max int range but a bit less, so rounding
1129    // errors in calculations do not overflow max int
1130    wxInt64 maxScrollbarRange = (wxInt64)(2147483647 * 0.999);
1131    if (viewInfo.sbarTotal > maxScrollbarRange)
1132       viewInfo.sbarScale = ((double)maxScrollbarRange) / viewInfo.sbarTotal;
1133    else
1134       viewInfo.sbarScale = 1.0; // use maximum resolution
1135 
1136    {
1137       int scaledSbarH = (int)(viewInfo.sbarH * viewInfo.sbarScale);
1138       int scaledSbarScreen = (int)(viewInfo.sbarScreen * viewInfo.sbarScale);
1139       int scaledSbarTotal = (int)(viewInfo.sbarTotal * viewInfo.sbarScale);
1140       const int offset =
1141          (int)(floor(0.5 + viewInfo.sbarScale * PixelWidthBeforeTime(0.0)));
1142 
1143       mHsbar->SetScrollbar(scaledSbarH + offset, scaledSbarScreen, scaledSbarTotal,
1144          scaledSbarScreen, TRUE);
1145    }
1146 
1147    // Vertical scrollbar
1148    mVsbar->SetScrollbar(viewInfo.vpos / viewInfo.scrollStep,
1149                         panelHeight / viewInfo.scrollStep,
1150                         totalHeight / viewInfo.scrollStep,
1151                         panelHeight / viewInfo.scrollStep, TRUE);
1152 
1153    if (refresh || (rescroll &&
1154        (viewInfo.GetScreenEndTime() - viewInfo.h) < viewInfo.total)) {
1155       trackPanel.Refresh(false);
1156    }
1157 
1158    MenuManager::Get( project ).UpdateMenus();
1159 
1160    if (oldhstate != newhstate || oldvstate != newvstate) {
1161       UpdateLayout();
1162    }
1163 }
1164 
UpdateLayout()1165 void ProjectWindow::UpdateLayout()
1166 {
1167    auto &project = mProject;
1168    auto &trackPanel = GetProjectPanel( project );
1169    auto &toolManager = ToolManager::Get( project );
1170 
1171    // 1. Layout panel, to get widths of the docks.
1172    Layout();
1173    // 2. Layout toolbars to pack the toolbars correctly in docks which
1174    // are now the correct width.
1175    toolManager.LayoutToolBars();
1176    // 3. Layout panel, to resize docks, in particular reducing the height
1177    // of any empty docks, or increasing the height of docks that need it.
1178    Layout();
1179 
1180    // Bug 2455
1181    // The commented out code below is to calculate a nice minimum size for
1182    // the window.  However on Ubuntu when the window is minimised it leads to
1183    // an insanely tall window.
1184    // Using a fixed min size fixes that.
1185    // However there is still something strange when minimised, as once
1186    // UpdateLayout is called once, when minimised, it gets called repeatedly.
1187 #if 0
1188    // Retrieve size of this projects window
1189    wxSize mainsz = GetSize();
1190 
1191    // Retrieve position of the track panel to use as the size of the top
1192    // third of the window
1193    wxPoint tppos = ClientToScreen(trackPanel.GetParent()->GetPosition());
1194 
1195    // Retrieve position of bottom dock to use as the size of the bottom
1196    // third of the window
1197    wxPoint sbpos = ClientToScreen(toolManager.GetBotDock()->GetPosition());
1198 
1199    // The "+ 50" is the minimum height of the TrackPanel
1200    SetMinSize( wxSize(250, (mainsz.y - sbpos.y) + tppos.y + 50));
1201 #endif
1202    SetMinSize( wxSize(250, 250));
1203    SetMaxSize( wxSize(20000, 20000));
1204 }
1205 
HandleResize()1206 void ProjectWindow::HandleResize()
1207 {
1208    // Activate events can fire during window teardown, so just
1209    // ignore them.
1210    if (mIsDeleting) {
1211       return;
1212    }
1213 
1214    CallAfter( [this]{
1215 
1216    if (mIsDeleting)
1217       return;
1218 
1219    FixScrollbars();
1220    UpdateLayout();
1221 
1222    });
1223 }
1224 
1225 
IsIconized() const1226 bool ProjectWindow::IsIconized() const
1227 {
1228    return mIconized;
1229 }
1230 
UpdateStatusWidths()1231 void ProjectWindow::UpdateStatusWidths()
1232 {
1233    enum { nWidths = nStatusBarFields + 1 };
1234    int widths[ nWidths ]{ 0 };
1235    widths[ rateStatusBarField ] = 150;
1236    const auto statusBar = GetStatusBar();
1237    const auto &functions = ProjectStatus::GetStatusWidthFunctions();
1238    // Start from 1 not 0
1239    // Specifying a first column always of width 0 was needed for reasons
1240    // I forget now
1241    for ( int ii = 1; ii <= nStatusBarFields; ++ii ) {
1242       int &width = widths[ ii ];
1243       for ( const auto &function : functions ) {
1244          auto results =
1245             function( mProject, static_cast< StatusBarField >( ii ) );
1246          for ( const auto &string : results.first ) {
1247             int w;
1248             statusBar->GetTextExtent(string.Translation(), &w, nullptr);
1249             width = std::max<int>( width, w + results.second );
1250          }
1251       }
1252    }
1253    // The main status field is not fixed width
1254    widths[ mainStatusBarField ] = -1;
1255    statusBar->SetStatusWidths( nWidths, widths );
1256 }
1257 
MacShowUndockedToolbars(bool show)1258 void ProjectWindow::MacShowUndockedToolbars(bool show)
1259 {
1260    (void)show;//compiler food
1261 #ifdef __WXMAC__
1262    // Save the focus so we can restore it to whatever had it before since
1263    // showing a previously hidden toolbar will cause the focus to be set to
1264    // its frame.  If this is not done it will appear that activation events
1265    // aren't being sent to the project window since they are actually being
1266    // delivered to the last tool frame shown.
1267    wxWindow *focused = FindFocus();
1268 
1269    // Find all the floating toolbars, and show or hide them
1270    const auto &children = GetChildren();
1271    for(const auto &child : children) {
1272       if (auto frame = dynamic_cast<ToolFrame*>(child)) {
1273          if (!show) {
1274             frame->Hide();
1275          }
1276          else if (frame->GetBar() &&
1277                   frame->GetBar()->IsVisible() ) {
1278             frame->Show();
1279          }
1280       }
1281    }
1282 
1283    // Restore the focus if needed
1284    if (focused) {
1285       focused->SetFocus();
1286    }
1287 #endif
1288 }
1289 
OnIconize(wxIconizeEvent & event)1290 void ProjectWindow::OnIconize(wxIconizeEvent &event)
1291 {
1292    //JKC: On Iconizing we get called twice.  Don't know
1293    // why but it does no harm.
1294    // Should we be returning true/false rather than
1295    // void return?  I don't know.
1296    mIconized = event.IsIconized();
1297 
1298 #if defined(__WXMAC__)
1299    // Readdresses bug 1431 since a crash could occur when restoring iconized
1300    // floating toolbars due to recursion (bug 2411).
1301    MacShowUndockedToolbars(!mIconized);
1302    if( !mIconized )
1303    {
1304       Raise();
1305    }
1306 #endif
1307 
1308    // VisibileProjectCount seems to be just a counter for debugging.
1309    // It's not used outside this function.
1310    auto VisibleProjectCount = std::count_if(
1311       AllProjects{}.begin(), AllProjects{}.end(),
1312       []( const AllProjects::value_type &ptr ){
1313          return !GetProjectFrame( *ptr ).IsIconized();
1314       }
1315    );
1316    event.Skip();
1317 
1318    // This step is to fix part of Bug 2040, where the BackingPanel
1319    // size was not restored after we leave Iconized state.
1320 
1321    // Queue up a resize event using OnShow so that we
1322    // refresh the track panel.  But skip this, if we're iconized.
1323    if( mIconized )
1324       return;
1325    wxShowEvent Evt;
1326    OnShow( Evt );
1327 }
1328 
OnMove(wxMoveEvent & event)1329 void ProjectWindow::OnMove(wxMoveEvent & event)
1330 {
1331    if (!this->IsMaximized() && !this->IsIconized())
1332       SetNormalizedWindowState(this->GetRect());
1333    event.Skip();
1334 }
1335 
OnSize(wxSizeEvent & event)1336 void ProjectWindow::OnSize(wxSizeEvent & event)
1337 {
1338    // (From Debian)
1339    //
1340    // (3.) GTK critical warning "IA__gdk_window_get_origin: assertion
1341    // 'GDK_IS_WINDOW (window)' failed": Received events of type wxSizeEvent
1342    // on the main project window cause calls to "ClientToScreen" - which is
1343    // not available until the window is first shown. So the class has to
1344    // keep track of wxShowEvent events and inhibit those actions until the
1345    // window is first shown.
1346    if (mShownOnce) {
1347       HandleResize();
1348       if (!this->IsMaximized() && !this->IsIconized())
1349          SetNormalizedWindowState(this->GetRect());
1350    }
1351    event.Skip();
1352 }
1353 
OnShow(wxShowEvent & event)1354 void ProjectWindow::OnShow(wxShowEvent & event)
1355 {
1356    // Remember that the window has been shown at least once
1357    mShownOnce = true;
1358 
1359    // (From Debian...see also TrackPanel::OnTimer and AudacityTimer::Notify)
1360    //
1361    // Description: Workaround for wxWidgets bug: Reentry in clipboard
1362    //  The wxWidgets bug http://trac.wxwidgets.org/ticket/16636 prevents
1363    //  us from doing clipboard operations in wxShowEvent and wxTimerEvent
1364    //  processing because those event could possibly be processed during
1365    //  the (not sufficiently protected) Yield() of a first clipboard
1366    //  operation, causing reentry. Audacity had a workaround in place
1367    //  for this problem (the class "CaptureEvents"), which however isn't
1368    //  applicable with wxWidgets 3.0 because it's based on changing the
1369    //  gdk event handler, a change that would be overridden by wxWidgets's
1370    //  own gdk event handler change.
1371    //  Instead, as a NEW workaround, specifically protect those processings
1372    //  of wxShowEvent and wxTimerEvent that try to do clipboard operations
1373    //  from being executed within Yield(). This is done by delaying their
1374    //  execution by posting pure wxWidgets events - which are never executed
1375    //  during Yield().
1376    // Author: Martin Stegh  fer <martin@steghoefer.eu>
1377    //  Bug-Debian: https://bugs.debian.org/765341
1378 
1379    // the actual creation/showing of the window).
1380    // Post the event instead of calling OnSize(..) directly. This ensures that
1381    // this is a pure wxWidgets event (no GDK event behind it) and that it
1382    // therefore isn't processed within the YieldFor(..) of the clipboard
1383    // operations (workaround for Debian bug #765341).
1384    // QueueEvent() will take ownership of the event
1385    GetEventHandler()->QueueEvent(safenew wxSizeEvent(GetSize()));
1386 
1387    // Further processing by default handlers
1388    event.Skip();
1389 }
1390 
1391 ///
1392 ///  A toolbar has been updated, so handle it like a sizing event.
1393 ///
OnToolBarUpdate(wxCommandEvent & event)1394 void ProjectWindow::OnToolBarUpdate(wxCommandEvent & event)
1395 {
1396    HandleResize();
1397 
1398    event.Skip(false);             /* No need to propagate any further */
1399 }
1400 
OnUndoPushedModified(wxCommandEvent & evt)1401 void ProjectWindow::OnUndoPushedModified( wxCommandEvent &evt )
1402 {
1403    evt.Skip();
1404    RedrawProject();
1405 }
1406 
OnUndoRedo(wxCommandEvent & evt)1407 void ProjectWindow::OnUndoRedo( wxCommandEvent &evt )
1408 {
1409    evt.Skip();
1410    HandleResize();
1411    RedrawProject();
1412 }
1413 
OnUndoReset(wxCommandEvent & evt)1414 void ProjectWindow::OnUndoReset( wxCommandEvent &evt )
1415 {
1416    evt.Skip();
1417    HandleResize();
1418    // RedrawProject();  // Should we do this here too?
1419 }
1420 
OnScroll(wxScrollEvent & WXUNUSED (event))1421 void ProjectWindow::OnScroll(wxScrollEvent & WXUNUSED(event))
1422 {
1423    auto &project = mProject;
1424    auto &viewInfo = ViewInfo::Get( project );
1425    const wxInt64 offset = PixelWidthBeforeTime(0.0);
1426    viewInfo.sbarH =
1427       (wxInt64)(mHsbar->GetThumbPosition() / viewInfo.sbarScale) - offset;
1428    DoScroll();
1429 
1430 #ifndef __WXMAC__
1431    // Bug2179
1432    // This keeps the time ruler in sync with horizontal scrolling, without
1433    // making an undesirable compilation dependency of this source file on
1434    // the ruler
1435    wxTheApp->ProcessIdle();
1436 #endif
1437 }
1438 
DoScroll()1439 void ProjectWindow::DoScroll()
1440 {
1441    auto &project = mProject;
1442    auto &trackPanel = GetProjectPanel( project );
1443    auto &viewInfo = ViewInfo::Get( project );
1444    const double lowerBound = ScrollingLowerBoundTime();
1445 
1446    auto width = viewInfo.GetTracksUsableWidth();
1447    viewInfo.SetBeforeScreenWidth(viewInfo.sbarH, width, lowerBound);
1448 
1449 
1450    if (MayScrollBeyondZero()) {
1451       enum { SCROLL_PIXEL_TOLERANCE = 10 };
1452       if (std::abs(viewInfo.TimeToPosition(0.0, 0
1453                                    )) < SCROLL_PIXEL_TOLERANCE) {
1454          // Snap the scrollbar to 0
1455          viewInfo.h = 0;
1456          SetHorizontalThumb(0.0);
1457       }
1458    }
1459 
1460    viewInfo.vpos = mVsbar->GetThumbPosition() * viewInfo.scrollStep;
1461 
1462    //mchinen: do not always set this project to be the active one.
1463    //a project may autoscroll while playing in the background
1464    //I think this is okay since OnMouseEvent has one of these.
1465    //SetActiveProject(this);
1466 
1467    if (!mAutoScrolling) {
1468       trackPanel.Refresh(false);
1469    }
1470 }
1471 
OnMenu(wxCommandEvent & event)1472 void ProjectWindow::OnMenu(wxCommandEvent & event)
1473 {
1474 #ifdef __WXMSW__
1475    // Bug 1642: We can arrive here with bogus menu IDs, which we
1476    // proceed to process.  So if bogus, don't.
1477    // The bogus menu IDs are probably generated by controls on the TrackPanel,
1478    // such as the Project Rate.
1479    // 17000 is the magic number at which we start our menu.
1480    // This code would probably NOT be OK on Mac, since we assign
1481    // some specific ID numbers.
1482    if( event.GetId() < 17000){
1483       event.Skip();
1484       return;
1485    }
1486 #endif
1487    auto &project = mProject;
1488    auto &commandManager = CommandManager::Get( project );
1489    bool handled = commandManager.HandleMenuID( GetProject(),
1490       event.GetId(), MenuManager::Get( project ).GetUpdateFlags(),
1491       false);
1492 
1493    if (handled)
1494       event.Skip(false);
1495    else{
1496       event.ResumePropagation( 999 );
1497       event.Skip(true);
1498    }
1499 }
1500 
OnUpdateUI(wxUpdateUIEvent & WXUNUSED (event))1501 void ProjectWindow::OnUpdateUI(wxUpdateUIEvent & WXUNUSED(event))
1502 {
1503    auto &project = mProject;
1504    MenuManager::Get( project ).UpdateMenus();
1505 }
1506 
OnActivate(wxActivateEvent & event)1507 void ProjectWindow::OnActivate(wxActivateEvent & event)
1508 {
1509    // Activate events can fire during window teardown, so just
1510    // ignore them.
1511    if (IsBeingDeleted()) {
1512       return;
1513    }
1514 
1515    auto &project = mProject;
1516 
1517    mActive = event.GetActive();
1518 
1519    // Under Windows, focus can be "lost" when returning to
1520    // Audacity from a different application.
1521    //
1522    // This was observed by minimizing all windows using WINDOWS+M and
1523    // then ALT+TAB to return to Audacity.  Focus will be given to the
1524    // project window frame which is not at all useful.
1525    //
1526    // So, we use ToolManager's observation of focus changes in a wxEventFilter.
1527    // Then, when we receive the
1528    // activate event, we restore that focus to the child or the track
1529    // panel if no child had the focus (which probably should never happen).
1530    if (mActive) {
1531       auto &toolManager = ToolManager::Get( project );
1532       SetActiveProject( &project );
1533       if ( ! toolManager.RestoreFocus() )
1534          GetProjectPanel( project ).SetFocus();
1535    }
1536    event.Skip();
1537 }
1538 
IsActive()1539 bool ProjectWindow::IsActive()
1540 {
1541    return mActive;
1542 }
1543 
OnMouseEvent(wxMouseEvent & event)1544 void ProjectWindow::OnMouseEvent(wxMouseEvent & event)
1545 {
1546    auto &project = mProject;
1547    if (event.ButtonDown())
1548       SetActiveProject( &project );
1549 }
1550 
ZoomAfterImport(Track * pTrack)1551 void ProjectWindow::ZoomAfterImport(Track *pTrack)
1552 {
1553    auto &project = mProject;
1554    auto &tracks = TrackList::Get( project );
1555    auto &trackPanel = GetProjectPanel( project );
1556 
1557    DoZoomFit();
1558 
1559    trackPanel.SetFocus();
1560    if (!pTrack)
1561       pTrack = *tracks.Selected().begin();
1562    if (!pTrack)
1563       pTrack = *tracks.Any().begin();
1564    if (pTrack) {
1565       TrackFocus::Get(project).Set(pTrack);
1566       pTrack->EnsureVisible();
1567    }
1568 }
1569 
1570 // Utility function called by other zoom methods
Zoom(double level)1571 void ProjectWindow::Zoom(double level)
1572 {
1573    auto &project = mProject;
1574    auto &viewInfo = ViewInfo::Get( project );
1575    viewInfo.SetZoom(level);
1576    FixScrollbars();
1577    // See if we can center the selection on screen, and have it actually fit.
1578    // tOnLeft is the amount of time we would need before the selection left edge to center it.
1579    float t0 = viewInfo.selectedRegion.t0();
1580    float t1 = viewInfo.selectedRegion.t1();
1581    float tAvailable = viewInfo.GetScreenEndTime() - viewInfo.h;
1582    float tOnLeft = (tAvailable - t0 + t1)/2.0;
1583    // Bug 1292 (Enh) is effectively a request to do this scrolling of  the selection into view.
1584    // If tOnLeft is positive, then we have room for the selection, so scroll to it.
1585    if( tOnLeft >=0 )
1586       TP_ScrollWindow( t0-tOnLeft);
1587 }
1588 
1589 // Utility function called by other zoom methods
ZoomBy(double multiplier)1590 void ProjectWindow::ZoomBy(double multiplier)
1591 {
1592    auto &project = mProject;
1593    auto &viewInfo = ViewInfo::Get( project );
1594    viewInfo.ZoomBy(multiplier);
1595    FixScrollbars();
1596 }
1597 
1598 ///////////////////////////////////////////////////////////////////
1599 // This method 'rewinds' the track, by setting the cursor to 0 and
1600 // scrolling the window to fit 0 on the left side of it
1601 // (maintaining  current zoom).
1602 // If shift is held down, it will extend the left edge of the
1603 // selection to 0 (holding right edge constant), otherwise it will
1604 // move both left and right edge of selection to 0
1605 ///////////////////////////////////////////////////////////////////
Rewind(bool shift)1606 void ProjectWindow::Rewind(bool shift)
1607 {
1608    auto &project = mProject;
1609    auto &viewInfo = ViewInfo::Get( project );
1610    viewInfo.selectedRegion.setT0(0, false);
1611    if (!shift)
1612       viewInfo.selectedRegion.setT1(0);
1613 
1614    TP_ScrollWindow(0);
1615 }
1616 
1617 
1618 ///////////////////////////////////////////////////////////////////
1619 // This method 'fast-forwards' the track, by setting the cursor to
1620 // the end of the samples on the selected track and  scrolling the
1621 //  window to fit the end on its right side (maintaining  current zoom).
1622 // If shift is held down, it will extend the right edge of the
1623 // selection to the end (holding left edge constant), otherwise it will
1624 // move both left and right edge of selection to the end
1625 ///////////////////////////////////////////////////////////////////
SkipEnd(bool shift)1626 void ProjectWindow::SkipEnd(bool shift)
1627 {
1628    auto &project = mProject;
1629    auto &tracks = TrackList::Get( project );
1630    auto &viewInfo = ViewInfo::Get( project );
1631    double len = tracks.GetEndTime();
1632 
1633    viewInfo.selectedRegion.setT1(len, false);
1634    if (!shift)
1635       viewInfo.selectedRegion.setT0(len);
1636 
1637    // Make sure the end of the track is visible
1638    ScrollIntoView(len);
1639 }
1640 
1641 // TrackPanel callback method
TP_ScrollLeft()1642 void ProjectWindow::TP_ScrollLeft()
1643 {
1644    OnScrollLeft();
1645 }
1646 
1647 // TrackPanel callback method
TP_ScrollRight()1648 void ProjectWindow::TP_ScrollRight()
1649 {
1650    OnScrollRight();
1651 }
1652 
1653 // TrackPanel callback method
TP_RedrawScrollbars()1654 void ProjectWindow::TP_RedrawScrollbars()
1655 {
1656    FixScrollbars();
1657 }
1658 
TP_HandleResize()1659 void ProjectWindow::TP_HandleResize()
1660 {
1661    HandleResize();
1662 }
1663 
PlaybackScroller(AudacityProject * project)1664 ProjectWindow::PlaybackScroller::PlaybackScroller(AudacityProject *project)
1665 : mProject(project)
1666 {
1667    mProject->Bind(EVT_TRACK_PANEL_TIMER,
1668       &PlaybackScroller::OnTimer,
1669       this);
1670 }
1671 
OnTimer(wxCommandEvent & event)1672 void ProjectWindow::PlaybackScroller::OnTimer(wxCommandEvent &event)
1673 {
1674    // Let other listeners get the notification
1675    event.Skip();
1676 
1677    auto gAudioIO = AudioIO::Get();
1678    mRecentStreamTime = gAudioIO->GetStreamTime();
1679 
1680    auto cleanup = finally([&]{
1681       // Propagate the message to other listeners bound to this
1682       this->SafelyProcessEvent( event );
1683    });
1684 
1685    if(!ProjectAudioIO::Get( *mProject ).IsAudioActive())
1686       return;
1687    else if (mMode == Mode::Refresh) {
1688       // PRL:  see comments in Scrubbing.cpp for why this is sometimes needed.
1689       // These unnecessary refreshes cause wheel rotation events to be delivered more uniformly
1690       // to the application, so scrub speed control is smoother.
1691       // (So I see at least with OS 10.10 and wxWidgets 3.0.2.)
1692       // Is there another way to ensure that than by refreshing?
1693       auto &trackPanel = GetProjectPanel( *mProject );
1694       trackPanel.Refresh(false);
1695    }
1696    else if (mMode != Mode::Off) {
1697       // Pan the view, so that we put the play indicator at some fixed
1698       // fraction of the window width.
1699 
1700       auto &viewInfo = ViewInfo::Get( *mProject );
1701       auto &trackPanel = GetProjectPanel( *mProject );
1702       const int posX = viewInfo.TimeToPosition(mRecentStreamTime);
1703       auto width = viewInfo.GetTracksUsableWidth();
1704       int deltaX;
1705       switch (mMode)
1706       {
1707          default:
1708             wxASSERT(false);
1709             /* fallthru */
1710          case Mode::Pinned:
1711             deltaX =
1712                posX - width * TracksPrefs::GetPinnedHeadPositionPreference();
1713             break;
1714          case Mode::Right:
1715             deltaX = posX - width;        break;
1716       }
1717       viewInfo.h =
1718          viewInfo.OffsetTimeByPixels(viewInfo.h, deltaX, true);
1719       if (!ProjectWindow::Get( *mProject ).MayScrollBeyondZero())
1720          // Can't scroll too far left
1721          viewInfo.h = std::max(0.0, viewInfo.h);
1722       trackPanel.Refresh(false);
1723    }
1724 }
1725 
ZoomInByFactor(double ZoomFactor)1726 void ProjectWindow::ZoomInByFactor( double ZoomFactor )
1727 {
1728    auto &project = mProject;
1729    auto &viewInfo = ViewInfo::Get( project );
1730 
1731    auto gAudioIO = AudioIO::Get();
1732    // LLL: Handling positioning differently when audio is
1733    // actively playing.  Don't do this if paused.
1734    if (gAudioIO->IsStreamActive(
1735          ProjectAudioIO::Get( project ).GetAudioIOToken()) &&
1736        !gAudioIO->IsPaused()){
1737       ZoomBy(ZoomFactor);
1738       ScrollIntoView(gAudioIO->GetStreamTime());
1739       return;
1740    }
1741 
1742    // DMM: Here's my attempt to get logical zooming behavior
1743    // when there's a selection that's currently at least
1744    // partially on-screen
1745 
1746    const double endTime = viewInfo.GetScreenEndTime();
1747    const double duration = endTime - viewInfo.h;
1748 
1749    bool selectionIsOnscreen =
1750       (viewInfo.selectedRegion.t0() < endTime) &&
1751       (viewInfo.selectedRegion.t1() >= viewInfo.h);
1752 
1753    bool selectionFillsScreen =
1754       (viewInfo.selectedRegion.t0() < viewInfo.h) &&
1755       (viewInfo.selectedRegion.t1() > endTime);
1756 
1757    if (selectionIsOnscreen && !selectionFillsScreen) {
1758       // Start with the center of the selection
1759       double selCenter = (viewInfo.selectedRegion.t0() +
1760                           viewInfo.selectedRegion.t1()) / 2;
1761 
1762       // If the selection center is off-screen, pick the
1763       // center of the part that is on-screen.
1764       if (selCenter < viewInfo.h)
1765          selCenter = viewInfo.h +
1766                      (viewInfo.selectedRegion.t1() - viewInfo.h) / 2;
1767       if (selCenter > endTime)
1768          selCenter = endTime -
1769             (endTime - viewInfo.selectedRegion.t0()) / 2;
1770 
1771       // Zoom in
1772       ZoomBy(ZoomFactor);
1773       const double newDuration =
1774          viewInfo.GetScreenEndTime() - viewInfo.h;
1775 
1776       // Recenter on selCenter
1777       TP_ScrollWindow(selCenter - newDuration / 2);
1778       return;
1779    }
1780 
1781 
1782    double origLeft = viewInfo.h;
1783    double origWidth = duration;
1784    ZoomBy(ZoomFactor);
1785 
1786    const double newDuration =
1787       viewInfo.GetScreenEndTime() - viewInfo.h;
1788    double newh = origLeft + (origWidth - newDuration) / 2;
1789 
1790    // MM: Commented this out because it was confusing users
1791    /*
1792    // make sure that the *right-hand* end of the selection is
1793    // no further *left* than 1/3 of the way across the screen
1794    if (viewInfo.selectedRegion.t1() < newh + viewInfo.screen / 3)
1795       newh = viewInfo.selectedRegion.t1() - viewInfo.screen / 3;
1796 
1797    // make sure that the *left-hand* end of the selection is
1798    // no further *right* than 2/3 of the way across the screen
1799    if (viewInfo.selectedRegion.t0() > newh + viewInfo.screen * 2 / 3)
1800       newh = viewInfo.selectedRegion.t0() - viewInfo.screen * 2 / 3;
1801    */
1802 
1803    TP_ScrollWindow(newh);
1804 }
1805 
ZoomOutByFactor(double ZoomFactor)1806 void ProjectWindow::ZoomOutByFactor( double ZoomFactor )
1807 {
1808    auto &project = mProject;
1809    auto &viewInfo = ViewInfo::Get( project );
1810 
1811    //Zoom() may change these, so record original values:
1812    const double origLeft = viewInfo.h;
1813    const double origWidth = viewInfo.GetScreenEndTime() - origLeft;
1814 
1815    ZoomBy(ZoomFactor);
1816    const double newWidth = viewInfo.GetScreenEndTime() - viewInfo.h;
1817 
1818    const double newh = origLeft + (origWidth - newWidth) / 2;
1819    // newh = (newh > 0) ? newh : 0;
1820    TP_ScrollWindow(newh);
1821 }
1822 
GetZoomOfToFit() const1823 double ProjectWindow::GetZoomOfToFit() const
1824 {
1825    auto &project = mProject;
1826    auto &tracks = TrackList::Get( project );
1827    auto &viewInfo = ViewInfo::Get( project );
1828 
1829    const double end = tracks.GetEndTime();
1830    const double start = viewInfo.bScrollBeyondZero
1831       ? std::min( tracks.GetStartTime(), 0.0)
1832       : 0;
1833    const double len = end - start;
1834 
1835    if (len <= 0.0)
1836       return viewInfo.GetZoom();
1837 
1838    auto w = viewInfo.GetTracksUsableWidth();
1839    w -= 10;
1840    return w/len;
1841 }
1842 
DoZoomFit()1843 void ProjectWindow::DoZoomFit()
1844 {
1845    auto &project = mProject;
1846    auto &viewInfo = ViewInfo::Get( project );
1847    auto &tracks = TrackList::Get( project );
1848    auto &window = *this;
1849 
1850    const double start = viewInfo.bScrollBeyondZero
1851       ? std::min(tracks.GetStartTime(), 0.0)
1852       : 0;
1853 
1854    window.Zoom( window.GetZoomOfToFit() );
1855    window.TP_ScrollWindow(start);
1856 }
1857 
InstallTopPanelHookInstallTopPanelHook1858 static struct InstallTopPanelHook{ InstallTopPanelHook() {
1859    ToolManager::SetGetTopPanelHook(
1860       []( wxWindow &window ){
1861          auto pProjectWindow = dynamic_cast< ProjectWindow* >( &window );
1862          return pProjectWindow ? pProjectWindow->GetTopPanel() : nullptr;
1863       }
1864    );
1865 }} installTopPanelHook;
1866