1 /**********************************************************************
2 
3 Audacity: A Digital Audio Editor
4 
5 ProjectFileManager.cpp
6 
7 Paul Licameli split from AudacityProject.cpp
8 
9 **********************************************************************/
10 
11 #include "ProjectFileManager.h"
12 
13 #include <wx/crt.h> // for wxPrintf
14 
15 #if defined(__WXGTK__)
16 #include <wx/evtloop.h>
17 #endif
18 
19 #include <wx/frame.h>
20 #include <wx/log.h>
21 #include "BasicUI.h"
22 #include "CodeConversions.h"
23 #include "Legacy.h"
24 #include "PlatformCompatibility.h"
25 #include "Project.h"
26 #include "ProjectFileIO.h"
27 #include "ProjectFSCK.h"
28 #include "ProjectHistory.h"
29 #include "ProjectSelectionManager.h"
30 #include "ProjectWindows.h"
31 #include "ProjectRate.h"
32 #include "ProjectSettings.h"
33 #include "ProjectStatus.h"
34 #include "ProjectWindow.h"
35 #include "SelectFile.h"
36 #include "SelectUtilities.h"
37 #include "SelectionState.h"
38 #include "Tags.h"
39 #include "TempDirectory.h"
40 #include "TrackPanelAx.h"
41 #include "TrackPanel.h"
42 #include "UndoManager.h"
43 #include "WaveTrack.h"
44 #include "WaveClip.h"
45 #include "wxFileNameWrapper.h"
46 #include "export/Export.h"
47 #include "import/Import.h"
48 #include "import/ImportMIDI.h"
49 #include "toolbars/SelectionBar.h"
50 #include "widgets/AudacityMessageBox.h"
51 #include "widgets/FileHistory.h"
52 #include "widgets/UnwritableLocationErrorDialog.h"
53 #include "widgets/Warning.h"
54 #include "widgets/wxPanelWrapper.h"
55 #include "XMLFileReader.h"
56 
57 #include "HelpText.h"
58 
59 static const AudacityProject::AttachedObjects::RegisteredFactory sFileManagerKey{
__anon92ae3f8c0102( )60    []( AudacityProject &parent ){
61       auto result = std::make_shared< ProjectFileManager >( parent );
62       return result;
63    }
64 };
65 
Get(AudacityProject & project)66 ProjectFileManager &ProjectFileManager::Get( AudacityProject &project )
67 {
68    return project.AttachedObjects::Get< ProjectFileManager >( sFileManagerKey );
69 }
70 
Get(const AudacityProject & project)71 const ProjectFileManager &ProjectFileManager::Get( const AudacityProject &project )
72 {
73    return Get( const_cast< AudacityProject & >( project ) );
74 }
75 
DiscardAutosave(const FilePath & filename)76 void ProjectFileManager::DiscardAutosave(const FilePath &filename)
77 {
78    InvisibleTemporaryProject tempProject;
79    auto &project = tempProject.Project();
80    auto &projectFileManager = Get(project);
81    // Read the project, discarding autosave
82    projectFileManager.ReadProjectFile(filename, true);
83 
84    if (projectFileManager.mLastSavedTracks) {
85       for (auto wt : projectFileManager.mLastSavedTracks->Any<WaveTrack>())
86          wt->CloseLock();
87       projectFileManager.mLastSavedTracks.reset();
88    }
89 
90    // Side-effect on database is done, and destructor of tempProject
91    // closes the temporary project properly
92 }
93 
ProjectFileManager(AudacityProject & project)94 ProjectFileManager::ProjectFileManager( AudacityProject &project )
95 : mProject{ project }
96 {
97 }
98 
99 ProjectFileManager::~ProjectFileManager() = default;
100 
101 namespace {
102 
103 const char *const defaultHelpUrl =
104    "FAQ:Errors_on_opening_or_recovering_an_Audacity_project";
105 
106 using Pair = std::pair< const char *, const char * >;
107 const Pair helpURLTable[] = {
108    {
109       "not well-formed (invalid token)",
110       "Error:_not_well-formed_(invalid_token)_at_line_x"
111    },
112    {
113       "reference to invalid character number",
114       "Error_Opening_Project:_Reference_to_invalid_character_number_at_line_x"
115    },
116    {
117       "mismatched tag",
118       "#mismatched"
119    },
120 // This error with FAQ entry is reported elsewhere, not here....
121 //#[[#corrupt|Error Opening File or Project: File may be invalid or corrupted]]
122 };
123 
FindHelpUrl(const TranslatableString & libraryError)124 wxString FindHelpUrl( const TranslatableString &libraryError )
125 {
126    wxString helpUrl;
127    if ( !libraryError.empty() ) {
128       helpUrl = defaultHelpUrl;
129 
130       auto msgid = libraryError.MSGID().GET();
131       auto found = std::find_if( begin(helpURLTable), end(helpURLTable),
132          [&]( const Pair &pair ) {
133             return msgid.Contains( pair.first ); }
134       );
135       if (found != end(helpURLTable)) {
136          auto url = found->second;
137          if (url[0] == '#')
138             helpUrl += url;
139          else
140             helpUrl = url;
141       }
142    }
143 
144    return helpUrl;
145 }
146 
147 }
148 
ReadProjectFile(const FilePath & fileName,bool discardAutosave)149 auto ProjectFileManager::ReadProjectFile(
150    const FilePath &fileName, bool discardAutosave )
151   -> ReadProjectResults
152 {
153    auto &project = mProject;
154    auto &projectFileIO = ProjectFileIO::Get( project );
155    auto &window = GetProjectFrame( project );
156 
157    ///
158    /// Parse project file
159    ///
160    bool bParseSuccess = projectFileIO.LoadProject(fileName, discardAutosave);
161 
162    bool err = false;
163 
164    if (bParseSuccess)
165    {
166       if (discardAutosave)
167          // REVIEW: Failure OK?
168          projectFileIO.AutoSaveDelete();
169       else if (projectFileIO.IsRecovered()) {
170          bool resaved = false;
171 
172          if (!projectFileIO.IsTemporary())
173          {
174             // Re-save non-temporary project to its own path.  This
175             // might fail to update the document blob in the database.
176             resaved = projectFileIO.SaveProject(fileName, nullptr);
177          }
178 
179          AudacityMessageBox(
180             resaved
181                ? XO("This project was not saved properly the last time Audacity ran.\n\n"
182                     "It has been recovered to the last snapshot.")
183                : XO("This project was not saved properly the last time Audacity ran.\n\n"
184                     "It has been recovered to the last snapshot, but you must save it\n"
185                     "to preserve its contents."),
186             XO("Project Recovered"),
187             wxICON_WARNING,
188             &window);
189       }
190 
191       // By making a duplicate set of pointers to the existing blocks
192       // on disk, we add one to their reference count, guaranteeing
193       // that their reference counts will never reach zero and thus
194       // the version saved on disk will be preserved until the
195       // user selects Save().
196       mLastSavedTracks = TrackList::Create( nullptr );
197 
198       auto &tracks = TrackList::Get( project );
199       for (auto t : tracks.Any())
200       {
201          if (t->GetErrorOpening())
202          {
203             wxLogWarning(
204                wxT("Track %s had error reading clip values from project file."),
205                t->GetName());
206             err = true;
207          }
208 
209          err = ( !t->LinkConsistencyCheck() ) || err;
210 
211          mLastSavedTracks->Add(t->Duplicate());
212       }
213    }
214 
215    return
216    {
217       bParseSuccess,
218       err,
219       projectFileIO.GetLastError(),
220       FindHelpUrl(projectFileIO.GetLibraryError())
221    };
222 }
223 
Save()224 bool ProjectFileManager::Save()
225 {
226    auto &projectFileIO = ProjectFileIO::Get(mProject);
227 
228    // Prompt for file name?
229    if (projectFileIO.IsTemporary())
230    {
231       return SaveAs(true);
232    }
233 
234    return DoSave(projectFileIO.GetFileName(), false);
235 }
236 
237 #if 0
238 // I added this to "fix" bug #334.  At that time, we were on wxWidgets 2.8.12 and
239 // there was a window between the closing of the "Save" progress dialog and the
240 // end of the actual save where the user was able to close the project window and
241 // recursively enter the Save code (where they could inadvertently cause the issue
242 // described in #334).
243 //
244 // When we converted to wx3, this "disabler" caused focus problems when returning
245 // to the project after the save (bug #1172) because the focus and activate events
246 // weren't being dispatched and the focus would get lost.
247 //
248 // After some testing, it looks like the window described above no longer exists,
249 // so I've disabled the disabler.  However, I'm leaving it here in case we run
250 // into the problem in the future.  (even though it can't be used as-is)
251 class ProjectDisabler
252 {
253 public:
254    ProjectDisabler(wxWindow *w)
255    :  mWindow(w)
256    {
257       mWindow->GetEventHandler()->SetEvtHandlerEnabled(false);
258    }
259    ~ProjectDisabler()
260    {
261       mWindow->GetEventHandler()->SetEvtHandlerEnabled(true);
262    }
263 private:
264    wxWindow *mWindow;
265 };
266 #endif
267 
268 // Assumes ProjectFileIO::mFileName has been set to the desired path.
DoSave(const FilePath & fileName,const bool fromSaveAs)269 bool ProjectFileManager::DoSave(const FilePath & fileName, const bool fromSaveAs)
270 {
271    // See explanation above
272    // ProjectDisabler disabler(this);
273    auto &proj = mProject;
274    auto &window = GetProjectFrame( proj );
275    auto &projectFileIO = ProjectFileIO::Get( proj );
276    const auto &settings = ProjectSettings::Get( proj );
277 
278    // Some confirmation dialogs
279    {
280       if (TempDirectory::FATFilesystemDenied(fileName, XO("Projects cannot be saved to FAT drives.")))
281       {
282          return false;
283       }
284 
285       auto &tracks = TrackList::Get( proj );
286       if (!tracks.Any())
287       {
288          if (UndoManager::Get( proj ).UnsavedChanges() &&
289                settings.EmptyCanBeDirty())
290          {
291             int result = AudacityMessageBox(
292                XO(
293    "Your project is now empty.\nIf saved, the project will have no tracks.\n\nTo save any previously open tracks:\nClick 'No', Edit > Undo until all tracks\nare open, then File > Save Project.\n\nSave anyway?"),
294                XO("Warning - Empty Project"),
295                wxYES_NO | wxICON_QUESTION,
296                &window);
297             if (result == wxNO)
298             {
299                return false;
300             }
301          }
302       }
303 
304       wxULongLong fileSize = wxFileName::GetSize(projectFileIO.GetFileName());
305 
306       wxDiskspaceSize_t freeSpace;
307       if (wxGetDiskSpace(FileNames::AbbreviatePath(fileName), NULL, &freeSpace))
308       {
309          if (freeSpace.GetValue() <= fileSize.GetValue())
310          {
311             BasicUI::ShowErrorDialog( *ProjectFramePlacement( &proj ),
312                XO("Insufficient Disk Space"),
313                XO("The project size exceeds the available free space on the target disk.\n\n"
314                   "Please select a different disk with more free space."),
315                "Error:_Disk_full_or_not_writable"
316                );
317 
318             return false;
319          }
320       }
321    }
322    // End of confirmations
323 
324    // Always save a backup of the original project file
325    Optional<ProjectFileIO::BackupProject> pBackupProject;
326    if (fromSaveAs && wxFileExists(fileName))
327    {
328       pBackupProject.emplace(projectFileIO, fileName);
329       if (!pBackupProject->IsOk())
330          return false;
331    }
332 
333    if (FileNames::IsOnFATFileSystem(fileName))
334    {
335       if (wxFileName::GetSize(projectFileIO.GetFileName()) > UINT32_MAX)
336       {
337          BasicUI::ShowErrorDialog( *ProjectFramePlacement( &proj ),
338             XO("Error Saving Project"),
339             XO("The project exceeds the maximum size of 4GB when writing to a FAT32 formatted filesystem."),
340             "Error:_Unsuitable_drive"
341             );
342          return false;
343       }
344    }
345 
346    bool success = projectFileIO.SaveProject(fileName, mLastSavedTracks.get());
347    if (!success)
348    {
349       // Show this error only if we didn't fail reconnection in SaveProject
350       // REVIEW: Could HasConnection() be true but SaveProject() still have failed?
351       if (!projectFileIO.HasConnection()) {
352          using namespace BasicUI;
353          ShowErrorDialog( *ProjectFramePlacement( &proj ),
354             XO("Error Saving Project"),
355             FileException::WriteFailureMessage(fileName),
356             "Error:_Disk_full_or_not_writable",
357             ErrorDialogOptions{ ErrorDialogType::ModalErrorReport } );
358       }
359       return false;
360    }
361 
362    proj.SetProjectName(wxFileName(fileName).GetName());
363    projectFileIO.SetProjectTitle();
364 
365    UndoManager::Get(proj).StateSaved();
366    ProjectStatus::Get(proj).Set(XO("Saved %s").Format(fileName));
367 
368    if (mLastSavedTracks)
369    {
370       mLastSavedTracks->Clear();
371    }
372    mLastSavedTracks = TrackList::Create(nullptr);
373 
374    auto &tracks = TrackList::Get(proj);
375    for (auto t : tracks.Any())
376    {
377       mLastSavedTracks->Add(t->Duplicate());
378    }
379 
380    // If we get here, saving the project was successful, so we can DELETE
381    // any backup project.
382    if (pBackupProject)
383       pBackupProject->Discard();
384 
385    return true;
386 }
387 
388 // This version of SaveAs is invoked only from scripting and does not
389 // prompt for a file name
SaveAs(const FilePath & newFileName,bool addToHistory)390 bool ProjectFileManager::SaveAs(const FilePath &newFileName, bool addToHistory /*= true*/)
391 {
392    auto &project = mProject;
393    auto &projectFileIO = ProjectFileIO::Get( project );
394 
395    auto oldFileName = projectFileIO.GetFileName();
396 
397    bool bOwnsNewName = !projectFileIO.IsTemporary() && (oldFileName == newFileName);
398    //check to see if the NEW project file already exists.
399    //We should only overwrite it if this project already has the same name, where the user
400    //simply chose to use the save as command although the save command would have the effect.
401    if( !bOwnsNewName && wxFileExists(newFileName)) {
402       AudacityMessageDialog m(
403          nullptr,
404          XO("The project was not saved because the file name provided would overwrite another project.\nPlease try again and select an original name."),
405          XO("Error Saving Project"),
406          wxOK|wxICON_ERROR );
407       m.ShowModal();
408       return false;
409    }
410 
411    auto success = DoSave(newFileName, !bOwnsNewName);
412    if (success && addToHistory) {
413       FileHistory::Global().Append( projectFileIO.GetFileName() );
414    }
415 
416    return(success);
417 }
418 
SaveAs(bool allowOverwrite)419 bool ProjectFileManager::SaveAs(bool allowOverwrite /* = false */)
420 {
421    auto &project = mProject;
422    auto &projectFileIO = ProjectFileIO::Get( project );
423    auto &window = GetProjectFrame( project );
424    TitleRestorer Restorer( window, project ); // RAII
425    wxFileName filename;
426    FilePath defaultSavePath = FileNames::FindDefaultPath(FileNames::Operation::Save);
427 
428    if (projectFileIO.IsTemporary()) {
429       filename.SetPath(defaultSavePath);
430       filename.SetName(project.GetProjectName());
431    }
432    else {
433       filename = projectFileIO.GetFileName();
434    }
435 
436    // Bug 1304: Set a default file path if none was given.  For Save/SaveAs/SaveCopy
437    if( !FileNames::IsPathAvailable( filename.GetPath( wxPATH_GET_VOLUME| wxPATH_GET_SEPARATOR) ) ){
438       filename.SetPath(defaultSavePath);
439    }
440 
441    TranslatableString title = XO("%sSave Project \"%s\" As...")
442       .Format( Restorer.sProjNumber, Restorer.sProjName );
443    TranslatableString message = XO("\
444 'Save Project' is for an Audacity project, not an audio file.\n\
445 For an audio file that will open in other apps, use 'Export'.\n");
446 
447    if (ShowWarningDialog(&window, wxT("FirstProjectSave"), message, true) != wxID_OK) {
448       return false;
449    }
450 
451    bool bPrompt = (project.mBatchMode == 0) || (projectFileIO.GetFileName().empty());
452    FilePath fName;
453    bool bOwnsNewName;
454 
455    do {
456       if (bPrompt) {
457          // JKC: I removed 'wxFD_OVERWRITE_PROMPT' because we are checking
458          // for overwrite ourselves later, and we disallow it.
459          fName = SelectFile(FileNames::Operation::Save,
460             title,
461             filename.GetPath(),
462             filename.GetFullName(),
463             wxT("aup3"),
464             { FileNames::AudacityProjects },
465             wxFD_SAVE | wxRESIZE_BORDER,
466             &window);
467 
468          if (fName.empty())
469             return false;
470 
471          filename = fName;
472       };
473 
474       filename.SetExt(wxT("aup3"));
475 
476       if ((!bPrompt || !allowOverwrite) && filename.FileExists()) {
477          // Saving a copy of the project should never overwrite an existing project.
478          AudacityMessageDialog m(
479             nullptr,
480             XO("The project was not saved because the file name provided would overwrite another project.\nPlease try again and select an original name."),
481             XO("Error Saving Project"),
482             wxOK|wxICON_ERROR );
483          m.ShowModal();
484          return false;
485       }
486 
487       fName = filename.GetFullPath();
488 
489       bOwnsNewName = !projectFileIO.IsTemporary() && ( projectFileIO.GetFileName() == fName );
490       // Check to see if the project file already exists, and if it does
491       // check that the project file 'belongs' to this project.
492       // otherwise, prompt the user before overwriting.
493       if (!bOwnsNewName && filename.FileExists()) {
494          // Ensure that project of same name is not open in another window.
495          // fName is the destination file.
496          // mFileName is this project.
497          // It is possible for mFileName == fName even when this project is not
498          // saved to disk, and we then need to check the destination file is not
499          // open in another window.
500          int mayOverwrite = ( projectFileIO.GetFileName() == fName ) ? 2 : 1;
501          for ( auto p : AllProjects{} ) {
502             const wxFileName openProjectName{ ProjectFileIO::Get(*p).GetFileName() };
503             if (openProjectName.SameAs(fName)) {
504                mayOverwrite -= 1;
505                if (mayOverwrite == 0)
506                   break;
507             }
508          }
509 
510          if (mayOverwrite > 0) {
511             /* i18n-hint: In each case, %s is the name
512              of the file being overwritten.*/
513             auto Message = XO("\
514    Do you want to overwrite the project:\n\"%s\"?\n\n\
515    If you select \"Yes\" the project\n\"%s\"\n\
516    will be irreversibly overwritten.").Format( fName, fName );
517 
518             // For safety, there should NOT be an option to hide this warning.
519             int result = AudacityMessageBox(
520                Message,
521                /* i18n-hint: Heading: A warning that a project is about to be overwritten.*/
522                XO("Overwrite Project Warning"),
523                wxYES_NO | wxNO_DEFAULT | wxICON_WARNING,
524                &window);
525             if (result == wxNO) {
526                continue;
527             }
528             if (result == wxCANCEL) {
529                return false;
530             }
531          }
532          else {
533             // Overwrite disallowed. The destination project is open in another window.
534             AudacityMessageDialog m(
535                nullptr,
536                XO("The project was not saved because the selected project is open in another window.\nPlease try again and select an original name."),
537                XO("Error Saving Project"),
538                wxOK|wxICON_ERROR );
539             m.ShowModal();
540             continue;
541          }
542       }
543 
544       break;
545    } while (bPrompt);
546 
547 
548    auto success = DoSave(fName, !bOwnsNewName);
549    if (success) {
550       FileHistory::Global().Append( projectFileIO.GetFileName() );
551    }
552 
553    return(success);
554 }
555 
SaveCopy(const FilePath & fileName)556 bool ProjectFileManager::SaveCopy(const FilePath &fileName /* = wxT("") */)
557 {
558    auto &project = mProject;
559    auto &projectFileIO = ProjectFileIO::Get(project);
560    auto &window = GetProjectFrame(project);
561    TitleRestorer Restorer(window, project); // RAII
562    wxFileName filename = fileName;
563    FilePath defaultSavePath = FileNames::FindDefaultPath(FileNames::Operation::Save);
564 
565    if (fileName.empty())
566    {
567       if (projectFileIO.IsTemporary())
568       {
569          filename.SetPath(defaultSavePath);
570       }
571       else
572       {
573          filename = projectFileIO.GetFileName();
574       }
575    }
576 
577    // Bug 1304: Set a default file path if none was given.  For Save/SaveAs/SaveCopy
578    if (!FileNames::IsPathAvailable(filename.GetPath(wxPATH_GET_VOLUME | wxPATH_GET_SEPARATOR)))
579    {
580       filename.SetPath(defaultSavePath);
581    }
582 
583    TranslatableString title =
584       XO("%sSave Copy of Project \"%s\" As...")
585          .Format(Restorer.sProjNumber, Restorer.sProjName);
586 
587    bool bPrompt = (project.mBatchMode == 0) || (projectFileIO.GetFileName().empty());
588    FilePath fName;
589 
590    do
591    {
592       if (bPrompt)
593       {
594          // JKC: I removed 'wxFD_OVERWRITE_PROMPT' because we are checking
595          // for overwrite ourselves later, and we disallow it.
596          // Previously we disallowed overwrite because we would have had
597          // to DELETE the many smaller files too, or prompt to move them.
598          // Maybe we could allow it now that we have aup3 format?
599          fName = SelectFile(FileNames::Operation::Export,
600                                        title,
601                                        filename.GetPath(),
602                                        filename.GetFullName(),
603                                        wxT("aup3"),
604                                        { FileNames::AudacityProjects },
605                                        wxFD_SAVE | wxRESIZE_BORDER,
606                                        &window);
607 
608          if (fName.empty())
609          {
610             return false;
611          }
612 
613          filename = fName;
614       };
615 
616       filename.SetExt(wxT("aup3"));
617 
618       if (TempDirectory::FATFilesystemDenied(filename.GetFullPath(), XO("Projects cannot be saved to FAT drives.")))
619       {
620          if (project.mBatchMode)
621          {
622             return false;
623          }
624 
625          continue;
626       }
627 
628       if (filename.FileExists())
629       {
630          // Saving a copy of the project should never overwrite an existing project.
631          AudacityMessageDialog m(nullptr,
632                                  XO("Saving a copy must not overwrite an existing saved project.\nPlease try again and select an original name."),
633                                  XO("Error Saving Copy of Project"),
634                                  wxOK | wxICON_ERROR);
635          m.ShowModal();
636 
637          if (project.mBatchMode)
638          {
639             return false;
640          }
641 
642          continue;
643       }
644 
645       wxULongLong fileSize = wxFileName::GetSize(projectFileIO.GetFileName());
646 
647       wxDiskspaceSize_t freeSpace;
648       if (wxGetDiskSpace(FileNames::AbbreviatePath(filename.GetFullPath()), NULL, &freeSpace))
649       {
650          if (freeSpace.GetValue() <= fileSize.GetValue())
651          {
652             BasicUI::ShowErrorDialog( *ProjectFramePlacement( &project ),
653                XO("Insufficient Disk Space"),
654                XO("The project size exceeds the available free space on the target disk.\n\n"
655                   "Please select a different disk with more free space."),
656                "Error:_Unsuitable_drive"
657                );
658 
659             continue;
660          }
661       }
662 
663       if (FileNames::IsOnFATFileSystem(filename.GetFullPath()))
664       {
665          if (fileSize > UINT32_MAX)
666          {
667             BasicUI::ShowErrorDialog( *ProjectFramePlacement( &project ),
668                XO("Error Saving Project"),
669                XO("The project exceeds the maximum size of 4GB when writing to a FAT32 formatted filesystem."),
670                "Error:_Unsuitable_drive"
671                );
672 
673             if (project.mBatchMode)
674             {
675                return false;
676             }
677 
678             continue;
679          }
680       }
681 
682       fName = filename.GetFullPath();
683       break;
684    } while (bPrompt);
685 
686    if (!projectFileIO.SaveCopy(fName))
687    {
688       auto msg = FileException::WriteFailureMessage(fName);
689       AudacityMessageDialog m(
690          nullptr, msg, XO("Error Saving Project"), wxOK | wxICON_ERROR);
691 
692       m.ShowModal();
693 
694       return false;
695    }
696 
697    return true;
698 }
699 
SaveFromTimerRecording(wxFileName fnFile)700 bool ProjectFileManager::SaveFromTimerRecording(wxFileName fnFile)
701 {
702    auto &project = mProject;
703    auto &projectFileIO = ProjectFileIO::Get( project );
704 
705    // MY: Will save the project to a NEW location a-la Save As
706    // and then tidy up after itself.
707 
708    wxString sNewFileName = fnFile.GetFullPath();
709 
710    // MY: To allow SaveAs from Timer Recording we need to check what
711    // the value of mFileName is before we change it.
712    FilePath sOldFilename;
713    if (!projectFileIO.IsModified()) {
714       sOldFilename = projectFileIO.GetFileName();
715    }
716 
717    // MY: If the project file already exists then bail out
718    // and send populate the message string (pointer) so
719    // we can tell the user what went wrong.
720    if (wxFileExists(sNewFileName)) {
721       return false;
722    }
723 
724    auto success = DoSave(sNewFileName, true);
725 
726    if (success) {
727       FileHistory::Global().Append( projectFileIO.GetFileName() );
728    }
729 
730    return success;
731 }
732 
CompactProjectOnClose()733 void ProjectFileManager::CompactProjectOnClose()
734 {
735    auto &project = mProject;
736    auto &projectFileIO = ProjectFileIO::Get(project);
737 
738    // Lock all blocks in all tracks of the last saved version, so that
739    // the sample blocks aren't deleted from the database when we destroy the
740    // sample block objects in memory.
741    if (mLastSavedTracks)
742    {
743       for (auto wt : mLastSavedTracks->Any<WaveTrack>())
744       {
745          wt->CloseLock();
746       }
747 
748       // Attempt to compact the project
749       projectFileIO.Compact( { mLastSavedTracks.get() } );
750 
751       if ( !projectFileIO.WasCompacted() &&
752           UndoManager::Get( project ).UnsavedChanges() ) {
753          // If compaction failed, we must do some work in case of close
754          // without save.  Don't leave the document blob from the last
755          // push of undo history, when that undo state may get purged
756          // with deletion of some new sample blocks.
757          // REVIEW: UpdateSaved() might fail too.  Do we need to test
758          // for that and report it?
759          projectFileIO.UpdateSaved( mLastSavedTracks.get() );
760       }
761    }
762 }
763 
OpenProject()764 bool ProjectFileManager::OpenProject()
765 {
766    auto &project = mProject;
767    auto &projectFileIO = ProjectFileIO::Get(project);
768 
769    return projectFileIO.OpenProject();
770 }
771 
OpenNewProject()772 bool ProjectFileManager::OpenNewProject()
773 {
774    auto &project = mProject;
775    auto &projectFileIO = ProjectFileIO::Get(project);
776 
777    bool bOK = OpenProject();
778    if( !bOK )
779    {
780        auto tmpdir = wxFileName(TempDirectory::UnsavedProjectFileName()).GetPath();
781 
782        UnwritableLocationErrorDialog dlg(nullptr, tmpdir);
783        dlg.ShowModal();
784    }
785    return bOK;
786 }
787 
CloseProject()788 void ProjectFileManager::CloseProject()
789 {
790    auto &project = mProject;
791    auto &projectFileIO = ProjectFileIO::Get(project);
792 
793    projectFileIO.CloseProject();
794 
795    // Blocks were locked in CompactProjectOnClose, so DELETE the data structure so that
796    // there's no memory leak.
797    if (mLastSavedTracks)
798    {
799       mLastSavedTracks->Clear();
800       mLastSavedTracks.reset();
801    }
802 }
803 
804 // static method, can be called outside of a project
ShowOpenDialog(FileNames::Operation op,const FileNames::FileType & extraType)805 wxArrayString ProjectFileManager::ShowOpenDialog(FileNames::Operation op,
806    const FileNames::FileType &extraType )
807 {
808    // Construct the filter
809    const auto fileTypes = Importer::Get().GetFileTypes( extraType );
810 
811    // Retrieve saved path
812    auto path = FileNames::FindDefaultPath(op);
813 
814    // Construct and display the file dialog
815    wxArrayString selected;
816 
817    FileDialogWrapper dlog(nullptr,
818       XO("Select one or more files"),
819       path,
820       wxT(""),
821       fileTypes,
822       wxFD_OPEN | wxFD_MULTIPLE | wxRESIZE_BORDER);
823 
824    dlog.SetFilterIndex( Importer::SelectDefaultOpenType( fileTypes ) );
825 
826    int dialogResult = dlog.ShowModal();
827 
828    // Convert the filter index to type and save
829    auto index = dlog.GetFilterIndex();
830    const auto &saveType = fileTypes[ index ];
831 
832    Importer::SetDefaultOpenType( saveType );
833    Importer::SetLastOpenType( saveType );
834 
835    if (dialogResult == wxID_OK) {
836       // Return the selected files
837       dlog.GetPaths(selected);
838 
839       // Remember the directory
840       FileNames::UpdateDefaultPath(op, ::wxPathOnly(dlog.GetPath()));
841    }
842 
843    return selected;
844 }
845 
846 // static method, can be called outside of a project
IsAlreadyOpen(const FilePath & projPathName)847 bool ProjectFileManager::IsAlreadyOpen(const FilePath &projPathName)
848 {
849    const wxFileName newProjPathName(projPathName);
850    auto start = AllProjects{}.begin(), finish = AllProjects{}.end(),
851    iter = std::find_if( start, finish,
852       [&]( const AllProjects::value_type &ptr ){
853          return newProjPathName.SameAs(wxFileNameWrapper{ ProjectFileIO::Get(*ptr).GetFileName() });
854       } );
855    if (iter != finish) {
856       auto errMsg =
857       XO("%s is already open in another window.")
858          .Format( newProjPathName.GetName() );
859       wxLogError(errMsg.Translation()); //Debug?
860       AudacityMessageBox(
861          errMsg,
862          XO("Error Opening Project"),
863          wxOK | wxCENTRE);
864       return true;
865    }
866    return false;
867 }
868 
OpenFile(const ProjectChooserFn & chooser,const FilePath & fileNameArg,bool addtohistory)869 AudacityProject *ProjectFileManager::OpenFile( const ProjectChooserFn &chooser,
870    const FilePath &fileNameArg, bool addtohistory)
871 {
872    // On Win32, we may be given a short (DOS-compatible) file name on rare
873    // occasions (e.g. stuff like "C:\PROGRA~1\AUDACI~1\PROJEC~1.AUP"). We
874    // convert these to long file name first.
875    auto fileName = PlatformCompatibility::GetLongFileName(fileNameArg);
876 
877    // Make sure it isn't already open.
878    // Vaughan, 2011-03-25: This was done previously in AudacityProject::OpenFiles()
879    //    and AudacityApp::MRUOpen(), but if you open an aup file by double-clicking it
880    //    from, e.g., Win Explorer, it would bypass those, get to here with no check,
881    //    then open a NEW project from the same data with no warning.
882    //    This was reported in http://bugzilla.audacityteam.org/show_bug.cgi?id=137#c17,
883    //    but is not really part of that bug. Anyway, prevent it!
884    if (IsAlreadyOpen(fileName))
885       return nullptr;
886 
887    // Data loss may occur if users mistakenly try to open ".aup3.bak" files
888    // left over from an unsuccessful save or by previous versions of Audacity.
889    // So we always refuse to open such files.
890    if (fileName.Lower().EndsWith(wxT(".aup3.bak")))
891    {
892       AudacityMessageBox(
893          XO(
894 "You are trying to open an automatically created backup file.\nDoing this may result in severe data loss.\n\nPlease open the actual Audacity project file instead."),
895          XO("Warning - Backup File Detected"),
896          wxOK | wxCENTRE,
897          nullptr);
898       return nullptr;
899    }
900 
901    if (!::wxFileExists(fileName)) {
902       AudacityMessageBox(
903          XO("Could not open file: %s").Format( fileName ),
904          XO("Error Opening File"),
905          wxOK | wxCENTRE,
906          nullptr);
907       return nullptr;
908    }
909 
910    // Following block covers cases other than a project file:
911    {
912       wxFFile ff(fileName, wxT("rb"));
913 
914       auto cleanup = finally([&]
915       {
916          if (ff.IsOpened())
917          {
918             ff.Close();
919          }
920       });
921 
922       if (!ff.IsOpened()) {
923          AudacityMessageBox(
924             XO("Could not open file: %s").Format( fileName ),
925             XO("Error opening file"),
926             wxOK | wxCENTRE,
927             nullptr);
928          return nullptr;
929       }
930 
931       char buf[7];
932       auto numRead = ff.Read(buf, 6);
933       if (numRead != 6) {
934          AudacityMessageBox(
935             XO("File may be invalid or corrupted: \n%s").Format( fileName ),
936             XO("Error Opening File or Project"),
937             wxOK | wxCENTRE,
938             nullptr);
939          return nullptr;
940       }
941 
942       if (wxStrncmp(buf, "SQLite", 6) != 0)
943       {
944          // Not a database
945 #ifdef EXPERIMENTAL_DRAG_DROP_PLUG_INS
946          // Is it a plug-in?
947          if (PluginManager::Get().DropFile(fileName)) {
948             MenuCreator::RebuildAllMenuBars();
949             // Plug-in installation happened, not really opening of a file,
950             // so return null
951             return nullptr;
952          }
953 #endif
954 #ifdef USE_MIDI
955          if (FileNames::IsMidi(fileName)) {
956             auto &project = chooser(false);
957             // If this succeeds, indo history is incremented, and it also does
958             // ZoomAfterImport:
959             if(DoImportMIDI(project, fileName))
960                return &project;
961             return nullptr;
962          }
963 #endif
964          auto &project = chooser(false);
965          // Undo history is incremented inside this:
966          if (Get(project).Import(fileName)) {
967             // Undo history is incremented inside this:
968             // Bug 2743: Don't zoom with lof.
969             if (!fileName.AfterLast('.').IsSameAs(wxT("lof"), false))
970                ProjectWindow::Get(project).ZoomAfterImport(nullptr);
971             return &project;
972          }
973          return nullptr;
974       }
975    }
976 
977    // Disallow opening of .aup3 project files from FAT drives, but only such
978    // files, not importable types.  (Bug 2800)
979    if (TempDirectory::FATFilesystemDenied(fileName,
980       XO("Project resides on FAT formatted drive.\n"
981         "Copy it to another drive to open it.")))
982    {
983       return nullptr;
984    }
985 
986    auto &project = chooser(true);
987    return Get(project).OpenProjectFile(fileName, addtohistory);
988 }
989 
OpenProjectFile(const FilePath & fileName,bool addtohistory)990 AudacityProject *ProjectFileManager::OpenProjectFile(
991    const FilePath &fileName, bool addtohistory)
992 {
993    auto &project = mProject;
994    auto &history = ProjectHistory::Get( project );
995    auto &tracks = TrackList::Get( project );
996    auto &trackPanel = TrackPanel::Get( project );
997    auto &projectFileIO = ProjectFileIO::Get( project );
998    auto &window = ProjectWindow::Get( project );
999 
1000    auto results = ReadProjectFile( fileName );
1001    const bool bParseSuccess = results.parseSuccess;
1002    const auto &errorStr = results.errorString;
1003    const bool err = results.trackError;
1004 
1005    if (bParseSuccess) {
1006       auto &settings = ProjectSettings::Get( project );
1007       window.mbInitializingScrollbar = true; // this must precede AS_SetSnapTo
1008          // to make persistence of the vertical scrollbar position work
1009 
1010       auto &selectionManager = ProjectSelectionManager::Get( project );
1011       selectionManager.AS_SetSnapTo(settings.GetSnapTo());
1012       selectionManager.AS_SetSelectionFormat(settings.GetSelectionFormat());
1013       selectionManager.TT_SetAudioTimeFormat(settings.GetAudioTimeFormat());
1014       selectionManager.SSBL_SetFrequencySelectionFormatName(
1015       settings.GetFrequencySelectionFormatName());
1016       selectionManager.SSBL_SetBandwidthSelectionFormatName(
1017       settings.GetBandwidthSelectionFormatName());
1018 
1019       SelectionBar::Get( project )
1020          .SetRate( ProjectRate::Get(project).GetRate() );
1021 
1022       ProjectHistory::Get( project ).InitialState();
1023       TrackFocus::Get( project ).Set( *tracks.Any().begin() );
1024       window.HandleResize();
1025       trackPanel.Refresh(false);
1026 
1027       // ? Old rationale in this comment no longer applies in 3.0.0, with no
1028       // more on-demand loading:
1029       trackPanel.Update(); // force any repaint to happen now,
1030       // else any asynch calls into the blockfile code will not have
1031       // finished logging errors (if any) before the call to ProjectFSCK()
1032 
1033       if (addtohistory)
1034          FileHistory::Global().Append(fileName);
1035    }
1036 
1037    if (bParseSuccess) {
1038       if (projectFileIO.IsRecovered())
1039       {
1040          // PushState calls AutoSave(), so no longer need to do so here.
1041          history.PushState(XO("Project was recovered"), XO("Recover"));
1042       }
1043       return &project;
1044    }
1045    else {
1046       // Vaughan, 2011-10-30:
1047       // See first topic at http://bugzilla.audacityteam.org/show_bug.cgi?id=451#c16.
1048       // Calling mTracks->Clear() with deleteTracks true results in data loss.
1049 
1050       // PRL 2014-12-19:
1051       // I made many changes for wave track memory management, but only now
1052       // read the above comment.  I may have invalidated the fix above (which
1053       // may have spared the files at the expense of leaked memory).  But
1054       // here is a better way to accomplish the intent, doing like what happens
1055       // when the project closes:
1056       for ( auto pTrack : tracks.Any< WaveTrack >() )
1057          pTrack->CloseLock();
1058 
1059       tracks.Clear(); //tracks.Clear(true);
1060 
1061       wxLogError(wxT("Could not parse file \"%s\". \nError: %s"), fileName, errorStr.Debug());
1062 
1063       projectFileIO.ShowError( *ProjectFramePlacement(&project),
1064          XO("Error Opening Project"),
1065          errorStr,
1066          results.helpUrl);
1067 
1068       return nullptr;
1069    }
1070 }
1071 
1072 void
AddImportedTracks(const FilePath & fileName,TrackHolders && newTracks)1073 ProjectFileManager::AddImportedTracks(const FilePath &fileName,
1074                                       TrackHolders &&newTracks)
1075 {
1076    auto &project = mProject;
1077    auto &history = ProjectHistory::Get( project );
1078    auto &projectFileIO = ProjectFileIO::Get( project );
1079    auto &tracks = TrackList::Get( project );
1080 
1081    std::vector< std::shared_ptr< Track > > results;
1082 
1083    SelectUtilities::SelectNone( project );
1084 
1085    wxFileName fn(fileName);
1086 
1087    bool initiallyEmpty = tracks.empty();
1088    double newRate = 0;
1089    wxString trackNameBase = fn.GetName();
1090    int i = -1;
1091 
1092    // Fix the bug 2109.
1093    // In case the project had soloed tracks before importing,
1094    // all newly imported tracks are muted.
1095    const bool projectHasSolo =
1096       !(tracks.Any<PlayableTrack>() + &PlayableTrack::GetSolo).empty();
1097    if (projectHasSolo)
1098    {
1099       for (auto& track : newTracks)
1100          for (auto& channel : track)
1101             channel->SetMute(true);
1102    }
1103 
1104    // Must add all tracks first (before using Track::IsLeader)
1105    for (auto &group : newTracks) {
1106       if (group.empty()) {
1107          wxASSERT(false);
1108          continue;
1109       }
1110       auto first = group.begin()->get();
1111       auto nChannels = group.size();
1112       for (auto &uNewTrack : group) {
1113          auto newTrack = tracks.Add( uNewTrack );
1114          results.push_back(newTrack->SharedPointer());
1115       }
1116       tracks.MakeMultiChannelTrack(*first, nChannels, true);
1117    }
1118    newTracks.clear();
1119 
1120    // Now name them
1121 
1122    // Add numbers to track names only if there is more than one (mono or stereo)
1123    // track (not necessarily, more than one channel)
1124    const bool useSuffix =
1125       make_iterator_range( results.begin() + 1, results.end() )
1126          .any_of( []( decltype(*results.begin()) &pTrack )
1127             { return pTrack->IsLeader(); } );
1128 
1129    for (const auto &newTrack : results) {
1130       if ( newTrack->IsLeader() )
1131          // Count groups only
1132          ++i;
1133 
1134       newTrack->SetSelected(true);
1135 
1136 
1137       if (useSuffix)
1138           //i18n-hint Name default name assigned to a clip on track import
1139           newTrack->SetName(XC("%s %d", "clip name template").Format(trackNameBase, i + 1).Translation());
1140       else
1141           newTrack->SetName(trackNameBase);
1142 
1143       newTrack->TypeSwitch([&](WaveTrack *wt) {
1144          if (newRate == 0)
1145             newRate = wt->GetRate();
1146          auto trackName = wt->GetName();
1147          for (auto& clip : wt->GetClips())
1148             clip->SetName(trackName);
1149       });
1150    }
1151 
1152    // Automatically assign rate of imported file to whole project,
1153    // if this is the first file that is imported
1154    if (initiallyEmpty && newRate > 0) {
1155       ProjectRate::Get(project).SetRate( newRate );
1156       SelectionBar::Get( project ).SetRate( newRate );
1157    }
1158 
1159    history.PushState(XO("Imported '%s'").Format( fileName ),
1160        XO("Import"));
1161 
1162 #if defined(__WXGTK__)
1163    // See bug #1224
1164    // The track panel hasn't we been fully created, so the DoZoomFit() will not give
1165    // expected results due to a window width of zero.  Should be safe to yield here to
1166    // allow the creation to complete.  If this becomes a problem, it "might" be possible
1167    // to queue a dummy event to trigger the DoZoomFit().
1168    wxEventLoopBase::GetActive()->YieldFor(wxEVT_CATEGORY_UI | wxEVT_CATEGORY_USER_INPUT);
1169 #endif
1170 
1171    // If the project was clean and temporary (not permanently saved), then set
1172    // the filename to the just imported path.
1173    if (initiallyEmpty && projectFileIO.IsTemporary()) {
1174       project.SetProjectName(fn.GetName());
1175       project.SetInitialImportPath(fn.GetPath());
1176       projectFileIO.SetProjectTitle();
1177    }
1178 
1179    // Moved this call to higher levels to prevent flicker redrawing everything on each file.
1180    //   HandleResize();
1181 }
1182 
1183 namespace {
ImportProject(AudacityProject & dest,const FilePath & fileName)1184 bool ImportProject(AudacityProject &dest, const FilePath &fileName)
1185 {
1186    InvisibleTemporaryProject temp;
1187    auto &project = temp.Project();
1188 
1189    auto &projectFileIO = ProjectFileIO::Get(project);
1190    if (!projectFileIO.LoadProject(fileName, false))
1191       return false;
1192    auto &srcTracks = TrackList::Get(project);
1193    auto &destTracks = TrackList::Get(dest);
1194    for (const Track *pTrack : srcTracks.Any()) {
1195       auto destTrack = pTrack->PasteInto(dest);
1196       Track::FinishCopy(pTrack, destTrack.get());
1197       if (destTrack.use_count() == 1)
1198          destTracks.Add(destTrack);
1199    }
1200    Tags::Get(dest).Merge(Tags::Get(project));
1201 
1202    return true;
1203 }
1204 }
1205 
1206 // If pNewTrackList is passed in non-NULL, it gets filled with the pointers to NEW tracks.
Import(const FilePath & fileName,bool addToHistory)1207 bool ProjectFileManager::Import(
1208    const FilePath &fileName,
1209    bool addToHistory /* = true */)
1210 {
1211    auto &project = mProject;
1212    auto &projectFileIO = ProjectFileIO::Get(project);
1213    auto oldTags = Tags::Get( project ).shared_from_this();
1214    bool initiallyEmpty = TrackList::Get(project).empty();
1215    TrackHolders newTracks;
1216    TranslatableString errorMessage;
1217 
1218 #ifdef EXPERIMENTAL_IMPORT_AUP3
1219    // Handle AUP3 ("project") files directly
1220    if (fileName.AfterLast('.').IsSameAs(wxT("aup3"), false)) {
1221       if (ImportProject(project, fileName)) {
1222          auto &history = ProjectHistory::Get(project);
1223 
1224          // If the project was clean and temporary (not permanently saved), then set
1225          // the filename to the just imported path.
1226          if (initiallyEmpty && projectFileIO.IsTemporary()) {
1227             wxFileName fn(fileName);
1228             project.SetProjectName(fn.GetName());
1229             project.SetInitialImportPath(fn.GetPath());
1230             projectFileIO.SetProjectTitle();
1231          }
1232 
1233          history.PushState(XO("Imported '%s'").Format(fileName), XO("Import"));
1234 
1235          if (addToHistory) {
1236             FileHistory::Global().Append(fileName);
1237          }
1238       }
1239       else {
1240          errorMessage = projectFileIO.GetLastError();
1241          if (errorMessage.empty()) {
1242             errorMessage = XO("Failed to import project");
1243          }
1244 
1245          // Additional help via a Help button links to the manual.
1246          ShowErrorDialog( *ProjectFramePlacement(&project),
1247             XO("Error Importing"),
1248             errorMessage, wxT("Importing_Audio"));
1249       }
1250 
1251       return false;
1252    }
1253 #endif
1254 
1255    {
1256       // Backup Tags, before the import.  Be prepared to roll back changes.
1257       bool committed = false;
1258       auto cleanup = finally([&]{
1259          if ( !committed )
1260             Tags::Set( project, oldTags );
1261       });
1262       auto newTags = oldTags->Duplicate();
1263       Tags::Set( project, newTags );
1264 
1265 #ifndef EXPERIMENTAL_IMPORT_AUP3
1266       // Handle AUP3 ("project") files specially
1267       if (fileName.AfterLast('.').IsSameAs(wxT("aup3"), false)) {
1268          BasicUI::ShowErrorDialog( *ProjectFramePlacement(&project),
1269             XO("Error Importing"),
1270             XO( "Cannot import AUP3 format.  Use File > Open instead"),
1271             wxT("File_Menu"));
1272          return false;
1273       }
1274 #endif
1275       bool success = Importer::Get().Import(project, fileName,
1276                                             &WaveTrackFactory::Get( project ),
1277                                             newTracks,
1278                                             newTags.get(),
1279                                             errorMessage);
1280       if (!errorMessage.empty()) {
1281          // Error message derived from Importer::Import
1282          // Additional help via a Help button links to the manual.
1283          BasicUI::ShowErrorDialog( *ProjectFramePlacement(&project),
1284             XO("Error Importing"), errorMessage, wxT("Importing_Audio"));
1285       }
1286       if (!success)
1287          return false;
1288 
1289       if (addToHistory) {
1290          FileHistory::Global().Append(fileName);
1291       }
1292 
1293       // no more errors, commit
1294       committed = true;
1295    }
1296 
1297    // for LOF ("list of files") files, do not import the file as if it
1298    // were an audio file itself
1299    if (fileName.AfterLast('.').IsSameAs(wxT("lof"), false)) {
1300       // PRL: don't redundantly do the steps below, because we already
1301       // did it in case of LOF, because of some weird recursion back to this
1302       // same function.  I think this should be untangled.
1303 
1304       // So Undo history push is not bypassed, despite appearances.
1305       return false;
1306    }
1307 
1308    // Handle AUP ("legacy project") files directly
1309    if (fileName.AfterLast('.').IsSameAs(wxT("aup"), false)) {
1310       // If the project was clean and temporary (not permanently saved), then set
1311       // the filename to the just imported path.
1312       if (initiallyEmpty && projectFileIO.IsTemporary()) {
1313          wxFileName fn(fileName);
1314          project.SetProjectName(fn.GetName());
1315          project.SetInitialImportPath(fn.GetPath());
1316          projectFileIO.SetProjectTitle();
1317       }
1318 
1319       auto &history = ProjectHistory::Get( project );
1320 
1321       history.PushState(XO("Imported '%s'").Format( fileName ), XO("Import"));
1322 
1323       return true;
1324    }
1325 
1326    // PRL: Undo history is incremented inside this:
1327    AddImportedTracks(fileName, std::move(newTracks));
1328 
1329    return true;
1330 }
1331 
1332 #include "Clipboard.h"
1333 #include "ShuttleGui.h"
1334 #include "widgets/HelpSystem.h"
1335 
1336 // Compact dialog
1337 namespace {
1338 class CompactDialog : public wxDialogWrapper
1339 {
1340 public:
CompactDialog(TranslatableString text)1341    CompactDialog(TranslatableString text)
1342    :  wxDialogWrapper(nullptr, wxID_ANY, XO("Compact Project"))
1343    {
1344       ShuttleGui S(this, eIsCreating);
1345 
1346       S.StartVerticalLay(true);
1347       {
1348          S.AddFixedText(text, false, 500);
1349 
1350          S.AddStandardButtons(eYesButton | eNoButton | eHelpButton);
1351       }
1352       S.EndVerticalLay();
1353 
1354       FindWindowById(wxID_YES, this)->Bind(wxEVT_BUTTON, &CompactDialog::OnYes, this);
1355       FindWindowById(wxID_NO, this)->Bind(wxEVT_BUTTON, &CompactDialog::OnNo, this);
1356       FindWindowById(wxID_HELP, this)->Bind(wxEVT_BUTTON, &CompactDialog::OnGetURL, this);
1357 
1358       Layout();
1359       Fit();
1360       Center();
1361    }
1362 
OnYes(wxCommandEvent & WXUNUSED (evt))1363    void OnYes(wxCommandEvent &WXUNUSED(evt))
1364    {
1365       EndModal(wxYES);
1366    }
1367 
OnNo(wxCommandEvent & WXUNUSED (evt))1368    void OnNo(wxCommandEvent &WXUNUSED(evt))
1369    {
1370       EndModal(wxNO);
1371    }
1372 
OnGetURL(wxCommandEvent & WXUNUSED (evt))1373    void OnGetURL(wxCommandEvent &WXUNUSED(evt))
1374    {
1375       HelpSystem::ShowHelp(this, L"File_Menu:_Compact_Project", true);
1376    }
1377 };
1378 }
1379 
Compact()1380 void ProjectFileManager::Compact()
1381 {
1382    auto &project = mProject;
1383    auto &undoManager = UndoManager::Get(project);
1384    auto &clipboard = Clipboard::Get();
1385    auto &projectFileIO = ProjectFileIO::Get(project);
1386    bool isBatch = project.mBatchMode > 0;
1387 
1388    // Purpose of this is to remove the -wal file.
1389    projectFileIO.ReopenProject();
1390 
1391    auto savedState = undoManager.GetSavedState();
1392    const auto currentState = undoManager.GetCurrentState();
1393    if (savedState < 0) {
1394       undoManager.StateSaved();
1395       savedState = undoManager.GetSavedState();
1396       if (savedState < 0) {
1397          wxASSERT(false);
1398          savedState = 0;
1399       }
1400    }
1401    const auto least = std::min<size_t>(savedState, currentState);
1402    const auto greatest = std::max<size_t>(savedState, currentState);
1403    std::vector<const TrackList*> trackLists;
1404    auto fn = [&](auto& elem){
1405       trackLists.push_back(elem.state.tracks.get()); };
1406    undoManager.VisitStates(fn, least, 1 + least);
1407    if (least != greatest)
1408       undoManager.VisitStates(fn, greatest, 1 + greatest);
1409 
1410    int64_t total = projectFileIO.GetTotalUsage();
1411    int64_t used = projectFileIO.GetCurrentUsage(trackLists);
1412 
1413    auto before = wxFileName::GetSize(projectFileIO.GetFileName());
1414 
1415    CompactDialog dlg(
1416          XO("Compacting this project will free up disk space by removing unused bytes within the file.\n\n"
1417             "There is %s of free disk space and this project is currently using %s.\n"
1418             "\n"
1419             "If you proceed, the current Undo/Redo History and clipboard contents will be discarded "
1420             "and you will recover approximately %s of disk space.\n"
1421             "\n"
1422             "Do you want to continue?")
1423          .Format(Internat::FormatSize(projectFileIO.GetFreeDiskSpace()),
1424                   Internat::FormatSize(before.GetValue()),
1425                   Internat::FormatSize(total - used)));
1426    if (isBatch || dlg.ShowModal() == wxYES)
1427    {
1428       // We can remove redo states, if they are after the saved state.
1429       undoManager.RemoveStates(1 + greatest, undoManager.GetNumStates());
1430 
1431       // We can remove all states between the current and the last saved.
1432       if (least < greatest)
1433          undoManager.RemoveStates(least + 1, greatest);
1434 
1435       // We can remove all states before the current and the last saved.
1436       undoManager.RemoveStates(0, least);
1437 
1438       // And clear the clipboard, if needed
1439       if (&mProject == clipboard.Project().lock().get())
1440          clipboard.Clear();
1441 
1442       // Refresh the before space usage since it may have changed due to the
1443       // above actions.
1444       auto before = wxFileName::GetSize(projectFileIO.GetFileName());
1445 
1446       projectFileIO.Compact(trackLists, true);
1447 
1448       auto after = wxFileName::GetSize(projectFileIO.GetFileName());
1449 
1450       if (!isBatch)
1451       {
1452          AudacityMessageBox(
1453             XO("Compacting actually freed %s of disk space.")
1454             .Format(Internat::FormatSize((before - after).GetValue())),
1455             XO("Compact Project"));
1456       }
1457 
1458       undoManager.RenameState( undoManager.GetCurrentState(),
1459          XO("Compacted project file"),
1460          XO("Compact") );
1461    }
1462 }
1463