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