/********************************************************************** Audacity: A Digital Audio Editor ProjectManager.cpp Paul Licameli split from AudacityProject.cpp **********************************************************************/ #include "ProjectManager.h" #include "AdornedRulerPanel.h" #include "AudioIO.h" #include "Clipboard.h" #include "FileNames.h" #include "Menus.h" #include "ModuleManager.h" #include "Project.h" #include "ProjectAudioIO.h" #include "ProjectAudioManager.h" #include "ProjectFileIO.h" #include "ProjectFileManager.h" #include "ProjectHistory.h" #include "ProjectSelectionManager.h" #include "ProjectSettings.h" #include "ProjectStatus.h" #include "ProjectWindow.h" #include "SelectUtilities.h" #include "TrackPanel.h" #include "TrackUtilities.h" #include "UndoManager.h" #include "WaveTrack.h" #include "wxFileNameWrapper.h" #include "import/Import.h" #include "import/ImportMIDI.h" #include "prefs/QualitySettings.h" #include "toolbars/MixerToolBar.h" #include "toolbars/SelectionBar.h" #include "toolbars/SpectralSelectionBar.h" #include "toolbars/TimeToolBar.h" #include "toolbars/ToolManager.h" #include "widgets/AudacityMessageBox.h" #include "widgets/FileHistory.h" #include "widgets/ErrorDialog.h" #include "widgets/WindowAccessible.h" #include #include #include #include #ifdef __WXGTK__ #include "../images/AudacityLogoAlpha.xpm" #endif const int AudacityProjectTimerID = 5200; static AudacityProject::AttachedObjects::RegisteredFactory sProjectManagerKey { []( AudacityProject &project ) { return std::make_shared< ProjectManager >( project ); } }; ProjectManager &ProjectManager::Get( AudacityProject &project ) { return project.AttachedObjects::Get< ProjectManager >( sProjectManagerKey ); } const ProjectManager &ProjectManager::Get( const AudacityProject &project ) { return Get( const_cast< AudacityProject & >( project ) ); } ProjectManager::ProjectManager( AudacityProject &project ) : mProject{ project } , mTimer{ std::make_unique(this, AudacityProjectTimerID) } { auto &window = ProjectWindow::Get( mProject ); window.Bind( wxEVT_CLOSE_WINDOW, &ProjectManager::OnCloseWindow, this ); mProject.Bind(EVT_PROJECT_STATUS_UPDATE, &ProjectManager::OnStatusChange, this); project.Bind( EVT_RECONNECTION_FAILURE, &ProjectManager::OnReconnectionFailure, this ); } ProjectManager::~ProjectManager() = default; // PRL: This event type definition used to be in AudacityApp.h, which created // a bad compilation dependency. The event was never emitted anywhere. I // preserve it and its handler here but I move it to remove the dependency. // Asynchronous open wxDECLARE_EXPORTED_EVENT(AUDACITY_DLL_API, EVT_OPEN_AUDIO_FILE, wxCommandEvent); wxDEFINE_EVENT(EVT_OPEN_AUDIO_FILE, wxCommandEvent); BEGIN_EVENT_TABLE( ProjectManager, wxEvtHandler ) EVT_COMMAND(wxID_ANY, EVT_OPEN_AUDIO_FILE, ProjectManager::OnOpenAudioFile) EVT_TIMER(AudacityProjectTimerID, ProjectManager::OnTimer) END_EVENT_TABLE() bool ProjectManager::sbWindowRectAlreadySaved = false; bool ProjectManager::sbSkipPromptingForSave = false; void ProjectManager::SaveWindowSize() { if (sbWindowRectAlreadySaved) { return; } bool validWindowForSaveWindowSize = FALSE; ProjectWindow * validProject = nullptr; bool foundIconizedProject = FALSE; for ( auto pProject : AllProjects{} ) { auto &window = ProjectWindow::Get( *pProject ); if (!window.IsIconized()) { validWindowForSaveWindowSize = TRUE; validProject = &window; break; } else foundIconizedProject = TRUE; } if (validWindowForSaveWindowSize) { wxRect windowRect = validProject->GetRect(); wxRect normalRect = validProject->GetNormalizedWindowState(); bool wndMaximized = validProject->IsMaximized(); gPrefs->Write(wxT("/Window/X"), windowRect.GetX()); gPrefs->Write(wxT("/Window/Y"), windowRect.GetY()); gPrefs->Write(wxT("/Window/Width"), windowRect.GetWidth()); gPrefs->Write(wxT("/Window/Height"), windowRect.GetHeight()); gPrefs->Write(wxT("/Window/Maximized"), wndMaximized); gPrefs->Write(wxT("/Window/Normal_X"), normalRect.GetX()); gPrefs->Write(wxT("/Window/Normal_Y"), normalRect.GetY()); gPrefs->Write(wxT("/Window/Normal_Width"), normalRect.GetWidth()); gPrefs->Write(wxT("/Window/Normal_Height"), normalRect.GetHeight()); gPrefs->Write(wxT("/Window/Iconized"), FALSE); } else { if (foundIconizedProject) { validProject = &ProjectWindow::Get( **AllProjects{}.begin() ); bool wndMaximized = validProject->IsMaximized(); wxRect normalRect = validProject->GetNormalizedWindowState(); // store only the normal rectangle because the itemized rectangle // makes no sense for an opening project window gPrefs->Write(wxT("/Window/X"), normalRect.GetX()); gPrefs->Write(wxT("/Window/Y"), normalRect.GetY()); gPrefs->Write(wxT("/Window/Width"), normalRect.GetWidth()); gPrefs->Write(wxT("/Window/Height"), normalRect.GetHeight()); gPrefs->Write(wxT("/Window/Maximized"), wndMaximized); gPrefs->Write(wxT("/Window/Normal_X"), normalRect.GetX()); gPrefs->Write(wxT("/Window/Normal_Y"), normalRect.GetY()); gPrefs->Write(wxT("/Window/Normal_Width"), normalRect.GetWidth()); gPrefs->Write(wxT("/Window/Normal_Height"), normalRect.GetHeight()); gPrefs->Write(wxT("/Window/Iconized"), TRUE); } else { // this would be a very strange case that might possibly occur on the Mac // Audacity would have to be running with no projects open // in this case we are going to write only the default values wxRect defWndRect; GetDefaultWindowRect(&defWndRect); gPrefs->Write(wxT("/Window/X"), defWndRect.GetX()); gPrefs->Write(wxT("/Window/Y"), defWndRect.GetY()); gPrefs->Write(wxT("/Window/Width"), defWndRect.GetWidth()); gPrefs->Write(wxT("/Window/Height"), defWndRect.GetHeight()); gPrefs->Write(wxT("/Window/Maximized"), FALSE); gPrefs->Write(wxT("/Window/Normal_X"), defWndRect.GetX()); gPrefs->Write(wxT("/Window/Normal_Y"), defWndRect.GetY()); gPrefs->Write(wxT("/Window/Normal_Width"), defWndRect.GetWidth()); gPrefs->Write(wxT("/Window/Normal_Height"), defWndRect.GetHeight()); gPrefs->Write(wxT("/Window/Iconized"), FALSE); } } gPrefs->Flush(); sbWindowRectAlreadySaved = true; } #if wxUSE_DRAG_AND_DROP class FileObject final : public wxFileDataObject { public: FileObject() { } bool IsSupportedFormat(const wxDataFormat & format, Direction WXUNUSED(dir = Get)) const // PRL: This function does NOT override any inherited virtual! What does it do? { if (format.GetType() == wxDF_FILENAME) { return true; } #if defined(__WXMAC__) #if !wxCHECK_VERSION(3, 0, 0) if (format.GetFormatId() == kDragPromisedFlavorFindFile) { return true; } #endif #endif return false; } }; class DropTarget final : public wxFileDropTarget { public: DropTarget(AudacityProject *proj) { mProject = proj; // SetDataObject takes ownership SetDataObject(safenew FileObject()); } ~DropTarget() { } #if defined(__WXMAC__) #if !wxCHECK_VERSION(3, 0, 0) bool GetData() override { bool foundSupported = false; bool firstFileAdded = false; OSErr result; UInt16 items = 0; CountDragItems((DragReference)m_currentDrag, &items); for (UInt16 index = 1; index <= items; index++) { DragItemRef theItem = 0; GetDragItemReferenceNumber((DragReference)m_currentDrag, index, &theItem); UInt16 flavors = 0; CountDragItemFlavors((DragReference)m_currentDrag, theItem , &flavors ) ; for (UInt16 flavor = 1 ;flavor <= flavors; flavor++) { FlavorType theType = 0; result = GetFlavorType((DragReference)m_currentDrag, theItem, flavor, &theType); if (theType != kDragPromisedFlavorFindFile && theType != kDragFlavorTypeHFS) { continue; } foundSupported = true; Size dataSize = 0; GetFlavorDataSize((DragReference)m_currentDrag, theItem, theType, &dataSize); ArrayOf theData{ dataSize }; GetFlavorData((DragReference)m_currentDrag, theItem, theType, (void*) theData.get(), &dataSize, 0L); wxString name; if (theType == kDragPromisedFlavorFindFile) { name = wxMacFSSpec2MacFilename((FSSpec *)theData.get()); } else if (theType == kDragFlavorTypeHFS) { name = wxMacFSSpec2MacFilename(&((HFSFlavor *)theData.get())->fileSpec); } if (!firstFileAdded) { // reset file list ((wxFileDataObject*)GetDataObject())->SetData(0, ""); firstFileAdded = true; } ((wxFileDataObject*)GetDataObject())->AddFile(name); // We only want to process one flavor break; } } return foundSupported; } #endif bool OnDrop(wxCoord x, wxCoord y) override { // bool foundSupported = false; #if !wxCHECK_VERSION(3, 0, 0) bool firstFileAdded = false; OSErr result; UInt16 items = 0; CountDragItems((DragReference)m_currentDrag, &items); for (UInt16 index = 1; index <= items; index++) { DragItemRef theItem = 0; GetDragItemReferenceNumber((DragReference)m_currentDrag, index, &theItem); UInt16 flavors = 0; CountDragItemFlavors((DragReference)m_currentDrag, theItem , &flavors ) ; for (UInt16 flavor = 1 ;flavor <= flavors; flavor++) { FlavorType theType = 0; result = GetFlavorType((DragReference)m_currentDrag, theItem, flavor, &theType); if (theType != kDragPromisedFlavorFindFile && theType != kDragFlavorTypeHFS) { continue; } return true; } } #endif return CurrentDragHasSupportedFormat(); } #endif bool OnDropFiles(wxCoord WXUNUSED(x), wxCoord WXUNUSED(y), const wxArrayString& filenames) override { // Experiment shows that this function can be reached while there is no // catch block above in wxWidgets. So stop all exceptions here. return GuardedCall< bool > ( [&] { wxArrayString sortednames(filenames); sortednames.Sort(FileNames::CompareNoCase); auto cleanup = finally( [&] { ProjectWindow::Get( *mProject ).HandleResize(); // Adjust scrollers for NEW track sizes. } ); for (const auto &name : sortednames) { #ifdef USE_MIDI if (FileNames::IsMidi(name)) DoImportMIDI( *mProject, name ); else #endif ProjectFileManager::Get( *mProject ).Import(name); } auto &window = ProjectWindow::Get( *mProject ); window.ZoomAfterImport(nullptr); return true; } ); } private: AudacityProject *mProject; }; #endif #ifdef EXPERIMENTAL_NOTEBOOK extern void AddPages( AudacityProject * pProj, GuiFactory & Factory, wxNotebook * pNotebook ); #endif void InitProjectWindow( ProjectWindow &window ) { auto &project = window.GetProject(); #ifdef EXPERIMENTAL_DA2 SetBackgroundColour(theTheme.Colour( clrMedium )); #endif // Note that the first field of the status bar is a dummy, and its width is set // to zero latter in the code. This field is needed for wxWidgets 2.8.12 because // if you move to the menu bar, the first field of the menu bar is cleared, which // is undesirable behaviour. // In addition, the help strings of menu items are by default sent to the first // field. Currently there are no such help strings, but it they were introduced, then // there would need to be an event handler to send them to the appropriate field. auto statusBar = window.CreateStatusBar(4); #if wxUSE_ACCESSIBILITY // so that name can be set on a standard control statusBar->SetAccessible(safenew WindowAccessible(statusBar)); #endif statusBar->SetName(wxT("status_line")); // not localized auto &viewInfo = ViewInfo::Get( project ); // LLL: Read this!!! // // Until the time (and cpu) required to refresh the track panel is // reduced, leave the following window creations in the order specified. // This will place the refresh of the track panel last, allowing all // the others to get done quickly. // // Near as I can tell, this is only a problem under Windows. // // // Create the ToolDock // ToolManager::Get( project ).CreateWindows(); ToolManager::Get( project ).LayoutToolBars(); // // Create the horizontal ruler // auto &ruler = AdornedRulerPanel::Get( project ); // // Create the TrackPanel and the scrollbars // auto topPanel = window.GetTopPanel(); { auto ubs = std::make_unique(wxVERTICAL); ubs->Add( ToolManager::Get( project ).GetTopDock(), 0, wxEXPAND | wxALIGN_TOP ); ubs->Add(&ruler, 0, wxEXPAND); topPanel->SetSizer(ubs.release()); } // Ensure that the topdock comes before the ruler in the tab order, // irrespective of the order in which they were created. ToolManager::Get(project).GetTopDock()->MoveBeforeInTabOrder(&ruler); const auto pPage = window.GetMainPage(); wxBoxSizer *bs; { auto ubs = std::make_unique(wxVERTICAL); bs = ubs.get(); bs->Add(topPanel, 0, wxEXPAND | wxALIGN_TOP); bs->Add(pPage, 1, wxEXPAND); bs->Add( ToolManager::Get( project ).GetBotDock(), 0, wxEXPAND ); window.SetAutoLayout(true); window.SetSizer(ubs.release()); } bs->Layout(); auto &trackPanel = TrackPanel::Get( project ); // 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 // making the TrackPanel that first window, we resolve several // keyboard focus problems. pPage->MoveBeforeInTabOrder(topPanel); bs = (wxBoxSizer *)pPage->GetSizer(); auto vsBar = &window.GetVerticalScrollBar(); auto hsBar = &window.GetHorizontalScrollBar(); { // Top horizontal grouping auto hs = std::make_unique(wxHORIZONTAL); // Track panel hs->Add(&trackPanel, 1, wxEXPAND | wxALIGN_LEFT | wxALIGN_TOP); { // Vertical grouping auto vs = std::make_unique(wxVERTICAL); // Vertical scroll bar vs->Add(vsBar, 1, wxEXPAND | wxALIGN_TOP); hs->Add(vs.release(), 0, wxEXPAND | wxALIGN_TOP); } bs->Add(hs.release(), 1, wxEXPAND | wxALIGN_LEFT | wxALIGN_TOP); } { // Bottom horizontal grouping auto hs = std::make_unique(wxHORIZONTAL); // Bottom scrollbar hs->Add(viewInfo.GetLeftOffset() - 1, 0); hs->Add(hsBar, 1, wxALIGN_BOTTOM); hs->Add(vsBar->GetSize().GetWidth(), 0); bs->Add(hs.release(), 0, wxEXPAND | wxALIGN_LEFT); } // Lay it out pPage->SetAutoLayout(true); pPage->Layout(); #ifdef EXPERIMENTAL_NOTEBOOK AddPages(this, Factory, pNotebook); #endif auto mainPanel = window.GetMainPanel(); mainPanel->Layout(); wxASSERT( trackPanel.GetProject() == &project ); // MM: Give track panel the focus to ensure keyboard commands work trackPanel.SetFocus(); window.FixScrollbars(); ruler.SetLeftOffset(viewInfo.GetLeftOffset()); // bevel on AdornedRuler // // Set the Icon // // loads either the XPM or the windows resource, depending on the platform #if !defined(__WXMAC__) && !defined(__WXX11__) { #if defined(__WXMSW__) wxIcon ic{ wxICON(AudacityLogo) }; #elif defined(__WXGTK__) wxIcon ic{wxICON(AudacityLogoAlpha)}; #else wxIcon ic{}; ic.CopyFromBitmap(theTheme.Bitmap(bmpAudacityLogo48x48)); #endif window.SetIcon(ic); } #endif window.UpdateStatusWidths(); auto msg = XO("Welcome to Audacity version %s") .Format( AUDACITY_VERSION_STRING ); ProjectManager::Get( project ).SetStatusText( msg, mainStatusBarField ); #ifdef EXPERIMENTAL_DA2 ClearBackground();// For wxGTK. #endif } AudacityProject *ProjectManager::New() { wxRect wndRect; bool bMaximized = false; bool bIconized = false; GetNextWindowPlacement(&wndRect, &bMaximized, &bIconized); // Create and show a NEW project // Use a non-default deleter in the smart pointer! auto sp = std::make_shared< AudacityProject >(); AllProjects{}.Add( sp ); auto p = sp.get(); auto &project = *p; auto &projectHistory = ProjectHistory::Get( project ); auto &projectManager = Get( project ); auto &window = ProjectWindow::Get( *p ); InitProjectWindow( window ); // wxGTK3 seems to need to require creating the window using default position // and then manually positioning it. window.SetPosition(wndRect.GetPosition()); auto &projectFileManager = ProjectFileManager::Get( *p ); // This may report an error. projectFileManager.OpenNewProject(); MenuManager::Get( project ).CreateMenusAndCommands( project ); projectHistory.InitialState(); projectManager.RestartTimer(); 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); } //Initialise the Listeners auto gAudioIO = AudioIO::Get(); gAudioIO->SetListener( ProjectAudioManager::Get( project ).shared_from_this() ); auto &projectSelectionManager = ProjectSelectionManager::Get( project ); SelectionBar::Get( project ).SetListener( &projectSelectionManager ); #ifdef EXPERIMENTAL_SPECTRAL_EDITING SpectralSelectionBar::Get( project ).SetListener( &projectSelectionManager ); #endif TimeToolBar::Get( project ).SetListener( &projectSelectionManager ); #if wxUSE_DRAG_AND_DROP // We can import now, so become a drag target // SetDropTarget(safenew AudacityDropTarget(this)); // mTrackPanel->SetDropTarget(safenew AudacityDropTarget(this)); // SetDropTarget takes ownership TrackPanel::Get( project ).SetDropTarget( safenew DropTarget( &project ) ); #endif //Set the NEW project as active: SetActiveProject(p); // Okay, GetActiveProject() is ready. Now we can get its CommandManager, // and add the shortcut keys to the tooltips. ToolManager::Get( *p ).RegenerateTooltips(); ModuleManager::Get().Dispatch(ProjectInitialized); window.Show(true); return p; } void ProjectManager::OnReconnectionFailure(wxCommandEvent & event) { event.Skip(); wxTheApp->CallAfter([this]{ ProjectWindow::Get(mProject).Close(true); }); } void ProjectManager::OnCloseWindow(wxCloseEvent & event) { auto &project = mProject; auto &projectFileIO = ProjectFileIO::Get( project ); auto &projectFileManager = ProjectFileManager::Get( project ); const auto &settings = ProjectSettings::Get( project ); auto &projectAudioIO = ProjectAudioIO::Get( project ); auto &tracks = TrackList::Get( project ); auto &window = ProjectWindow::Get( project ); auto gAudioIO = AudioIO::Get(); // We are called for the wxEVT_CLOSE_WINDOW, wxEVT_END_SESSION, and // wxEVT_QUERY_END_SESSION, so we have to protect against multiple // entries. This is a hack until the whole application termination // process can be reviewed and reworked. (See bug #964 for ways // to exercise the bug that instigated this hack.) if (window.IsBeingDeleted()) { event.Skip(); return; } if (event.CanVeto() && (::wxIsBusy() || project.mbBusyImporting)) { event.Veto(); return; } // Check to see if we were playing or recording // audio, and if so, make sure Audio I/O is completely finished. // The main point of this is to properly push the state // and flush the tracks once we've completely finished // recording NEW state. // This code is derived from similar code in // AudacityProject::~AudacityProject() and TrackPanel::OnTimer(). if (projectAudioIO.GetAudioIOToken()>0 && gAudioIO->IsStreamActive(projectAudioIO.GetAudioIOToken())) { // We were playing or recording audio, but we've stopped the stream. ProjectAudioManager::Get( project ).Stop(); projectAudioIO.SetAudioIOToken(0); window.RedrawProject(); } else if (gAudioIO->IsMonitoring()) { gAudioIO->StopStream(); } // MY: Use routine here so other processes can make same check bool bHasTracks = !tracks.empty(); // We may not bother to prompt the user to save, if the // project is now empty. if (!sbSkipPromptingForSave && event.CanVeto() && (settings.EmptyCanBeDirty() || bHasTracks)) { if ( UndoManager::Get( project ).UnsavedChanges() ) { TitleRestorer Restorer( window, project );// RAII /* i18n-hint: The first %s numbers the project, the second %s is the project name.*/ auto Title = XO("%sSave changes to %s?") .Format( Restorer.sProjNumber, Restorer.sProjName ); auto Message = XO("Save project before closing?"); if( !bHasTracks ) { Message += XO("\nIf saved, the project will have no tracks.\n\nTo save any previously open tracks:\nCancel, Edit > Undo until all tracks\nare open, then File > Save Project."); } int result = AudacityMessageBox( Message, Title, wxYES_NO | wxCANCEL | wxICON_QUESTION, &window); if (result == wxCANCEL || (result == wxYES && !GuardedCall( [&]{ return projectFileManager.Save(); } ) )) { event.Veto(); return; } } } #ifdef __WXMAC__ // Fix bug apparently introduced into 2.1.2 because of wxWidgets 3: // closing a project that was made full-screen (as by clicking the green dot // or command+/; not merely "maximized" as by clicking the title bar or // Zoom in the Window menu) leaves the screen black. // Fix it by un-full-screening. // (But is there a different way to do this? What do other applications do? // I don't see full screen windows of Safari shrinking, but I do see // momentary blackness.) window.ShowFullScreen(false); #endif ModuleManager::Get().Dispatch(ProjectClosing); // Stop the timer since there's no need to update anything anymore mTimer.reset(); // DMM: Save the size of the last window the user closes // // LL: Save before doing anything else to the window that might make // its size change. SaveWindowSize(); window.SetIsBeingDeleted(); // Mac: we never quit as the result of a close. // Other systems: we quit only when the close is the result of an external // command (on Windows, those are taskbar closes, "X" box, Alt+F4, etc.) bool quitOnClose; #ifdef __WXMAC__ quitOnClose = false; #else quitOnClose = !projectFileManager.GetMenuClose(); #endif // DanH: If we're definitely about to quit, clear the clipboard. auto &clipboard = Clipboard::Get(); if ((AllProjects{}.size() == 1) && (quitOnClose || AllProjects::Closing())) clipboard.Clear(); else { auto clipboardProject = clipboard.Project().lock(); if ( clipboardProject.get() == &mProject ) { // Closing the project from which content was cut or copied. // For 3.0.0, clear the clipboard, because accessing clipboard contents // would depend on a database connection to the closing project, but // that connection should closed now so that the project file can be // freely moved. // Notes: // 1) maybe clipboard contents could be saved by migrating them to // another temporary database, but that extra effort is beyond the // scope of 3.0.0. // 2) strictly speaking this is necessary only when the clipboard // contains WaveTracks. clipboard.Clear(); } } // JKC: For Win98 and Linux do not detach the menu bar. // We want wxWidgets to clean it up for us. // TODO: Is there a Mac issue here?? // SetMenuBar(NULL); // Compact the project. projectFileManager.CompactProjectOnClose(); // Set (or not) the bypass flag to indicate that deletes that would happen during // the UndoManager::ClearStates() below are not necessary. projectFileIO.SetBypass(); { // This can reduce reference counts of sample blocks in the project's // tracks. UndoManager::Get( project ).ClearStates(); // Delete all the tracks to free up memory tracks.Clear(); } // Some of the AdornedRulerPanel functions refer to the TrackPanel, so destroy this // before the TrackPanel is destroyed. This change was needed to stop Audacity // crashing when running with Jaws on Windows 10 1703. AdornedRulerPanel::Destroy( project ); // Destroy the TrackPanel early so it's not around once we start // deleting things like tracks and such out from underneath it. // Check validity of mTrackPanel per bug 584 Comment 1. // Deeper fix is in the Import code, but this failsafes against crash. TrackPanel::Destroy( project ); // Finalize the tool manager before the children since it needs // to save the state of the toolbars. ToolManager::Get( project ).Destroy(); window.DestroyChildren(); // Close project only now, because TrackPanel might have been holding // some shared_ptr to WaveTracks keeping SampleBlocks alive. // We're all done with the project file, so close it now projectFileManager.CloseProject(); WaveTrackFactory::Destroy( project ); // Remove self from the global array, but defer destruction of self auto pSelf = AllProjects{}.Remove( project ); wxASSERT( pSelf ); if (GetActiveProject() == &project) { // Find a NEW active project if ( !AllProjects{}.empty() ) { SetActiveProject(AllProjects{}.begin()->get()); } else { SetActiveProject(NULL); } } // Since we're going to be destroyed, make sure we're not to // receive audio notifications anymore. // PRL: Maybe all this is unnecessary now that the listener is managed // by a weak pointer. if ( gAudioIO->GetListener().get() == &ProjectAudioManager::Get( project ) ) { auto active = GetActiveProject(); gAudioIO->SetListener( active ? ProjectAudioManager::Get( *active ).shared_from_this() : nullptr ); } if (AllProjects{}.empty() && !AllProjects::Closing()) { #if !defined(__WXMAC__) if (quitOnClose) { // Simulate the application Exit menu item wxCommandEvent evt{ wxEVT_MENU, wxID_EXIT }; wxTheApp->AddPendingEvent( evt ); } else { sbWindowRectAlreadySaved = false; // For non-Mac, always keep at least one project window open (void) New(); } #endif } window.Destroy(); // Destroys this pSelf.reset(); } // PRL: I preserve this handler function for an event that was never sent, but // I don't know the intention. void ProjectManager::OnOpenAudioFile(wxCommandEvent & event) { const wxString &cmd = event.GetString(); if (!cmd.empty()) { ProjectChooser chooser{ &mProject, true }; if (auto project = ProjectFileManager::OpenFile( std::ref(chooser), cmd)) { auto &window = GetProjectFrame( *project ); window.RequestUserAttention(); chooser.Commit(); } } } // static method, can be called outside of a project void ProjectManager::OpenFiles(AudacityProject *proj) { auto selectedFiles = ProjectFileManager::ShowOpenDialog(FileNames::Operation::Open); if (selectedFiles.size() == 0) { Importer::SetLastOpenType({}); return; } //first sort selectedFiles. selectedFiles.Sort(FileNames::CompareNoCase); auto cleanup = finally( [] { Importer::SetLastOpenType({}); } ); for (const auto &fileName : selectedFiles) { // Make sure it isn't already open. if (ProjectFileManager::IsAlreadyOpen(fileName)) continue; // Skip ones that are already open. proj = OpenProject( proj, fileName, true /* addtohistory */, false /* reuseNonemptyProject */ ); } } bool ProjectManager::SafeToOpenProjectInto(AudacityProject &proj) { // DMM: If the project is dirty, that means it's been touched at // all, and it's not safe to open a fresh project directly in its // place. Only if the project is brandnew clean and the user // hasn't done any action at all is it safe for Open to take place // inside the current project. // // If you try to Open a fresh project inside the current window when // there are no tracks, but there's an Undo history, etc, then // bad things can happen, including orphan blocks, or tracks // referring to non-existent blocks if ( ProjectHistory::Get( proj ).GetDirty() || !TrackList::Get( proj ).empty() ) return false; // This project is clean; it's never been touched. Therefore // all relevant member variables are in their initial state, // and it's okay to open a NEW project inside its window. return true; } ProjectManager::ProjectChooser::~ProjectChooser() { if (mpUsedProject) { if (mpUsedProject == mpGivenProject) { // Ensure that it happens here: don't wait for the application level // exception handler, because the exception may be intercepted ProjectHistory::Get(*mpGivenProject).RollbackState(); // Any exception now continues propagating } else GetProjectFrame( *mpUsedProject ).Close(true); } } AudacityProject & ProjectManager::ProjectChooser::operator() ( bool openingProjectFile ) { if (mpGivenProject) { // Always check before opening a project file (for safety); // May check even when opening other files // (to preserve old behavior; as with the File > Open command specifying // multiple files of whatever types, so that each gets its own window) bool checkReuse = (openingProjectFile || !mReuseNonemptyProject); if (!checkReuse || SafeToOpenProjectInto(*mpGivenProject)) return *(mpUsedProject = mpGivenProject); } return *(mpUsedProject = New()); } void ProjectManager::ProjectChooser::Commit() { mpUsedProject = nullptr; } AudacityProject *ProjectManager::OpenProject( AudacityProject *pGivenProject, const FilePath &fileNameArg, bool addtohistory, bool reuseNonemptyProject) { ProjectManager::ProjectChooser chooser{ pGivenProject, reuseNonemptyProject }; if (auto pProject = ProjectFileManager::OpenFile( std::ref(chooser), fileNameArg, addtohistory )) { chooser.Commit(); auto &projectFileIO = ProjectFileIO::Get( *pProject ); if( projectFileIO.IsRecovered() ) { auto &window = ProjectWindow::Get( *pProject ); window.Zoom( window.GetZoomOfToFit() ); // "Project was recovered" replaces "Create new project" in Undo History. auto &undoManager = UndoManager::Get( *pProject ); undoManager.RemoveStates(0, 1); } return pProject; } return nullptr; } // This is done to empty out the tracks, but without creating a new project. void ProjectManager::ResetProjectToEmpty() { auto &project = mProject; auto &projectFileIO = ProjectFileIO::Get( project ); auto &projectFileManager = ProjectFileManager::Get( project ); auto &projectHistory = ProjectHistory::Get( project ); auto &viewInfo = ViewInfo::Get( project ); SelectUtilities::DoSelectAll( project ); TrackUtilities::DoRemoveTracks( project ); WaveTrackFactory::Reset( project ); projectHistory.SetDirty( false ); auto &undoManager = UndoManager::Get( project ); undoManager.ClearStates(); projectFileManager.CloseProject(); projectFileManager.OpenProject(); } void ProjectManager::RestartTimer() { if (mTimer) { // mTimer->Stop(); // not really needed mTimer->Start( 3000 ); // Update messages as needed once every 3 s. } } void ProjectManager::OnTimer(wxTimerEvent& WXUNUSED(event)) { auto &project = mProject; auto &projectAudioIO = ProjectAudioIO::Get( project ); auto mixerToolBar = &MixerToolBar::Get( project ); mixerToolBar->UpdateControls(); auto gAudioIO = AudioIO::Get(); // gAudioIO->GetNumCaptureChannels() should only be positive // when we are recording. if (projectAudioIO.GetAudioIOToken() > 0 && gAudioIO->GetNumCaptureChannels() > 0) { wxLongLong freeSpace = ProjectFileIO::Get(project).GetFreeDiskSpace(); if (freeSpace >= 0) { int iRecordingMins = GetEstimatedRecordingMinsLeftOnDisk(gAudioIO->GetNumCaptureChannels()); auto sMessage = XO("Disk space remaining for recording: %s") .Format( GetHoursMinsString(iRecordingMins) ); // Do not change mLastMainStatusMessage SetStatusText(sMessage, mainStatusBarField); } } // As also with the TrackPanel timer: wxTimer may be unreliable without // some restarts RestartTimer(); } void ProjectManager::OnStatusChange( wxCommandEvent &evt ) { evt.Skip(); auto &project = mProject; // Be careful to null-check the window. We might get to this function // during shut-down, but a timer hasn't been told to stop sending its // messages yet. auto pWindow = ProjectWindow::Find( &project ); if ( !pWindow ) return; auto &window = *pWindow; window.UpdateStatusWidths(); auto field = static_cast( evt.GetInt() ); const auto &msg = ProjectStatus::Get( project ).Get( field ); SetStatusText( msg, field ); if ( field == mainStatusBarField ) // When recording, let the NEW status message stay at least as long as // the timer interval (if it is not replaced again by this function), // before replacing it with the message about remaining disk capacity. RestartTimer(); } void ProjectManager::SetStatusText( const TranslatableString &text, int number ) { auto &project = mProject; auto pWindow = ProjectWindow::Find( &project ); if ( !pWindow ) return; auto &window = *pWindow; window.GetStatusBar()->SetStatusText(text.Translation(), number); } TranslatableString ProjectManager::GetHoursMinsString(int iMinutes) { if (iMinutes < 1) // Less than a minute... return XO("Less than 1 minute"); // Calculate int iHours = iMinutes / 60; int iMins = iMinutes % 60; auto sHours = XP( "%d hour", "%d hours", 0 )( iHours ); auto sMins = XP( "%d minute", "%d minutes", 0 )( iMins ); /* i18n-hint: A time in hours and minutes. Only translate the "and". */ return XO("%s and %s.").Format( sHours, sMins ); } // This routine will give an estimate of how many // minutes of recording time we have available. // The calculations made are based on the user's current // preferences. int ProjectManager::GetEstimatedRecordingMinsLeftOnDisk(long lCaptureChannels) { auto &project = mProject; // Obtain the current settings auto oCaptureFormat = QualitySettings::SampleFormatChoice(); if (lCaptureChannels == 0) lCaptureChannels = AudioIORecordChannels.Read(); // Find out how much free space we have on disk wxLongLong lFreeSpace = ProjectFileIO::Get( project ).GetFreeDiskSpace(); if (lFreeSpace < 0) { return 0; } // Calculate the remaining time double dRecTime = 0.0; double bytesOnDiskPerSample = SAMPLE_SIZE_DISK(oCaptureFormat); dRecTime = lFreeSpace.GetHi() * 4294967296.0 + lFreeSpace.GetLo(); dRecTime /= bytesOnDiskPerSample; dRecTime /= lCaptureChannels; dRecTime /= ProjectSettings::Get( project ).GetRate(); // Convert to minutes before returning int iRecMins = (int)round(dRecTime / 60.0); return iRecMins; }