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