/********************************************************************** Audacity: A Digital Audio Editor ProjectWindow.cpp Paul Licameli split from AudacityProject.cpp **********************************************************************/ #include "ProjectWindow.h" #include "AllThemeResources.h" #include "AudioIO.h" #include "Menus.h" #include "Project.h" #include "ProjectAudioIO.h" #include "ProjectStatus.h" #include "RefreshCode.h" #include "TrackPanelMouseEvent.h" #include "TrackPanelAx.h" #include "UndoManager.h" #include "ViewInfo.h" #include "WaveClip.h" #include "WaveTrack.h" #include "prefs/ThemePrefs.h" #include "prefs/TracksPrefs.h" #include "toolbars/ToolManager.h" #include "tracks/ui/Scrubbing.h" #include "tracks/ui/TrackView.h" #include "widgets/wxPanelWrapper.h" #include "widgets/WindowAccessible.h" #include #include #include // Returns the screen containing a rectangle, or -1 if none does. int ScreenContaining( wxRect & r ){ unsigned int n = wxDisplay::GetCount(); for(unsigned int i = 0;iGetLeftTop(), requestedRect->GetBottomRight()); // Hackery to approximate a window top bar size from a window size. // and exclude the open/close and borders. targetTitleRect.x += 15; targetTitleRect.width -= 100; if (targetTitleRect.width < 165) targetTitleRect.width = 165; targetTitleRect.height = 15; int targetBottom = targetTitleRect.GetBottom(); int targetRight = targetTitleRect.GetRight(); // This looks like overkill to check each and every pixel in the ranges. // and decide that if any is visible on screen we are OK. for (int i = targetTitleRect.GetLeft(); i < targetRight; i++) { for (int j = targetTitleRect.GetTop(); j < targetBottom; j++) { int monitor = display.GetFromPoint(wxPoint(i, j)); if (monitor != wxNOT_FOUND) { return TRUE; } } } return FALSE; } // BG: The default size and position of the first window void GetDefaultWindowRect(wxRect *defRect) { *defRect = wxGetClientDisplayRect(); int width = 940; int height = 674; //These conditional values assist in improving placement and size //of NEW windows on different platforms. #ifdef __WXGTK__ height += 20; #endif #ifdef __WXMSW__ height += 40; #endif #ifdef __WXMAC__ height += 55; #endif // Use screen size where it is smaller than the values we would like. // Otherwise use the values we would like, and centred. if (width < defRect->width) { defRect->x = (defRect->width - width)/2; defRect->width = width; } if (height < defRect->height) { defRect->y = (defRect->height - height)/2; // Bug 1119 workaround // Small adjustment for very small Mac screens. // If there is only a tiny space at the top // then instead of vertical centre, align to bottom. const int pixelsFormenu = 60; if( defRect->y < pixelsFormenu ) defRect->y *=2; defRect->height = height; } } // BG: Calculate where to place the next window (could be the first window) // BG: Does not store X and Y in prefs. This is intentional. // // LL: This should NOT need to be this complicated...FIXME void GetNextWindowPlacement(wxRect *nextRect, bool *pMaximized, bool *pIconized) { int inc = 25; wxRect defaultRect; GetDefaultWindowRect(&defaultRect); gPrefs->Read(wxT("/Window/Maximized"), pMaximized, false); gPrefs->Read(wxT("/Window/Iconized"), pIconized, false); wxRect windowRect; gPrefs->Read(wxT("/Window/X"), &windowRect.x, defaultRect.x); gPrefs->Read(wxT("/Window/Y"), &windowRect.y, defaultRect.y); gPrefs->Read(wxT("/Window/Width"), &windowRect.width, defaultRect.width); gPrefs->Read(wxT("/Window/Height"), &windowRect.height, defaultRect.height); wxRect normalRect; gPrefs->Read(wxT("/Window/Normal_X"), &normalRect.x, defaultRect.x); gPrefs->Read(wxT("/Window/Normal_Y"), &normalRect.y, defaultRect.y); gPrefs->Read(wxT("/Window/Normal_Width"), &normalRect.width, defaultRect.width); gPrefs->Read(wxT("/Window/Normal_Height"), &normalRect.height, defaultRect.height); // Workaround 2.1.1 and earlier bug on OSX...affects only normalRect, but let's just // validate for all rects and plats if (normalRect.width == 0 || normalRect.height == 0) { normalRect = defaultRect; } if (windowRect.width == 0 || windowRect.height == 0) { windowRect = defaultRect; } wxRect screenRect( wxGetClientDisplayRect()); #if defined(__WXMAC__) // On OSX, the top of the window should never be less than the menu height, // so something is amiss if it is if (normalRect.y < screenRect.y) { normalRect = defaultRect; } if (windowRect.y < screenRect.y) { windowRect = defaultRect; } #endif // IF projects empty, THEN it's the first window. // It lands where the config says it should, and can straddle screen. if (AllProjects{}.empty()) { if (*pMaximized || *pIconized) { *nextRect = normalRect; } else { *nextRect = windowRect; } // Resize, for example if one monitor that was on is now off. if (!CornersOnScreen( wxRect(*nextRect).Deflate( 32, 32 ))) { *nextRect = defaultRect; } if (!IsWindowAccessible(nextRect)) { *nextRect = defaultRect; } // Do not trim the first project window down. // All corners are on screen (or almost so), and // the rect may straddle screens. return; } // ELSE a subsequent NEW window. It will NOT straddle screens. // We don't mind being 32 pixels off the screen in any direction. // Make sure initial sizes (pretty much) fit within the display bounds // We used to trim the sizes which could result in ridiculously small windows. // contributing to bug 1243. // Now instead if the window significantly doesn't fit the screen, we use the default // window instead, which we know does. if (ScreenContaining( wxRect(normalRect).Deflate( 32, 32 ))<0) { normalRect = defaultRect; } if (ScreenContaining( wxRect(windowRect).Deflate( 32, 32 ) )<0) { windowRect = defaultRect; } bool validWindowSize = false; ProjectWindow * validProject = NULL; for ( auto iter = AllProjects{}.rbegin(), end = AllProjects{}.rend(); iter != end; ++iter ) { auto pProject = *iter; if (!GetProjectFrame( *pProject ).IsIconized()) { validWindowSize = true; validProject = &ProjectWindow::Get( *pProject ); break; } } if (validWindowSize) { *nextRect = validProject->GetRect(); *pMaximized = validProject->IsMaximized(); *pIconized = validProject->IsIconized(); // Do not straddle screens. if (ScreenContaining( wxRect(*nextRect).Deflate( 32, 32 ) )<0) { *nextRect = defaultRect; } } else { *nextRect = normalRect; } //Placement depends on the increments nextRect->x += inc; nextRect->y += inc; // defaultrect is a rectangle on the first screen. It's the right fallback to // use most of the time if things are not working out right with sizing. // windowRect is a saved rectangle size. // normalRect seems to be a substitute for windowRect when iconized or maximised. // Windows can say that we are off screen when actually we are not. // On Windows 10 I am seeing miscalculation by about 6 pixels. // To fix this we allow some sloppiness on the edge being counted as off screen. // This matters most when restoring very carefully sized windows that are maximised // in one dimension (height or width) but not both. const int edgeSlop = 10; // Next four lines are getting the rectangle for the screen that contains the // top left corner of nextRect (and defaulting to rect of screen 0 otherwise). wxPoint p = nextRect->GetLeftTop(); int scr = std::max( 0, wxDisplay::GetFromPoint( p )); wxDisplay d( scr ); screenRect = d.GetClientArea(); // Now we (possibly) start trimming our rectangle down. // Have we hit the right side of the screen? wxPoint bottomRight = nextRect->GetBottomRight(); if (bottomRight.x > (screenRect.GetRight()+edgeSlop)) { int newWidth = screenRect.GetWidth() - nextRect->GetLeft(); if (newWidth < defaultRect.GetWidth()) { nextRect->x = windowRect.x; nextRect->y = windowRect.y; nextRect->width = windowRect.width; } else { nextRect->width = newWidth; } } // Have we hit the bottom of the screen? bottomRight = nextRect->GetBottomRight(); if (bottomRight.y > (screenRect.GetBottom()+edgeSlop)) { nextRect->y -= inc; bottomRight = nextRect->GetBottomRight(); if (bottomRight.y > (screenRect.GetBottom()+edgeSlop)) { nextRect->SetBottom(screenRect.GetBottom()); } } // After all that we could have a window that does not have a visible // top bar. [It is unlikely, but something might have gone wrong] // If so, use the safe fallback size. if (!IsWindowAccessible(nextRect)) { *nextRect = defaultRect; } } namespace { // This wrapper prevents the scrollbars from retaining focus after being // used. Otherwise, the only way back to the track panel is to click it // and that causes your original location to be lost. class ScrollBar final : public wxScrollBar { public: ScrollBar(wxWindow* parent, wxWindowID id, long style) : wxScrollBar(parent, id, wxDefaultPosition, wxDefaultSize, style) { } void OnSetFocus(wxFocusEvent & e) { wxWindow *w = e.GetWindow(); if (w != NULL) { w->SetFocus(); } } void SetScrollbar(int position, int thumbSize, int range, int pageSize, bool refresh = true) override; private: DECLARE_EVENT_TABLE() }; void ScrollBar::SetScrollbar(int position, int thumbSize, int range, int pageSize, bool refresh) { // Mitigate flashing of scrollbars by refreshing only when something really changes. // PRL: This may have been made unnecessary by other fixes for flashing, see // commit ac05b190bee7dd0000bce56edb0e5e26185c972f auto changed = position != GetThumbPosition() || thumbSize != GetThumbSize() || range != GetRange() || pageSize != GetPageSize(); if (!changed) return; wxScrollBar::SetScrollbar(position, thumbSize, range, pageSize, refresh); } BEGIN_EVENT_TABLE(ScrollBar, wxScrollBar) EVT_SET_FOCUS(ScrollBar::OnSetFocus) END_EVENT_TABLE() // Common mouse wheel handling in track panel cells, moved here to avoid // compilation dependencies on Track, TrackPanel, and Scrubbing at low levels // which made cycles static struct MouseWheelHandler { MouseWheelHandler() { CommonTrackPanelCell::InstallMouseWheelHook( *this ); } // Need a bit of memory from one call to the next mutable double mVertScrollRemainder = 0.0; unsigned operator() ( const TrackPanelMouseEvent &evt, AudacityProject *pProject ) const { using namespace RefreshCode; if ( TrackList::Get( *pProject ).empty() ) // Scrolling and Zoom in and out commands are disabled when there are no tracks. // This should be disabled too for consistency. Otherwise // you do see changes in the time ruler. return Cancelled; unsigned result = RefreshAll; const wxMouseEvent &event = evt.event; auto &viewInfo = ViewInfo::Get( *pProject ); Scrubber &scrubber = Scrubber::Get( *pProject ); auto &window = ProjectWindow::Get( *pProject ); const auto steps = evt.steps; if (event.ShiftDown() // Don't pan during smooth scrolling. That would conflict with keeping // the play indicator centered. && !scrubber.IsScrollScrubbing() ) { // MM: Scroll left/right when used with Shift key down window.TP_ScrollWindow( viewInfo.OffsetTimeByPixels( viewInfo.PositionToTime(0), 50.0 * -steps)); } else if (event.CmdDown()) { #if 0 // JKC: Alternative scroll wheel zooming code // using AudacityProject zooming, which is smarter, // it keeps selections on screen and centred if it can, // also this ensures mousewheel and zoom buttons give same result. double ZoomFactor = pow(2.0, steps); AudacityProject *p = GetProject(); if( steps > 0 ) // PRL: Track panel refresh may be needed if you reenable this // code, but we don't want this file dependent on TrackPanel.cpp p->ZoomInByFactor( ZoomFactor ); else p->ZoomOutByFactor( ZoomFactor ); #endif // MM: Zoom in/out when used with Control key down // We're converting pixel positions to times, // counting pixels from the left edge of the track. int trackLeftEdge = viewInfo.GetLeftOffset(); // Time corresponding to mouse position wxCoord xx; double center_h; double mouse_h = viewInfo.PositionToTime(event.m_x, trackLeftEdge); // Scrubbing? Expand or contract about the center, ignoring mouse position if (scrubber.IsScrollScrubbing()) center_h = viewInfo.h + (viewInfo.GetScreenEndTime() - viewInfo.h) / 2.0; // Zooming out? Focus on mouse. else if( steps <= 0 ) center_h = mouse_h; // No Selection? Focus on mouse. else if((viewInfo.selectedRegion.t1() - viewInfo.selectedRegion.t0() ) < 0.00001 ) center_h = mouse_h; // Before Selection? Focus on left else if( mouse_h < viewInfo.selectedRegion.t0() ) center_h = viewInfo.selectedRegion.t0(); // After Selection? Focus on right else if( mouse_h > viewInfo.selectedRegion.t1() ) center_h = viewInfo.selectedRegion.t1(); // Inside Selection? Focus on mouse else center_h = mouse_h; xx = viewInfo.TimeToPosition(center_h, trackLeftEdge); // Time corresponding to last (most far right) audio. double audioEndTime = TrackList::Get( *pProject ).GetEndTime(); // Disabled this code to fix Bug 1923 (tricky to wheel-zoom right of waveform). #if 0 // When zooming in empty space, it's easy to 'lose' the waveform. // This prevents it. // IF zooming in if (steps > 0) { // IF mouse is to right of audio if (center_h > audioEndTime) // Zooming brings far right of audio to mouse. center_h = audioEndTime; } #endif wxCoord xTrackEnd = viewInfo.TimeToPosition( audioEndTime ); viewInfo.ZoomBy(pow(2.0, steps)); double new_center_h = viewInfo.PositionToTime(xx, trackLeftEdge); viewInfo.h += (center_h - new_center_h); // If wave has gone off screen, bring it back. // This means that the end of the track stays where it was. if( viewInfo.h > audioEndTime ) viewInfo.h += audioEndTime - viewInfo.PositionToTime( xTrackEnd ); result |= FixScrollbars; } else { #ifdef EXPERIMENTAL_SCRUBBING_SCROLL_WHEEL if (scrubber.IsScrubbing()) { scrubber.HandleScrollWheel(steps); evt.event.Skip(false); } else #endif { // MM: Scroll up/down when used without modifier keys double lines = steps * 4 + mVertScrollRemainder; mVertScrollRemainder = lines - floor(lines); lines = floor(lines); auto didSomething = window.TP_ScrollUpDown((int)-lines); if (!didSomething) result |= Cancelled; } } return result; } } sMouseWheelHandler; AudacityProject::AttachedWindows::RegisteredFactory sProjectWindowKey{ []( AudacityProject &parent ) -> wxWeakRef< wxWindow > { wxRect wndRect; bool bMaximized = false; bool bIconized = false; GetNextWindowPlacement(&wndRect, &bMaximized, &bIconized); auto pWindow = safenew ProjectWindow( nullptr, -1, wxDefaultPosition, wxSize(wndRect.width, wndRect.height), parent ); auto &window = *pWindow; // wxGTK3 seems to need to require creating the window using default position // and then manually positioning it. window.SetPosition(wndRect.GetPosition()); if(bMaximized) { window.Maximize(true); } else if (bIconized) { // if the user close down and iconized state we could start back up and iconized state // window.Iconize(TRUE); } return pWindow; } }; } ProjectWindow &ProjectWindow::Get( AudacityProject &project ) { return project.AttachedWindows::Get< ProjectWindow >( sProjectWindowKey ); } const ProjectWindow &ProjectWindow::Get( const AudacityProject &project ) { return Get( const_cast< AudacityProject & >( project ) ); } ProjectWindow *ProjectWindow::Find( AudacityProject *pProject ) { return pProject ? pProject->AttachedWindows::Find< ProjectWindow >( sProjectWindowKey ) : nullptr; } const ProjectWindow *ProjectWindow::Find( const AudacityProject *pProject ) { return Find( const_cast< AudacityProject * >( pProject ) ); } int ProjectWindow::NextWindowID() { return mNextWindowID++; } enum { FirstID = 1000, // Window controls HSBarID, VSBarID, NextID, }; //If you want any of these files, ask JKC. They are not //yet checked in to Audacity SVN as of 12-Feb-2010 #ifdef EXPERIMENTAL_NOTEBOOK #include "GuiFactory.h" #include "APanel.h" #endif ProjectWindow::ProjectWindow(wxWindow * parent, wxWindowID id, const wxPoint & pos, const wxSize & size, AudacityProject &project) : ProjectWindowBase{ parent, id, pos, size, project } { mNextWindowID = NextID; // Two sub-windows need to be made before Init(), // so that this constructor can complete, and then TrackPanel and // AdornedRulerPanel can retrieve those windows from this in their // factory functions // PRL: this panel groups the top tool dock and the ruler into one // tab cycle. // Must create it with non-default width equal to the main window width, // or else the device toolbar doesn't make initial widths of the choice // controls correct. mTopPanel = safenew wxPanelWrapper { this, wxID_ANY, wxDefaultPosition, wxSize{ this->GetSize().GetWidth(), -1 } }; mTopPanel->SetLabel( "Top Panel" );// Not localised mTopPanel->SetLayoutDirection(wxLayout_LeftToRight); mTopPanel->SetAutoLayout(true); #ifdef EXPERIMENTAL_DA2 mTopPanel->SetBackgroundColour(theTheme.Colour( clrMedium )); #endif wxWindow * pPage; #ifdef EXPERIMENTAL_NOTEBOOK // We are using a notebook (tabbed panel), so we create the notebook and add pages. GuiFactory Factory; wxNotebook * pNotebook; mMainPanel = Factory.AddPanel( this, wxPoint( left, top ), wxSize( width, height ) ); pNotebook = Factory.AddNotebook( mMainPanel ); /* i18n-hint: This is an experimental feature where the main panel in Audacity is put on a notebook tab, and this is the name on that tab. Other tabs in that notebook may have instruments, patch panels etc.*/ pPage = Factory.AddPage( pNotebook, _("Main Mix")); #else // Not using a notebook, so we place the track panel inside another panel, // this keeps the notebook code and normal code consistent and also // paves the way for adding additional windows inside the track panel. mMainPanel = safenew wxPanelWrapper(this, -1, wxDefaultPosition, wxDefaultSize, wxNO_BORDER); mMainPanel->SetSizer( safenew wxBoxSizer(wxVERTICAL) ); mMainPanel->SetLabel("Main Panel");// Not localised. pPage = mMainPanel; // Set the colour here to the track panel background to avoid // flicker when Audacity starts up. // However, that leads to areas next to the horizontal scroller // being painted in background colour and not scroller background // colour, so suppress this for now. //pPage->SetBackgroundColour( theTheme.Colour( clrDark )); #endif pPage->SetLayoutDirection(wxLayout_LeftToRight); #ifdef EXPERIMENTAL_DA2 pPage->SetBackgroundColour(theTheme.Colour( clrMedium )); #endif mMainPage = pPage; mPlaybackScroller = std::make_unique( &project ); // PRL: Old comments below. No longer observing the ordering that it // recommends. ProjectWindow::OnActivate puts the focus directly into // the TrackPanel, which avoids the problems. // LLL: When Audacity starts or becomes active after returning from // another application, the first window that can accept focus // will be given the focus even if we try to SetFocus(). By // creating the scrollbars after the TrackPanel, we resolve // several focus problems. mHsbar = safenew ScrollBar(pPage, HSBarID, wxSB_HORIZONTAL); mVsbar = safenew ScrollBar(pPage, VSBarID, wxSB_VERTICAL); #if wxUSE_ACCESSIBILITY // so that name can be set on a standard control mHsbar->SetAccessible(safenew WindowAccessible(mHsbar)); mVsbar->SetAccessible(safenew WindowAccessible(mVsbar)); #endif mHsbar->SetLayoutDirection(wxLayout_LeftToRight); mHsbar->SetName(_("Horizontal Scrollbar")); mVsbar->SetName(_("Vertical Scrollbar")); project.Bind( EVT_UNDO_MODIFIED, &ProjectWindow::OnUndoPushedModified, this ); project.Bind( EVT_UNDO_PUSHED, &ProjectWindow::OnUndoPushedModified, this ); project.Bind( EVT_UNDO_OR_REDO, &ProjectWindow::OnUndoRedo, this ); project.Bind( EVT_UNDO_RESET, &ProjectWindow::OnUndoReset, this ); wxTheApp->Bind(EVT_THEME_CHANGE, &ProjectWindow::OnThemeChange, this); } ProjectWindow::~ProjectWindow() { // Tool manager gives us capture sometimes if(HasCapture()) ReleaseMouse(); } BEGIN_EVENT_TABLE(ProjectWindow, wxFrame) EVT_MENU(wxID_ANY, ProjectWindow::OnMenu) EVT_MOUSE_EVENTS(ProjectWindow::OnMouseEvent) EVT_CLOSE(ProjectWindow::OnCloseWindow) EVT_SIZE(ProjectWindow::OnSize) EVT_SHOW(ProjectWindow::OnShow) EVT_ICONIZE(ProjectWindow::OnIconize) EVT_MOVE(ProjectWindow::OnMove) EVT_ACTIVATE(ProjectWindow::OnActivate) EVT_COMMAND_SCROLL_LINEUP(HSBarID, ProjectWindow::OnScrollLeftButton) EVT_COMMAND_SCROLL_LINEDOWN(HSBarID, ProjectWindow::OnScrollRightButton) EVT_COMMAND_SCROLL(HSBarID, ProjectWindow::OnScroll) EVT_COMMAND_SCROLL(VSBarID, ProjectWindow::OnScroll) // Fires for menu with ID #1...first menu defined EVT_UPDATE_UI(1, ProjectWindow::OnUpdateUI) EVT_COMMAND(wxID_ANY, EVT_TOOLBAR_UPDATED, ProjectWindow::OnToolBarUpdate) //mchinen:multithreaded calls - may not be threadsafe with CommandEvent: may have to change. END_EVENT_TABLE() void ProjectWindow::ApplyUpdatedTheme() { auto &project = mProject; SetBackgroundColour(theTheme.Colour( clrMedium )); ClearBackground();// For wxGTK. } void ProjectWindow::RedrawProject(const bool bForceWaveTracks /*= false*/) { auto pThis = wxWeakRef(this); CallAfter( [pThis, bForceWaveTracks]{ if (!pThis) return; if (pThis->IsBeingDeleted()) return; auto &project = pThis->mProject ; auto &tracks = TrackList::Get( project ); auto &trackPanel = GetProjectPanel( project ); pThis->FixScrollbars(); if (bForceWaveTracks) { for ( auto pWaveTrack : tracks.Any< WaveTrack >() ) for (const auto &clip: pWaveTrack->GetClips()) clip->MarkChanged(); } trackPanel.Refresh(false); }); } void ProjectWindow::OnThemeChange(wxCommandEvent& evt) { evt.Skip(); auto &project = mProject; this->ApplyUpdatedTheme(); auto &toolManager = ToolManager::Get( project ); for( int ii = 0; ii < ToolBarCount; ++ii ) { ToolBar *pToolBar = toolManager.GetToolBar(ii); if( pToolBar ) pToolBar->ReCreateButtons(); } } void ProjectWindow::UpdatePrefs() { // Update status bar widths in case of language change UpdateStatusWidths(); } void ProjectWindow::FinishAutoScroll() { // Set a flag so we don't have to generate two update events mAutoScrolling = true; // Call our Scroll method which updates our ViewInfo variables // to reflect the positions of the scrollbars DoScroll(); mAutoScrolling = false; } #if defined(__WXMAC__) // const int sbarSpaceWidth = 15; // const int sbarControlWidth = 16; // const int sbarExtraLen = 1; 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 #elif defined(__WXMSW__) const int sbarSpaceWidth = 16; const int sbarControlWidth = 16; const int sbarExtraLen = 0; 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 #else // wxGTK, wxMOTIF, wxX11 const int sbarSpaceWidth = 15; const int sbarControlWidth = 15; const int sbarExtraLen = 0; 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 #include "AllThemeResources.h" #endif // Make sure selection edge is in view void ProjectWindow::ScrollIntoView(double pos) { auto &trackPanel = GetProjectPanel( mProject ); auto &viewInfo = ViewInfo::Get( mProject ); auto w = viewInfo.GetTracksUsableWidth(); int pixel = viewInfo.TimeToPosition(pos); if (pixel < 0 || pixel >= w) { TP_ScrollWindow (viewInfo.OffsetTimeByPixels(pos, -(w / 2))); trackPanel.Refresh(false); } } void ProjectWindow::ScrollIntoView(int x) { auto &viewInfo = ViewInfo::Get( mProject ); ScrollIntoView(viewInfo.PositionToTime(x, viewInfo.GetLeftOffset())); } /// /// This method handles general left-scrolling, either for drag-scrolling /// or when the scrollbar is clicked to the left of the thumb /// void ProjectWindow::OnScrollLeft() { auto &project = mProject; auto &viewInfo = ViewInfo::Get( project ); wxInt64 pos = mHsbar->GetThumbPosition(); // move at least one scroll increment pos -= wxMax((wxInt64)(sbarHjump * viewInfo.sbarScale), 1); pos = wxMax(pos, 0); viewInfo.sbarH -= sbarHjump; viewInfo.sbarH = std::max(viewInfo.sbarH, -(wxInt64) PixelWidthBeforeTime(0.0)); if (pos != mHsbar->GetThumbPosition()) { mHsbar->SetThumbPosition((int)pos); FinishAutoScroll(); } } /// /// This method handles general right-scrolling, either for drag-scrolling /// or when the scrollbar is clicked to the right of the thumb /// void ProjectWindow::OnScrollRight() { auto &project = mProject; auto &viewInfo = ViewInfo::Get( project ); wxInt64 pos = mHsbar->GetThumbPosition(); // move at least one scroll increment // use wxInt64 for calculation to prevent temporary overflow pos += wxMax((wxInt64)(sbarHjump * viewInfo.sbarScale), 1); wxInt64 max = mHsbar->GetRange() - mHsbar->GetThumbSize(); pos = wxMin(pos, max); viewInfo.sbarH += sbarHjump; viewInfo.sbarH = std::min(viewInfo.sbarH, viewInfo.sbarTotal - (wxInt64) PixelWidthBeforeTime(0.0) - viewInfo.sbarScreen); if (pos != mHsbar->GetThumbPosition()) { mHsbar->SetThumbPosition((int)pos); FinishAutoScroll(); } } /// /// This handles the event when the left direction button on the scrollbar is depressed /// void ProjectWindow::OnScrollLeftButton(wxScrollEvent & /*event*/) { auto &project = mProject; auto &viewInfo = ViewInfo::Get( project ); wxInt64 pos = mHsbar->GetThumbPosition(); // move at least one scroll increment pos -= wxMax((wxInt64)(sbarHjump * viewInfo.sbarScale), 1); pos = wxMax(pos, 0); viewInfo.sbarH -= sbarHjump; viewInfo.sbarH = std::max(viewInfo.sbarH, - (wxInt64) PixelWidthBeforeTime(0.0)); if (pos != mHsbar->GetThumbPosition()) { mHsbar->SetThumbPosition((int)pos); DoScroll(); } } /// /// This handles the event when the right direction button on the scrollbar is depressed /// void ProjectWindow::OnScrollRightButton(wxScrollEvent & /*event*/) { auto &project = mProject; auto &viewInfo = ViewInfo::Get( project ); wxInt64 pos = mHsbar->GetThumbPosition(); // move at least one scroll increment // use wxInt64 for calculation to prevent temporary overflow pos += wxMax((wxInt64)(sbarHjump * viewInfo.sbarScale), 1); wxInt64 max = mHsbar->GetRange() - mHsbar->GetThumbSize(); pos = wxMin(pos, max); viewInfo.sbarH += sbarHjump; viewInfo.sbarH = std::min(viewInfo.sbarH, viewInfo.sbarTotal - (wxInt64) PixelWidthBeforeTime(0.0) - viewInfo.sbarScreen); if (pos != mHsbar->GetThumbPosition()) { mHsbar->SetThumbPosition((int)pos); DoScroll(); } } bool ProjectWindow::MayScrollBeyondZero() const { auto &project = mProject; auto &scrubber = Scrubber::Get( project ); auto &viewInfo = ViewInfo::Get( project ); if (viewInfo.bScrollBeyondZero) return true; if (scrubber.HasMark() || ProjectAudioIO::Get( project ).IsAudioActive()) { if (mPlaybackScroller) { auto mode = mPlaybackScroller->GetMode(); if (mode == PlaybackScroller::Mode::Pinned || mode == PlaybackScroller::Mode::Right) return true; } } return false; } double ProjectWindow::ScrollingLowerBoundTime() const { auto &project = mProject; auto &tracks = TrackList::Get( project ); auto &viewInfo = ViewInfo::Get( project ); if (!MayScrollBeyondZero()) return 0; const double screen = viewInfo.GetScreenEndTime() - viewInfo.h; return std::min(tracks.GetStartTime(), -screen); } // PRL: Bug1197: we seem to need to compute all in double, to avoid differing results on Mac // That's why ViewInfo::TimeRangeToPixelWidth was defined, with some regret. double ProjectWindow::PixelWidthBeforeTime(double scrollto) const { auto &project = mProject; auto &viewInfo = ViewInfo::Get( project ); const double lowerBound = ScrollingLowerBoundTime(); return // Ignoring fisheye is correct here viewInfo.TimeRangeToPixelWidth(scrollto - lowerBound); } void ProjectWindow::SetHorizontalThumb(double scrollto) { auto &project = mProject; auto &viewInfo = ViewInfo::Get( project ); const auto unscaled = PixelWidthBeforeTime(scrollto); const int max = mHsbar->GetRange() - mHsbar->GetThumbSize(); const int pos = std::min(max, std::max(0, (int)(floor(0.5 + unscaled * viewInfo.sbarScale)))); mHsbar->SetThumbPosition(pos); viewInfo.sbarH = floor(0.5 + unscaled - PixelWidthBeforeTime(0.0)); viewInfo.sbarH = std::max(viewInfo.sbarH, - (wxInt64) PixelWidthBeforeTime(0.0)); viewInfo.sbarH = std::min(viewInfo.sbarH, viewInfo.sbarTotal - (wxInt64) PixelWidthBeforeTime(0.0) - viewInfo.sbarScreen); } // // This method, like the other methods prefaced with TP, handles TrackPanel // 'callback'. // void ProjectWindow::TP_ScrollWindow(double scrollto) { SetHorizontalThumb(scrollto); // Call our Scroll method which updates our ViewInfo variables // to reflect the positions of the scrollbars DoScroll(); } // // Scroll vertically. This is called for example by the mouse wheel // handler in Track Panel. A positive argument makes the window // scroll down, while a negative argument scrolls up. // bool ProjectWindow::TP_ScrollUpDown(int delta) { int oldPos = mVsbar->GetThumbPosition(); int pos = oldPos + delta; int max = mVsbar->GetRange() - mVsbar->GetThumbSize(); // Can be negative in case of only one track if (max < 0) max = 0; if (pos > max) pos = max; else if (pos < 0) pos = 0; if (pos != oldPos) { mVsbar->SetThumbPosition(pos); DoScroll(); return true; } else return false; } void ProjectWindow::FixScrollbars() { auto &project = mProject; auto &tracks = TrackList::Get( project ); auto &trackPanel = GetProjectPanel( project ); auto &viewInfo = ViewInfo::Get( project ); bool refresh = false; bool rescroll = false; int totalHeight = TrackView::GetTotalHeight( tracks ) + 32; auto panelWidth = viewInfo.GetTracksUsableWidth(); auto panelHeight = viewInfo.GetHeight(); // (From Debian...at least I think this is the change corresponding // to this comment) // // (2.) GTK critical warning "IA__gtk_range_set_range: assertion // 'min < max' failed" because of negative numbers as result of window // size checking. Added a sanity check that straightens up the numbers // in edge cases. if (panelWidth < 0) { panelWidth = 0; } if (panelHeight < 0) { panelHeight = 0; } auto LastTime = std::numeric_limits::lowest(); for (const Track *track : tracks) { // Iterate over pending changed tracks if present. track = track->SubstitutePendingChangedTrack().get(); LastTime = std::max( LastTime, track->GetEndTime() ); } LastTime = std::max(LastTime, viewInfo.selectedRegion.t1()); const double screen = viewInfo.GetScreenEndTime() - viewInfo.h; const double halfScreen = screen / 2.0; // If we can scroll beyond zero, // Add 1/2 of a screen of blank space to the end // and another 1/2 screen before the beginning // so that any point within the union of the selection and the track duration // may be scrolled to the midline. // May add even more to the end, so that you can always scroll the starting time to zero. const double lowerBound = ScrollingLowerBoundTime(); const double additional = MayScrollBeyondZero() ? -lowerBound + std::max(halfScreen, screen - LastTime) : screen / 4.0; viewInfo.total = LastTime + additional; // Don't remove time from total that's still on the screen viewInfo.total = std::max(viewInfo.total, viewInfo.h + screen); if (viewInfo.h < lowerBound) { viewInfo.h = lowerBound; rescroll = true; } viewInfo.sbarTotal = (wxInt64) (viewInfo.GetTotalWidth()); viewInfo.sbarScreen = (wxInt64)(panelWidth); viewInfo.sbarH = (wxInt64) (viewInfo.GetBeforeScreenWidth()); // PRL: Can someone else find a more elegant solution to bug 812, than // introducing this boolean member variable? // Setting mVSbar earlier, int HandlXMLTag, didn't succeed in restoring // the vertical scrollbar to its saved position. So defer that till now. // mbInitializingScrollbar should be true only at the start of the life // of an AudacityProject reopened from disk. if (!mbInitializingScrollbar) { viewInfo.vpos = mVsbar->GetThumbPosition() * viewInfo.scrollStep; } mbInitializingScrollbar = false; if (viewInfo.vpos >= totalHeight) viewInfo.vpos = totalHeight - 1; if (viewInfo.vpos < 0) viewInfo.vpos = 0; bool oldhstate; bool oldvstate; bool newhstate = (viewInfo.GetScreenEndTime() - viewInfo.h) < viewInfo.total; bool newvstate = panelHeight < totalHeight; #ifdef __WXGTK__ oldhstate = mHsbar->IsShown(); oldvstate = mVsbar->IsShown(); mHsbar->Show(newhstate); mVsbar->Show(panelHeight < totalHeight); #else oldhstate = mHsbar->IsEnabled(); oldvstate = mVsbar->IsEnabled(); mHsbar->Enable(newhstate); mVsbar->Enable(panelHeight < totalHeight); #endif if (panelHeight >= totalHeight && viewInfo.vpos != 0) { viewInfo.vpos = 0; refresh = true; rescroll = false; } if (!newhstate && viewInfo.sbarH != 0) { viewInfo.sbarH = 0; refresh = true; rescroll = false; } // wxScrollbar only supports int values but we need a greater range, so // we scale the scrollbar coordinates on demand. We only do this if we // would exceed the int range, so we can always use the maximum resolution // available. // Don't use the full 2^31 max int range but a bit less, so rounding // errors in calculations do not overflow max int wxInt64 maxScrollbarRange = (wxInt64)(2147483647 * 0.999); if (viewInfo.sbarTotal > maxScrollbarRange) viewInfo.sbarScale = ((double)maxScrollbarRange) / viewInfo.sbarTotal; else viewInfo.sbarScale = 1.0; // use maximum resolution { int scaledSbarH = (int)(viewInfo.sbarH * viewInfo.sbarScale); int scaledSbarScreen = (int)(viewInfo.sbarScreen * viewInfo.sbarScale); int scaledSbarTotal = (int)(viewInfo.sbarTotal * viewInfo.sbarScale); const int offset = (int)(floor(0.5 + viewInfo.sbarScale * PixelWidthBeforeTime(0.0))); mHsbar->SetScrollbar(scaledSbarH + offset, scaledSbarScreen, scaledSbarTotal, scaledSbarScreen, TRUE); } // Vertical scrollbar mVsbar->SetScrollbar(viewInfo.vpos / viewInfo.scrollStep, panelHeight / viewInfo.scrollStep, totalHeight / viewInfo.scrollStep, panelHeight / viewInfo.scrollStep, TRUE); if (refresh || (rescroll && (viewInfo.GetScreenEndTime() - viewInfo.h) < viewInfo.total)) { trackPanel.Refresh(false); } MenuManager::Get( project ).UpdateMenus(); if (oldhstate != newhstate || oldvstate != newvstate) { UpdateLayout(); } } void ProjectWindow::UpdateLayout() { auto &project = mProject; auto &trackPanel = GetProjectPanel( project ); auto &toolManager = ToolManager::Get( project ); // 1. Layout panel, to get widths of the docks. Layout(); // 2. Layout toolbars to pack the toolbars correctly in docks which // are now the correct width. toolManager.LayoutToolBars(); // 3. Layout panel, to resize docks, in particular reducing the height // of any empty docks, or increasing the height of docks that need it. Layout(); // Bug 2455 // The commented out code below is to calculate a nice minimum size for // the window. However on Ubuntu when the window is minimised it leads to // an insanely tall window. // Using a fixed min size fixes that. // However there is still something strange when minimised, as once // UpdateLayout is called once, when minimised, it gets called repeatedly. #if 0 // Retrieve size of this projects window wxSize mainsz = GetSize(); // Retrieve position of the track panel to use as the size of the top // third of the window wxPoint tppos = ClientToScreen(trackPanel.GetParent()->GetPosition()); // Retrieve position of bottom dock to use as the size of the bottom // third of the window wxPoint sbpos = ClientToScreen(toolManager.GetBotDock()->GetPosition()); // The "+ 50" is the minimum height of the TrackPanel SetMinSize( wxSize(250, (mainsz.y - sbpos.y) + tppos.y + 50)); #endif SetMinSize( wxSize(250, 250)); SetMaxSize( wxSize(20000, 20000)); } void ProjectWindow::HandleResize() { // Activate events can fire during window teardown, so just // ignore them. if (mIsDeleting) { return; } CallAfter( [this]{ if (mIsDeleting) return; FixScrollbars(); UpdateLayout(); }); } bool ProjectWindow::IsIconized() const { return mIconized; } void ProjectWindow::UpdateStatusWidths() { enum { nWidths = nStatusBarFields + 1 }; int widths[ nWidths ]{ 0 }; widths[ rateStatusBarField ] = 150; const auto statusBar = GetStatusBar(); const auto &functions = ProjectStatus::GetStatusWidthFunctions(); // Start from 1 not 0 // Specifying a first column always of width 0 was needed for reasons // I forget now for ( int ii = 1; ii <= nStatusBarFields; ++ii ) { int &width = widths[ ii ]; for ( const auto &function : functions ) { auto results = function( mProject, static_cast< StatusBarField >( ii ) ); for ( const auto &string : results.first ) { int w; statusBar->GetTextExtent(string.Translation(), &w, nullptr); width = std::max( width, w + results.second ); } } } // The main status field is not fixed width widths[ mainStatusBarField ] = -1; statusBar->SetStatusWidths( nWidths, widths ); } void ProjectWindow::MacShowUndockedToolbars(bool show) { (void)show;//compiler food #ifdef __WXMAC__ // Save the focus so we can restore it to whatever had it before since // showing a previously hidden toolbar will cause the focus to be set to // its frame. If this is not done it will appear that activation events // aren't being sent to the project window since they are actually being // delivered to the last tool frame shown. wxWindow *focused = FindFocus(); // Find all the floating toolbars, and show or hide them const auto &children = GetChildren(); for(const auto &child : children) { if (auto frame = dynamic_cast(child)) { if (!show) { frame->Hide(); } else if (frame->GetBar() && frame->GetBar()->IsVisible() ) { frame->Show(); } } } // Restore the focus if needed if (focused) { focused->SetFocus(); } #endif } void ProjectWindow::OnIconize(wxIconizeEvent &event) { //JKC: On Iconizing we get called twice. Don't know // why but it does no harm. // Should we be returning true/false rather than // void return? I don't know. mIconized = event.IsIconized(); #if defined(__WXMAC__) // Readdresses bug 1431 since a crash could occur when restoring iconized // floating toolbars due to recursion (bug 2411). MacShowUndockedToolbars(!mIconized); if( !mIconized ) { Raise(); } #endif // VisibileProjectCount seems to be just a counter for debugging. // It's not used outside this function. auto VisibleProjectCount = std::count_if( AllProjects{}.begin(), AllProjects{}.end(), []( const AllProjects::value_type &ptr ){ return !GetProjectFrame( *ptr ).IsIconized(); } ); event.Skip(); // This step is to fix part of Bug 2040, where the BackingPanel // size was not restored after we leave Iconized state. // Queue up a resize event using OnShow so that we // refresh the track panel. But skip this, if we're iconized. if( mIconized ) return; wxShowEvent Evt; OnShow( Evt ); } void ProjectWindow::OnMove(wxMoveEvent & event) { if (!this->IsMaximized() && !this->IsIconized()) SetNormalizedWindowState(this->GetRect()); event.Skip(); } void ProjectWindow::OnSize(wxSizeEvent & event) { // (From Debian) // // (3.) GTK critical warning "IA__gdk_window_get_origin: assertion // 'GDK_IS_WINDOW (window)' failed": Received events of type wxSizeEvent // on the main project window cause calls to "ClientToScreen" - which is // not available until the window is first shown. So the class has to // keep track of wxShowEvent events and inhibit those actions until the // window is first shown. if (mShownOnce) { HandleResize(); if (!this->IsMaximized() && !this->IsIconized()) SetNormalizedWindowState(this->GetRect()); } event.Skip(); } void ProjectWindow::OnShow(wxShowEvent & event) { // Remember that the window has been shown at least once mShownOnce = true; // (From Debian...see also TrackPanel::OnTimer and AudacityTimer::Notify) // // Description: Workaround for wxWidgets bug: Reentry in clipboard // The wxWidgets bug http://trac.wxwidgets.org/ticket/16636 prevents // us from doing clipboard operations in wxShowEvent and wxTimerEvent // processing because those event could possibly be processed during // the (not sufficiently protected) Yield() of a first clipboard // operation, causing reentry. Audacity had a workaround in place // for this problem (the class "CaptureEvents"), which however isn't // applicable with wxWidgets 3.0 because it's based on changing the // gdk event handler, a change that would be overridden by wxWidgets's // own gdk event handler change. // Instead, as a NEW workaround, specifically protect those processings // of wxShowEvent and wxTimerEvent that try to do clipboard operations // from being executed within Yield(). This is done by delaying their // execution by posting pure wxWidgets events - which are never executed // during Yield(). // Author: Martin Stegh fer // Bug-Debian: https://bugs.debian.org/765341 // the actual creation/showing of the window). // Post the event instead of calling OnSize(..) directly. This ensures that // this is a pure wxWidgets event (no GDK event behind it) and that it // therefore isn't processed within the YieldFor(..) of the clipboard // operations (workaround for Debian bug #765341). // QueueEvent() will take ownership of the event GetEventHandler()->QueueEvent(safenew wxSizeEvent(GetSize())); // Further processing by default handlers event.Skip(); } /// /// A toolbar has been updated, so handle it like a sizing event. /// void ProjectWindow::OnToolBarUpdate(wxCommandEvent & event) { HandleResize(); event.Skip(false); /* No need to propagate any further */ } void ProjectWindow::OnUndoPushedModified( wxCommandEvent &evt ) { evt.Skip(); RedrawProject(); } void ProjectWindow::OnUndoRedo( wxCommandEvent &evt ) { evt.Skip(); HandleResize(); RedrawProject(); } void ProjectWindow::OnUndoReset( wxCommandEvent &evt ) { evt.Skip(); HandleResize(); // RedrawProject(); // Should we do this here too? } void ProjectWindow::OnScroll(wxScrollEvent & WXUNUSED(event)) { auto &project = mProject; auto &viewInfo = ViewInfo::Get( project ); const wxInt64 offset = PixelWidthBeforeTime(0.0); viewInfo.sbarH = (wxInt64)(mHsbar->GetThumbPosition() / viewInfo.sbarScale) - offset; DoScroll(); #ifndef __WXMAC__ // Bug2179 // This keeps the time ruler in sync with horizontal scrolling, without // making an undesirable compilation dependency of this source file on // the ruler wxTheApp->ProcessIdle(); #endif } void ProjectWindow::DoScroll() { auto &project = mProject; auto &trackPanel = GetProjectPanel( project ); auto &viewInfo = ViewInfo::Get( project ); const double lowerBound = ScrollingLowerBoundTime(); auto width = viewInfo.GetTracksUsableWidth(); viewInfo.SetBeforeScreenWidth(viewInfo.sbarH, width, lowerBound); if (MayScrollBeyondZero()) { enum { SCROLL_PIXEL_TOLERANCE = 10 }; if (std::abs(viewInfo.TimeToPosition(0.0, 0 )) < SCROLL_PIXEL_TOLERANCE) { // Snap the scrollbar to 0 viewInfo.h = 0; SetHorizontalThumb(0.0); } } viewInfo.vpos = mVsbar->GetThumbPosition() * viewInfo.scrollStep; //mchinen: do not always set this project to be the active one. //a project may autoscroll while playing in the background //I think this is okay since OnMouseEvent has one of these. //SetActiveProject(this); if (!mAutoScrolling) { trackPanel.Refresh(false); } } void ProjectWindow::OnMenu(wxCommandEvent & event) { #ifdef __WXMSW__ // Bug 1642: We can arrive here with bogus menu IDs, which we // proceed to process. So if bogus, don't. // The bogus menu IDs are probably generated by controls on the TrackPanel, // such as the Project Rate. // 17000 is the magic number at which we start our menu. // This code would probably NOT be OK on Mac, since we assign // some specific ID numbers. if( event.GetId() < 17000){ event.Skip(); return; } #endif auto &project = mProject; auto &commandManager = CommandManager::Get( project ); bool handled = commandManager.HandleMenuID( GetProject(), event.GetId(), MenuManager::Get( project ).GetUpdateFlags(), false); if (handled) event.Skip(false); else{ event.ResumePropagation( 999 ); event.Skip(true); } } void ProjectWindow::OnUpdateUI(wxUpdateUIEvent & WXUNUSED(event)) { auto &project = mProject; MenuManager::Get( project ).UpdateMenus(); } void ProjectWindow::OnActivate(wxActivateEvent & event) { // Activate events can fire during window teardown, so just // ignore them. if (IsBeingDeleted()) { return; } auto &project = mProject; mActive = event.GetActive(); // Under Windows, focus can be "lost" when returning to // Audacity from a different application. // // This was observed by minimizing all windows using WINDOWS+M and // then ALT+TAB to return to Audacity. Focus will be given to the // project window frame which is not at all useful. // // So, we use ToolManager's observation of focus changes in a wxEventFilter. // Then, when we receive the // activate event, we restore that focus to the child or the track // panel if no child had the focus (which probably should never happen). if (mActive) { auto &toolManager = ToolManager::Get( project ); SetActiveProject( &project ); if ( ! toolManager.RestoreFocus() ) GetProjectPanel( project ).SetFocus(); } event.Skip(); } bool ProjectWindow::IsActive() { return mActive; } void ProjectWindow::OnMouseEvent(wxMouseEvent & event) { auto &project = mProject; if (event.ButtonDown()) SetActiveProject( &project ); } void ProjectWindow::ZoomAfterImport(Track *pTrack) { auto &project = mProject; auto &tracks = TrackList::Get( project ); auto &trackPanel = GetProjectPanel( project ); DoZoomFit(); trackPanel.SetFocus(); if (!pTrack) pTrack = *tracks.Selected().begin(); if (!pTrack) pTrack = *tracks.Any().begin(); if (pTrack) { TrackFocus::Get(project).Set(pTrack); pTrack->EnsureVisible(); } } // Utility function called by other zoom methods void ProjectWindow::Zoom(double level) { auto &project = mProject; auto &viewInfo = ViewInfo::Get( project ); viewInfo.SetZoom(level); FixScrollbars(); // See if we can center the selection on screen, and have it actually fit. // tOnLeft is the amount of time we would need before the selection left edge to center it. float t0 = viewInfo.selectedRegion.t0(); float t1 = viewInfo.selectedRegion.t1(); float tAvailable = viewInfo.GetScreenEndTime() - viewInfo.h; float tOnLeft = (tAvailable - t0 + t1)/2.0; // Bug 1292 (Enh) is effectively a request to do this scrolling of the selection into view. // If tOnLeft is positive, then we have room for the selection, so scroll to it. if( tOnLeft >=0 ) TP_ScrollWindow( t0-tOnLeft); } // Utility function called by other zoom methods void ProjectWindow::ZoomBy(double multiplier) { auto &project = mProject; auto &viewInfo = ViewInfo::Get( project ); viewInfo.ZoomBy(multiplier); FixScrollbars(); } /////////////////////////////////////////////////////////////////// // This method 'rewinds' the track, by setting the cursor to 0 and // scrolling the window to fit 0 on the left side of it // (maintaining current zoom). // If shift is held down, it will extend the left edge of the // selection to 0 (holding right edge constant), otherwise it will // move both left and right edge of selection to 0 /////////////////////////////////////////////////////////////////// void ProjectWindow::Rewind(bool shift) { auto &project = mProject; auto &viewInfo = ViewInfo::Get( project ); viewInfo.selectedRegion.setT0(0, false); if (!shift) viewInfo.selectedRegion.setT1(0); TP_ScrollWindow(0); } /////////////////////////////////////////////////////////////////// // This method 'fast-forwards' the track, by setting the cursor to // the end of the samples on the selected track and scrolling the // window to fit the end on its right side (maintaining current zoom). // If shift is held down, it will extend the right edge of the // selection to the end (holding left edge constant), otherwise it will // move both left and right edge of selection to the end /////////////////////////////////////////////////////////////////// void ProjectWindow::SkipEnd(bool shift) { auto &project = mProject; auto &tracks = TrackList::Get( project ); auto &viewInfo = ViewInfo::Get( project ); double len = tracks.GetEndTime(); viewInfo.selectedRegion.setT1(len, false); if (!shift) viewInfo.selectedRegion.setT0(len); // Make sure the end of the track is visible ScrollIntoView(len); } // TrackPanel callback method void ProjectWindow::TP_ScrollLeft() { OnScrollLeft(); } // TrackPanel callback method void ProjectWindow::TP_ScrollRight() { OnScrollRight(); } // TrackPanel callback method void ProjectWindow::TP_RedrawScrollbars() { FixScrollbars(); } void ProjectWindow::TP_HandleResize() { HandleResize(); } ProjectWindow::PlaybackScroller::PlaybackScroller(AudacityProject *project) : mProject(project) { mProject->Bind(EVT_TRACK_PANEL_TIMER, &PlaybackScroller::OnTimer, this); } void ProjectWindow::PlaybackScroller::OnTimer(wxCommandEvent &event) { // Let other listeners get the notification event.Skip(); auto gAudioIO = AudioIO::Get(); mRecentStreamTime = gAudioIO->GetStreamTime(); auto cleanup = finally([&]{ // Propagate the message to other listeners bound to this this->SafelyProcessEvent( event ); }); if(!ProjectAudioIO::Get( *mProject ).IsAudioActive()) return; else if (mMode == Mode::Refresh) { // PRL: see comments in Scrubbing.cpp for why this is sometimes needed. // These unnecessary refreshes cause wheel rotation events to be delivered more uniformly // to the application, so scrub speed control is smoother. // (So I see at least with OS 10.10 and wxWidgets 3.0.2.) // Is there another way to ensure that than by refreshing? auto &trackPanel = GetProjectPanel( *mProject ); trackPanel.Refresh(false); } else if (mMode != Mode::Off) { // Pan the view, so that we put the play indicator at some fixed // fraction of the window width. auto &viewInfo = ViewInfo::Get( *mProject ); auto &trackPanel = GetProjectPanel( *mProject ); const int posX = viewInfo.TimeToPosition(mRecentStreamTime); auto width = viewInfo.GetTracksUsableWidth(); int deltaX; switch (mMode) { default: wxASSERT(false); /* fallthru */ case Mode::Pinned: deltaX = posX - width * TracksPrefs::GetPinnedHeadPositionPreference(); break; case Mode::Right: deltaX = posX - width; break; } viewInfo.h = viewInfo.OffsetTimeByPixels(viewInfo.h, deltaX, true); if (!ProjectWindow::Get( *mProject ).MayScrollBeyondZero()) // Can't scroll too far left viewInfo.h = std::max(0.0, viewInfo.h); trackPanel.Refresh(false); } } void ProjectWindow::ZoomInByFactor( double ZoomFactor ) { auto &project = mProject; auto &viewInfo = ViewInfo::Get( project ); auto gAudioIO = AudioIO::Get(); // LLL: Handling positioning differently when audio is // actively playing. Don't do this if paused. if (gAudioIO->IsStreamActive( ProjectAudioIO::Get( project ).GetAudioIOToken()) && !gAudioIO->IsPaused()){ ZoomBy(ZoomFactor); ScrollIntoView(gAudioIO->GetStreamTime()); return; } // DMM: Here's my attempt to get logical zooming behavior // when there's a selection that's currently at least // partially on-screen const double endTime = viewInfo.GetScreenEndTime(); const double duration = endTime - viewInfo.h; bool selectionIsOnscreen = (viewInfo.selectedRegion.t0() < endTime) && (viewInfo.selectedRegion.t1() >= viewInfo.h); bool selectionFillsScreen = (viewInfo.selectedRegion.t0() < viewInfo.h) && (viewInfo.selectedRegion.t1() > endTime); if (selectionIsOnscreen && !selectionFillsScreen) { // Start with the center of the selection double selCenter = (viewInfo.selectedRegion.t0() + viewInfo.selectedRegion.t1()) / 2; // If the selection center is off-screen, pick the // center of the part that is on-screen. if (selCenter < viewInfo.h) selCenter = viewInfo.h + (viewInfo.selectedRegion.t1() - viewInfo.h) / 2; if (selCenter > endTime) selCenter = endTime - (endTime - viewInfo.selectedRegion.t0()) / 2; // Zoom in ZoomBy(ZoomFactor); const double newDuration = viewInfo.GetScreenEndTime() - viewInfo.h; // Recenter on selCenter TP_ScrollWindow(selCenter - newDuration / 2); return; } double origLeft = viewInfo.h; double origWidth = duration; ZoomBy(ZoomFactor); const double newDuration = viewInfo.GetScreenEndTime() - viewInfo.h; double newh = origLeft + (origWidth - newDuration) / 2; // MM: Commented this out because it was confusing users /* // make sure that the *right-hand* end of the selection is // no further *left* than 1/3 of the way across the screen if (viewInfo.selectedRegion.t1() < newh + viewInfo.screen / 3) newh = viewInfo.selectedRegion.t1() - viewInfo.screen / 3; // make sure that the *left-hand* end of the selection is // no further *right* than 2/3 of the way across the screen if (viewInfo.selectedRegion.t0() > newh + viewInfo.screen * 2 / 3) newh = viewInfo.selectedRegion.t0() - viewInfo.screen * 2 / 3; */ TP_ScrollWindow(newh); } void ProjectWindow::ZoomOutByFactor( double ZoomFactor ) { auto &project = mProject; auto &viewInfo = ViewInfo::Get( project ); //Zoom() may change these, so record original values: const double origLeft = viewInfo.h; const double origWidth = viewInfo.GetScreenEndTime() - origLeft; ZoomBy(ZoomFactor); const double newWidth = viewInfo.GetScreenEndTime() - viewInfo.h; const double newh = origLeft + (origWidth - newWidth) / 2; // newh = (newh > 0) ? newh : 0; TP_ScrollWindow(newh); } double ProjectWindow::GetZoomOfToFit() const { auto &project = mProject; auto &tracks = TrackList::Get( project ); auto &viewInfo = ViewInfo::Get( project ); const double end = tracks.GetEndTime(); const double start = viewInfo.bScrollBeyondZero ? std::min( tracks.GetStartTime(), 0.0) : 0; const double len = end - start; if (len <= 0.0) return viewInfo.GetZoom(); auto w = viewInfo.GetTracksUsableWidth(); w -= 10; return w/len; } void ProjectWindow::DoZoomFit() { auto &project = mProject; auto &viewInfo = ViewInfo::Get( project ); auto &tracks = TrackList::Get( project ); auto &window = *this; const double start = viewInfo.bScrollBeyondZero ? std::min(tracks.GetStartTime(), 0.0) : 0; window.Zoom( window.GetZoomOfToFit() ); window.TP_ScrollWindow(start); } static struct InstallTopPanelHook{ InstallTopPanelHook() { ToolManager::SetGetTopPanelHook( []( wxWindow &window ){ auto pProjectWindow = dynamic_cast< ProjectWindow* >( &window ); return pProjectWindow ? pProjectWindow->GetTopPanel() : nullptr; } ); }} installTopPanelHook;