Audacity 3.2.0
ProjectFileManager.cpp
Go to the documentation of this file.
1/**********************************************************************
2
3Audacity: A Digital Audio Editor
4
5ProjectFileManager.cpp
6
7Paul 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 "AnalyzedWaveClip.h"
20#include "AudacityMessageBox.h"
21#include "AudacityMirProject.h"
22#include "BasicUI.h"
23#include "ClipMirAudioReader.h"
24#include "CodeConversions.h"
25#include "Export.h"
26#include "HelpText.h"
27#include "Import.h"
28#include "ImportPlugin.h"
30#include "Legacy.h"
33#include "Project.h"
34#include "ProjectFileIO.h"
35#include "ProjectHistory.h"
37#include "ProjectRate.h"
39#include "ProjectSettings.h"
40#include "ProjectStatus.h"
42#include "ProjectWindow.h"
43#include "ProjectWindows.h"
44#include "RealtimeEffectList.h"
45#include "SelectFile.h"
46#include "SelectUtilities.h"
47#include "SelectionState.h"
48#include "Tags.h"
49#include "TempDirectory.h"
50#include "TempoChange.h"
51#include "TimeDisplayMode.h"
52#include "TrackFocus.h"
53#include "TrackPanel.h"
54#include "UndoTracks.h"
55#include "UserException.h"
56#include "WaveClip.h"
57#include "WaveTrack.h"
58#include "WaveTrackUtilities.h"
59#include "XMLFileReader.h"
62#include "widgets/FileHistory.h"
64#include "widgets/Warning.h"
65#include "wxFileNameWrapper.h"
66#include "wxPanelWrapper.h"
67
69
70#include <optional>
71#include <wx/frame.h>
72#include <wx/log.h>
73
75 []( AudacityProject &parent ){
76 auto result = std::make_shared< ProjectFileManager >( parent );
77 return result;
78 }
79};
80
82{
83 return project.AttachedObjects::Get< ProjectFileManager >( sFileManagerKey );
84}
85
87{
88 return Get( const_cast< AudacityProject & >( project ) );
89}
90
92{
93 InvisibleTemporaryProject tempProject;
94 auto &project = tempProject.Project();
95 auto &projectFileManager = Get(project);
96 // Read the project, discarding autosave
97 projectFileManager.ReadProjectFile(filename, true);
98
99 if (projectFileManager.mLastSavedTracks) {
100 for (auto wt : projectFileManager.mLastSavedTracks->Any<WaveTrack>())
102 projectFileManager.mLastSavedTracks.reset();
103 }
104
105 // Side-effect on database is done, and destructor of tempProject
106 // closes the temporary project properly
107}
108
110: mProject{ project }
111{
112}
113
115
116namespace {
117
118const char *const defaultHelpUrl =
119 "FAQ:Errors_on_opening_or_recovering_an_Audacity_project";
120
121using Pair = std::pair< const char *, const char * >;
123 {
124 "not well-formed (invalid token)",
125 "Error:_not_well-formed_(invalid_token)_at_line_x"
126 },
127 {
128 "reference to invalid character number",
129 "Error_Opening_Project:_Reference_to_invalid_character_number_at_line_x"
130 },
131 {
132 "mismatched tag",
133 "#mismatched"
134 },
135// This error with FAQ entry is reported elsewhere, not here....
136//#[[#corrupt|Error Opening File or Project: File may be invalid or corrupted]]
137};
138
139wxString FindHelpUrl( const TranslatableString &libraryError )
140{
141 wxString helpUrl;
142 if ( !libraryError.empty() ) {
143 helpUrl = defaultHelpUrl;
144
145 auto msgid = libraryError.MSGID().GET();
146 auto found = std::find_if( begin(helpURLTable), end(helpURLTable),
147 [&]( const Pair &pair ) {
148 return msgid.Contains( pair.first ); }
149 );
150 if (found != end(helpURLTable)) {
151 auto url = found->second;
152 if (url[0] == '#')
153 helpUrl += url;
154 else
155 helpUrl = url;
156 }
157 }
158
159 return helpUrl;
160}
161
162}
163
165 const FilePath &fileName, bool discardAutosave )
167{
168 auto &project = mProject;
169 auto &projectFileIO = ProjectFileIO::Get( project );
170 auto &window = GetProjectFrame( project );
171
175 auto parseResult = projectFileIO.LoadProject(fileName, discardAutosave);
176 const bool bParseSuccess = parseResult.has_value();
177
178 bool err = false;
179 std::optional<TranslatableString> linkTypeChangeReason;
180
181 TranslatableString otherError;
182
183 if (bParseSuccess)
184 {
186 // By making a duplicate set of pointers to the existing blocks
187 // on disk, we add one to their reference count, guaranteeing
188 // that their reference counts will never reach zero and thus
189 // the version saved on disk will be preserved until the
190 // user selects Save().
191 // Do this before FixTracks might delete zero-length clips!
192 mLastSavedTracks = TrackList::Create( nullptr );
193 WaveTrack *leader{};
194 for (auto pTrack : tracks.Any<WaveTrack>()) {
195 // A rare place where TrackList::Channels remains necessary, to visit
196 // the right channels of stereo tracks not yet "zipped", otherwise
197 // later, CloseLock() will be missed for some sample blocks and
198 // corrupt the project
199 for (const auto pChannel : TrackList::Channels(pTrack)) {
200 auto left = leader;
201 auto newTrack =
203 leader = left
204 ? nullptr // now visiting the right channel
205 : (pChannel->GetLinkType() == Track::LinkType::None)
206 ? nullptr // now visiting a mono channel
207 : static_cast<WaveTrack*>(newTrack.get())
208 // now visiting a left channel
209 ;
210 mLastSavedTracks->Add(newTrack);
211 if (left)
212 // Zip clips allowing misalignment -- this may be a legacy
213 // project. This duplicate track will NOT be used for normal
214 // editing, but only later to visit all the sample blocks that
215 // existed at last save time.
216 left->ZipClips(false);
217 }
218 }
219
220 FixTracks(
221 tracks,
222 // Keep at most one of the error messages
223 [&](const auto& errorMessage) { otherError = errorMessage; err = true; },
224 [&](const auto& unlinkReason) { linkTypeChangeReason = unlinkReason; });
225
226 if (!err) {
227 if(linkTypeChangeReason && !discardAutosave)
228 {
230//i18n-hint: Text of the message dialog that may appear on attempt
231//to open a project created by Audacity version prior to 3.4.
232//%s will be replaced with an explanation of the actual reason of
233//project modification.
234"%s\n"
235"This feature is not supported in Audacity versions past 3.3.3.\n"
236"These stereo tracks have been split into mono tracks.\n"
237"As a result, some realtime effects may be missing.\n"
238"Please verify that everything works as intended before saving."
239 ).Format(linkTypeChangeReason->Translation()));
240 }
241
242 parseResult->Commit();
243 if (discardAutosave)
244 // REVIEW: Failure OK?
245 projectFileIO.AutoSaveDelete();
246 else if (projectFileIO.IsRecovered()) {
247 bool resaved = false;
248
249 if (!projectFileIO.IsTemporary() &&
250 !linkTypeChangeReason)
251 {
252 // Re-save non-temporary project to its own path. This
253 // might fail to update the document blob in the database.
254 resaved = projectFileIO.SaveProject(fileName, nullptr);
255 }
256
258 resaved
259 ? XO(
260"This project was not saved properly the last time Audacity ran.\n\n"
261"It has been recovered to the last snapshot.")
262 : XO(
263"This project was not saved properly the last time Audacity ran.\n\n"
264"It has been recovered to the last snapshot, but you must save it\n"
265"to preserve its contents."),
266 XO("Project Recovered"),
267 wxICON_WARNING,
268 &window);
269 }
270 }
271
273 }
274
275 return {
276 bParseSuccess,
277 err,
278 (bParseSuccess ? otherError : projectFileIO.GetLastError()),
279 FindHelpUrl(projectFileIO.GetLibraryError())
280 };
281}
282
284{
285 auto &projectFileIO = ProjectFileIO::Get(mProject);
286
287
289 mProject, [this](auto& path, bool rename)
290 { return DoSave(audacity::ToWXString(path), rename); });
291 action != OnSaveAction::Continue)
292 return action == OnSaveAction::Handled;
293
294 // Prompt for file name?
295 if (projectFileIO.IsTemporary())
296 {
297 return SaveAs(true);
298 }
299
300 return DoSave(projectFileIO.GetFileName(), false);
301}
302
303#if 0
304// I added this to "fix" bug #334. At that time, we were on wxWidgets 2.8.12 and
305// there was a window between the closing of the "Save" progress dialog and the
306// end of the actual save where the user was able to close the project window and
307// recursively enter the Save code (where they could inadvertently cause the issue
308// described in #334).
309//
310// When we converted to wx3, this "disabler" caused focus problems when returning
311// to the project after the save (bug #1172) because the focus and activate events
312// weren't being dispatched and the focus would get lost.
313//
314// After some testing, it looks like the window described above no longer exists,
315// so I've disabled the disabler. However, I'm leaving it here in case we run
316// into the problem in the future. (even though it can't be used as-is)
317class ProjectDisabler
318{
319public:
320 ProjectDisabler(wxWindow *w)
321 : mWindow(w)
322 {
323 mWindow->GetEventHandler()->SetEvtHandlerEnabled(false);
324 }
325 ~ProjectDisabler()
326 {
327 mWindow->GetEventHandler()->SetEvtHandlerEnabled(true);
328 }
329private:
330 wxWindow *mWindow;
331};
332#endif
333
334// Assumes ProjectFileIO::mFileName has been set to the desired path.
335bool ProjectFileManager::DoSave(const FilePath & fileName, const bool fromSaveAs)
336{
337 // See explanation above
338 // ProjectDisabler disabler(this);
339 auto &proj = mProject;
340 auto &window = GetProjectFrame( proj );
341 auto &projectFileIO = ProjectFileIO::Get( proj );
342 const auto &settings = ProjectSettings::Get( proj );
343
344 // Some confirmation dialogs
345 {
346 if (TempDirectory::FATFilesystemDenied(fileName, XO("Projects cannot be saved to FAT drives.")))
347 {
348 return false;
349 }
350
351 auto &tracks = TrackList::Get( proj );
352 if (tracks.empty())
353 {
354 if (UndoManager::Get( proj ).UnsavedChanges() &&
355 settings.EmptyCanBeDirty())
356 {
357 int result = AudacityMessageBox(
358 XO(
359 "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?"),
360 XO("Warning - Empty Project"),
361 wxYES_NO | wxICON_QUESTION,
362 &window);
363 if (result == wxNO)
364 {
365 return false;
366 }
367 }
368 }
369
370 wxULongLong fileSize = wxFileName::GetSize(projectFileIO.GetFileName());
371
372 wxDiskspaceSize_t freeSpace;
373 if (wxGetDiskSpace(FileNames::AbbreviatePath(fileName), NULL, &freeSpace))
374 {
375 if (freeSpace.GetValue() <= fileSize.GetValue())
376 {
378 XO("Insufficient Disk Space"),
379 XO("The project size exceeds the available free space on the target disk.\n\n"
380 "Please select a different disk with more free space."),
381 "Error:_Disk_full_or_not_writable"
382 );
383
384 return false;
385 }
386 }
387 }
388 // End of confirmations
389
390 // Always save a backup of the original project file
391 std::optional<ProjectFileIO::BackupProject> pBackupProject;
392 if (fromSaveAs && wxFileExists(fileName))
393 {
394 pBackupProject.emplace(projectFileIO, fileName);
395 if (!pBackupProject->IsOk())
396 return false;
397 }
398
399 if (FileNames::IsOnFATFileSystem(fileName))
400 {
401 if (wxFileName::GetSize(projectFileIO.GetFileName()) > UINT32_MAX)
402 {
404 XO("Error Saving Project"),
405 XO("The project exceeds the maximum size of 4GB when writing to a FAT32 formatted filesystem."),
406 "Error:_Unsuitable_drive"
407 );
408 return false;
409 }
410 }
411
412 bool success = projectFileIO.SaveProject(fileName, mLastSavedTracks.get());
413 if (!success)
414 {
415 // Show this error only if we didn't fail reconnection in SaveProject
416 // REVIEW: Could HasConnection() be true but SaveProject() still have failed?
417 if (!projectFileIO.HasConnection()) {
418 using namespace BasicUI;
420 XO("Error Saving Project"),
422 "Error:_Disk_full_or_not_writable",
423 ErrorDialogOptions{ ErrorDialogType::ModalErrorReport } );
424 }
425 return false;
426 }
427
428 proj.SetProjectName(wxFileName(fileName).GetName());
429 projectFileIO.SetProjectTitle();
430
432 ProjectStatus::Get(proj).Set(XO("Saved %s").Format(fileName));
433
435 {
436 mLastSavedTracks->Clear();
437 }
439
440 auto &tracks = TrackList::Get(proj);
441 for (auto t : tracks)
442 mLastSavedTracks->Add(t->Duplicate(Track::DuplicateOptions{}.Backup()));
443
444 // If we get here, saving the project was successful, so we can DELETE
445 // any backup project.
446 if (pBackupProject)
447 pBackupProject->Discard();
448
449 return true;
450}
451
452// This version of SaveAs is invoked only from scripting and does not
453// prompt for a file name
454bool ProjectFileManager::SaveAs(const FilePath &newFileName, bool addToHistory /*= true*/)
455{
456 auto &project = mProject;
457 auto &projectFileIO = ProjectFileIO::Get( project );
458
459 auto oldFileName = projectFileIO.GetFileName();
460
461 bool bOwnsNewName = !projectFileIO.IsTemporary() && (oldFileName == newFileName);
462 //check to see if the NEW project file already exists.
463 //We should only overwrite it if this project already has the same name, where the user
464 //simply chose to use the save as command although the save command would have the effect.
465 if( !bOwnsNewName && wxFileExists(newFileName)) {
467 nullptr,
468 XO("The project was not saved because the file name provided would overwrite another project.\nPlease try again and select an original name."),
469 XO("Error Saving Project"),
470 wxOK|wxICON_ERROR );
471 m.ShowModal();
472 return false;
473 }
474
475 auto success = DoSave(newFileName, !bOwnsNewName);
476 if (success && addToHistory) {
477 FileHistory::Global().Append( projectFileIO.GetFileName() );
478 }
479
480 return(success);
481}
482
483bool ProjectFileManager::SaveAs(bool allowOverwrite /* = false */)
484{
485 auto &project = mProject;
486 auto &projectFileIO = ProjectFileIO::Get( project );
487 auto &window = GetProjectFrame( project );
488 TitleRestorer Restorer( window, project ); // RAII
489 wxFileName filename;
490 FilePath defaultSavePath = FileNames::FindDefaultPath(FileNames::Operation::Save);
491
492 if (projectFileIO.IsTemporary()) {
493 filename.SetPath(defaultSavePath);
494 filename.SetName(project.GetProjectName());
495 }
496 else {
497 filename = projectFileIO.GetFileName();
498 }
499
500 // Bug 1304: Set a default file path if none was given. For Save/SaveAs/SaveCopy
501 if( !FileNames::IsPathAvailable( filename.GetPath( wxPATH_GET_VOLUME| wxPATH_GET_SEPARATOR) ) ){
502 filename.SetPath(defaultSavePath);
503 }
504
505 TranslatableString title = XO("%sSave Project \"%s\" As...")
506 .Format( Restorer.sProjNumber, Restorer.sProjName );
507 TranslatableString message = XO("\
508'Save Project' is for an Audacity project, not an audio file.\n\
509For an audio file that will open in other apps, use 'Export'.\n");
510
511 if (ShowWarningDialog(&window, wxT("FirstProjectSave"), message, true) != wxID_OK) {
512 return false;
513 }
514
515 bool bPrompt = (project.mBatchMode == 0) || (projectFileIO.GetFileName().empty());
516 FilePath fName;
517 bool bOwnsNewName;
518
519 do {
520 if (bPrompt) {
521 // JKC: I removed 'wxFD_OVERWRITE_PROMPT' because we are checking
522 // for overwrite ourselves later, and we disallow it.
523 fName = SelectFile(FileNames::Operation::Save,
524 title,
525 filename.GetPath(),
526 filename.GetFullName(),
527 wxT("aup3"),
529 wxFD_SAVE | wxRESIZE_BORDER,
530 &window);
531
532 if (fName.empty())
533 return false;
534
535 filename = fName;
536 };
537
538 filename.SetExt(wxT("aup3"));
539
540 if ((!bPrompt || !allowOverwrite) && filename.FileExists()) {
541 // Saving a copy of the project should never overwrite an existing project.
543 nullptr,
544 XO("The project was not saved because the file name provided would overwrite another project.\nPlease try again and select an original name."),
545 XO("Error Saving Project"),
546 wxOK|wxICON_ERROR );
547 m.ShowModal();
548 return false;
549 }
550
551 fName = filename.GetFullPath();
552
553 bOwnsNewName = !projectFileIO.IsTemporary() && ( projectFileIO.GetFileName() == fName );
554 // Check to see if the project file already exists, and if it does
555 // check that the project file 'belongs' to this project.
556 // otherwise, prompt the user before overwriting.
557 if (!bOwnsNewName && filename.FileExists()) {
558 // Ensure that project of same name is not open in another window.
559 // fName is the destination file.
560 // mFileName is this project.
561 // It is possible for mFileName == fName even when this project is not
562 // saved to disk, and we then need to check the destination file is not
563 // open in another window.
564 int mayOverwrite = ( projectFileIO.GetFileName() == fName ) ? 2 : 1;
565 for ( auto p : AllProjects{} ) {
566 const wxFileName openProjectName{ ProjectFileIO::Get(*p).GetFileName() };
567 if (openProjectName.SameAs(fName)) {
568 mayOverwrite -= 1;
569 if (mayOverwrite == 0)
570 break;
571 }
572 }
573
574 if (mayOverwrite > 0) {
575 /* i18n-hint: In each case, %s is the name
576 of the file being overwritten.*/
577 auto Message = XO("\
578 Do you want to overwrite the project:\n\"%s\"?\n\n\
579 If you select \"Yes\" the project\n\"%s\"\n\
580 will be irreversibly overwritten.").Format( fName, fName );
581
582 // For safety, there should NOT be an option to hide this warning.
583 int result = AudacityMessageBox(
584 Message,
585 /* i18n-hint: Heading: A warning that a project is about to be overwritten.*/
586 XO("Overwrite Project Warning"),
587 wxYES_NO | wxNO_DEFAULT | wxICON_WARNING,
588 &window);
589 if (result == wxNO) {
590 continue;
591 }
592 if (result == wxCANCEL) {
593 return false;
594 }
595 }
596 else {
597 // Overwrite disallowed. The destination project is open in another window.
599 nullptr,
600 XO("The project was not saved because the selected project is open in another window.\nPlease try again and select an original name."),
601 XO("Error Saving Project"),
602 wxOK|wxICON_ERROR );
603 m.ShowModal();
604 continue;
605 }
606 }
607
608 break;
609 } while (bPrompt);
610
611
612 // Pretend that we are closing the project
613 if (!bOwnsNewName)
614 {
615 if (
618 return false;
619 }
620
621 auto success = DoSave(fName, !bOwnsNewName);
622 if (success) {
623 FileHistory::Global().Append( projectFileIO.GetFileName() );
624 }
625
626 return(success);
627}
628
629bool ProjectFileManager::SaveCopy(const FilePath &fileName /* = wxT("") */)
630{
631 auto &project = mProject;
632 auto &projectFileIO = ProjectFileIO::Get(project);
633 auto &window = GetProjectFrame(project);
634 TitleRestorer Restorer(window, project); // RAII
635 wxFileName filename = fileName;
636 FilePath defaultSavePath = FileNames::FindDefaultPath(FileNames::Operation::Save);
637
638 if (fileName.empty())
639 {
640 if (projectFileIO.IsTemporary())
641 {
642 filename.SetPath(defaultSavePath);
643 }
644 else
645 {
646 filename = projectFileIO.GetFileName();
647 }
648 }
649
650 // Bug 1304: Set a default file path if none was given. For Save/SaveAs/SaveCopy
651 if (!FileNames::IsPathAvailable(filename.GetPath(wxPATH_GET_VOLUME | wxPATH_GET_SEPARATOR)))
652 {
653 filename.SetPath(defaultSavePath);
654 }
655
657 XO("%sSave Copy of Project \"%s\" As...")
658 .Format(Restorer.sProjNumber, Restorer.sProjName);
659
660 bool bPrompt = (project.mBatchMode == 0) || (projectFileIO.GetFileName().empty());
661 FilePath fName;
662
663 do
664 {
665 if (bPrompt)
666 {
667 // JKC: I removed 'wxFD_OVERWRITE_PROMPT' because we are checking
668 // for overwrite ourselves later, and we disallow it.
669 // Previously we disallowed overwrite because we would have had
670 // to DELETE the many smaller files too, or prompt to move them.
671 // Maybe we could allow it now that we have aup3 format?
672 fName = SelectFile(FileNames::Operation::Export,
673 title,
674 filename.GetPath(),
675 filename.GetFullName(),
676 wxT("aup3"),
678 wxFD_SAVE | wxRESIZE_BORDER,
679 &window);
680
681 if (fName.empty())
682 {
683 return false;
684 }
685
686 filename = fName;
687 };
688
689 filename.SetExt(wxT("aup3"));
690
691 if (TempDirectory::FATFilesystemDenied(filename.GetFullPath(), XO("Projects cannot be saved to FAT drives.")))
692 {
693 if (project.mBatchMode)
694 {
695 return false;
696 }
697
698 continue;
699 }
700
701 if (filename.FileExists())
702 {
703 // Saving a copy of the project should never overwrite an existing project.
704 AudacityMessageDialog m(nullptr,
705 XO("Saving a copy must not overwrite an existing saved project.\nPlease try again and select an original name."),
706 XO("Error Saving Copy of Project"),
707 wxOK | wxICON_ERROR);
708 m.ShowModal();
709
710 if (project.mBatchMode)
711 {
712 return false;
713 }
714
715 continue;
716 }
717
718 wxULongLong fileSize = wxFileName::GetSize(projectFileIO.GetFileName());
719
720 wxDiskspaceSize_t freeSpace;
721 if (wxGetDiskSpace(FileNames::AbbreviatePath(filename.GetFullPath()), NULL, &freeSpace))
722 {
723 if (freeSpace.GetValue() <= fileSize.GetValue())
724 {
726 XO("Insufficient Disk Space"),
727 XO("The project size exceeds the available free space on the target disk.\n\n"
728 "Please select a different disk with more free space."),
729 "Error:_Unsuitable_drive"
730 );
731
732 continue;
733 }
734 }
735
736 if (FileNames::IsOnFATFileSystem(filename.GetFullPath()))
737 {
738 if (fileSize > UINT32_MAX)
739 {
741 XO("Error Saving Project"),
742 XO("The project exceeds the maximum size of 4GB when writing to a FAT32 formatted filesystem."),
743 "Error:_Unsuitable_drive"
744 );
745
746 if (project.mBatchMode)
747 {
748 return false;
749 }
750
751 continue;
752 }
753 }
754
755 fName = filename.GetFullPath();
756 break;
757 } while (bPrompt);
758
759 if (!projectFileIO.SaveCopy(fName))
760 {
761 auto msg = FileException::WriteFailureMessage(fName);
763 nullptr, msg, XO("Error Saving Project"), wxOK | wxICON_ERROR);
764
765 m.ShowModal();
766
767 return false;
768 }
769
770 return true;
771}
772
774{
775 auto &project = mProject;
776 auto &projectFileIO = ProjectFileIO::Get( project );
777
778 // MY: Will save the project to a NEW location a-la Save As
779 // and then tidy up after itself.
780
781 wxString sNewFileName = fnFile.GetFullPath();
782
783 // MY: To allow SaveAs from Timer Recording we need to check what
784 // the value of mFileName is before we change it.
785 FilePath sOldFilename;
786 if (!projectFileIO.IsModified()) {
787 sOldFilename = projectFileIO.GetFileName();
788 }
789
790 // MY: If the project file already exists then bail out
791 // and send populate the message string (pointer) so
792 // we can tell the user what went wrong.
793 if (wxFileExists(sNewFileName)) {
794 return false;
795 }
796
797 auto success = DoSave(sNewFileName, true);
798
799 if (success) {
800 FileHistory::Global().Append( projectFileIO.GetFileName() );
801 }
802
803 return success;
804}
805
807{
808 auto &project = mProject;
809 auto &projectFileIO = ProjectFileIO::Get(project);
810
811 // Lock all blocks in all tracks of the last saved version, so that
812 // the sample blocks aren't deleted from the database when we destroy the
813 // sample block objects in memory.
815 {
816 for (auto wt : mLastSavedTracks->Any<WaveTrack>())
818
819 // Attempt to compact the project
820 projectFileIO.Compact( { mLastSavedTracks.get() } );
821
822 if ( !projectFileIO.WasCompacted() &&
824 // If compaction failed, we must do some work in case of close
825 // without save. Don't leave the document blob from the last
826 // push of undo history, when that undo state may get purged
827 // with deletion of some new sample blocks.
828 // REVIEW: UpdateSaved() might fail too. Do we need to test
829 // for that and report it?
830 projectFileIO.UpdateSaved( mLastSavedTracks.get() );
831 }
832 }
833}
834
836{
837 auto &project = mProject;
838 auto &projectFileIO = ProjectFileIO::Get(project);
839
840 return projectFileIO.OpenProject();
841}
842
844{
845 auto &project = mProject;
846 auto &projectFileIO = ProjectFileIO::Get(project);
847
848 bool bOK = OpenProject();
849 if( !bOK )
850 {
851 auto tmpdir = wxFileName(TempDirectory::UnsavedProjectFileName()).GetPath();
852
853 UnwritableLocationErrorDialog dlg(nullptr, tmpdir);
854 dlg.ShowModal();
855 }
856 return bOK;
857}
858
860{
861 auto &project = mProject;
862 auto &projectFileIO = ProjectFileIO::Get(project);
863
864 projectFileIO.CloseProject();
865
866 // Blocks were locked in CompactProjectOnClose, so DELETE the data structure so that
867 // there's no memory leak.
869 {
870 mLastSavedTracks->Clear();
871 mLastSavedTracks.reset();
872 }
873}
874
875// static method, can be called outside of a project
877 const FileNames::FileType &extraType )
878{
879 // Construct the filter
880 const auto fileTypes = Importer::Get().GetFileTypes( extraType );
881
882 // Retrieve saved path
883 auto path = FileNames::FindDefaultPath(op);
884
885 // Construct and display the file dialog
886 wxArrayString selected;
887
888 FileDialogWrapper dlog(nullptr,
889 XO("Select one or more files"),
890 path,
891 wxT(""),
892 fileTypes,
893 wxFD_OPEN | wxFD_MULTIPLE | wxRESIZE_BORDER);
894
896
897 int dialogResult = dlog.ShowModal();
898
899 // Convert the filter index to type and save
900 auto index = dlog.GetFilterIndex();
901 const auto &saveType = fileTypes[ index ];
902
904 Importer::SetLastOpenType( saveType );
905
906 if (dialogResult == wxID_OK) {
907 // Return the selected files
908 dlog.GetPaths(selected);
909
910 // Remember the directory
911 FileNames::UpdateDefaultPath(op, ::wxPathOnly(dlog.GetPath()));
912 }
913
914 return selected;
915}
916
917// static method, can be called outside of a project
919{
920 const wxFileName newProjPathName(projPathName);
921 auto start = AllProjects{}.begin(), finish = AllProjects{}.end(),
922 iter = std::find_if( start, finish,
923 [&]( const AllProjects::value_type &ptr ){
924 return newProjPathName.SameAs(wxFileNameWrapper{ ProjectFileIO::Get(*ptr).GetFileName() });
925 } );
926 if (iter != finish) {
927 auto errMsg =
928 XO("%s is already open in another window.")
929 .Format( newProjPathName.GetName() );
930 wxLogError(errMsg.Translation()); //Debug?
932 errMsg,
933 XO("Error Opening Project"),
934 wxOK | wxCENTRE);
935 return true;
936 }
937 return false;
938}
939
941 const FilePath &fileNameArg, bool addtohistory)
942{
943 // On Win32, we may be given a short (DOS-compatible) file name on rare
944 // occasions (e.g. stuff like "C:\PROGRA~1\AUDACI~1\PROJEC~1.AUP"). We
945 // convert these to long file name first.
946 auto fileName = PlatformCompatibility::GetLongFileName(fileNameArg);
947
948 // Make sure it isn't already open.
949 // Vaughan, 2011-03-25: This was done previously in AudacityProject::OpenFiles()
950 // and AudacityApp::MRUOpen(), but if you open an aup file by double-clicking it
951 // from, e.g., Win Explorer, it would bypass those, get to here with no check,
952 // then open a NEW project from the same data with no warning.
953 // This was reported in http://bugzilla.audacityteam.org/show_bug.cgi?id=137#c17,
954 // but is not really part of that bug. Anyway, prevent it!
955 if (IsAlreadyOpen(fileName))
956 return nullptr;
957
958 // Data loss may occur if users mistakenly try to open ".aup3.bak" files
959 // left over from an unsuccessful save or by previous versions of Audacity.
960 // So we always refuse to open such files.
961 if (fileName.Lower().EndsWith(wxT(".aup3.bak")))
962 {
964 XO(
965"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."),
966 XO("Warning - Backup File Detected"),
967 wxOK | wxCENTRE,
968 nullptr);
969 return nullptr;
970 }
971
972 if (!::wxFileExists(fileName)) {
974 XO("Could not open file: %s").Format( fileName ),
975 XO("Error Opening File"),
976 wxOK | wxCENTRE,
977 nullptr);
978 return nullptr;
979 }
980
981 // Following block covers cases other than a project file:
982 {
983 wxFFile ff(fileName, wxT("rb"));
984
985 auto cleanup = finally([&]
986 {
987 if (ff.IsOpened())
988 {
989 ff.Close();
990 }
991 });
992
993 if (!ff.IsOpened()) {
995 XO("Could not open file: %s").Format( fileName ),
996 XO("Error opening file"),
997 wxOK | wxCENTRE,
998 nullptr);
999 return nullptr;
1000 }
1001
1002 char buf[7];
1003 auto numRead = ff.Read(buf, 6);
1004 if (numRead != 6) {
1006 XO("File may be invalid or corrupted: \n%s").Format( fileName ),
1007 XO("Error Opening File or Project"),
1008 wxOK | wxCENTRE,
1009 nullptr);
1010 return nullptr;
1011 }
1012
1013 if (wxStrncmp(buf, "SQLite", 6) != 0)
1014 {
1015 // Not a database
1016#ifdef EXPERIMENTAL_DRAG_DROP_PLUG_INS
1017 // Is it a plug-in?
1018 if (PluginManager::Get().DropFile(fileName)) {
1020 // Plug-in installation happened, not really opening of a file,
1021 // so return null
1022 return nullptr;
1023 }
1024#endif
1025 auto &project = chooser(false);
1026 // Undo history is incremented inside this:
1027 if (Get(project).Import(fileName))
1028 {
1029 // Undo history is incremented inside this:
1030 // Bug 2743: Don't zoom with lof.
1031 if (!fileName.AfterLast('.').IsSameAs(wxT("lof"), false))
1033 return &project;
1034 }
1035 return nullptr;
1036 }
1037 }
1038
1039 // Disallow opening of .aup3 project files from FAT drives, but only such
1040 // files, not importable types. (Bug 2800)
1042 XO("Project resides on FAT formatted drive.\n"
1043 "Copy it to another drive to open it.")))
1044 {
1045 return nullptr;
1046 }
1047
1048 auto &project = chooser(true);
1049 return Get(project).OpenProjectFile(fileName, addtohistory);
1050}
1051
1053 const std::function<void(const TranslatableString&)>& onError,
1054 const std::function<void(const TranslatableString&)>& onUnlink)
1055{
1056 // This is successively assigned the left member of each pair that
1057 // becomes unlinked
1058 Track::Holder unlinkedTrack;
1059 // Beware iterator invalidation, because stereo channels get zipped,
1060 // replacing WaveTracks
1061 for (auto iter = tracks.begin(); iter != tracks.end();) {
1062 auto t = (*iter++)->SharedPointer();
1063 const auto linkType = t->GetLinkType();
1064 // Note, the next function may have an important upgrading side effect,
1065 // and return no error; or it may find a real error and repair it, but
1066 // that repaired track won't be used because opening will fail.
1067 if (!t->LinkConsistencyFix()) {
1068 onError(XO("A channel of a stereo track was missing."));
1069 unlinkedTrack = nullptr;
1070 }
1071 if (!unlinkedTrack) {
1072 if (linkType != ChannelGroup::LinkType::None &&
1073 t->NChannels() == 1) {
1074 // The track became unlinked.
1075 // It should NOT have been replaced with a "zip"
1076 assert(t->GetOwner().get() == &tracks);
1077 // Wait until LinkConsistencyFix is called on the second track
1078 unlinkedTrack = t;
1079 // Fix the iterator, which skipped the right channel before the
1080 // unlinking
1081 iter = tracks.Find(t.get());
1082 ++iter;
1083 }
1084 }
1085 else {
1086 //Not an elegant way to deal with stereo wave track linking
1087 //compatibility between versions
1088 if (const auto left = dynamic_cast<WaveTrack*>(unlinkedTrack.get())) {
1089 if (const auto right = dynamic_cast<WaveTrack*>(t.get())) {
1090 // As with the left, it should not have vanished from the list
1091 assert(right->GetOwner().get() == &tracks);
1092 left->SetPan(-1.0f);
1093 right->SetPan(1.0f);
1096
1097 if(left->GetRate() != right->GetRate())
1098 //i18n-hint: explains why opened project was auto-modified
1099 onUnlink(XO("This project contained stereo tracks with different sample rates per channel."));
1100 if(left->GetSampleFormat() != right->GetSampleFormat())
1101 //i18n-hint: explains why opened project was auto-modified
1102 onUnlink(XO("This project contained stereo tracks with different sample formats in channels."));
1103 //i18n-hint: explains why opened project was auto-modified
1104 onUnlink(XO("This project contained stereo tracks with non-aligned content."));
1105 }
1106 }
1107 unlinkedTrack = nullptr;
1108 }
1109
1110 if (const auto message = t->GetErrorOpening()) {
1111 wxLogWarning(
1112 wxT("Track %s had error reading clip values from project file."),
1113 t->GetName());
1114 onError(*message);
1115 }
1116 }
1117}
1118
1120 const FilePath &fileName, bool addtohistory)
1121{
1122 // Allow extensions to update the project before opening it.
1125 return nullptr;
1126
1127 auto &project = mProject;
1128 auto &history = ProjectHistory::Get( project );
1129 auto &tracks = TrackList::Get( project );
1130 auto &trackPanel = TrackPanel::Get( project );
1131 auto &projectFileIO = ProjectFileIO::Get( project );
1132 auto &viewport = Viewport::Get( project );
1133
1134 auto results = ReadProjectFile( fileName );
1135 const bool bParseSuccess = results.parseSuccess;
1136 const auto &errorStr = results.errorString;
1137 const bool err = results.trackError;
1138
1139 if (bParseSuccess && !err) {
1141
1143 TrackFocus::Get(project).Set(*tracks.begin());
1144 viewport.HandleResize();
1145 trackPanel.Refresh(false);
1146
1147 // ? Old rationale in this comment no longer applies in 3.0.0, with no
1148 // more on-demand loading:
1149 trackPanel.Update(); // force any repaint to happen now,
1150 // else any asynch calls into the blockfile code will not have
1151 // finished logging errors (if any) before the call to ProjectFSCK()
1152
1153 if (addtohistory)
1154 FileHistory::Global().Append(fileName);
1155 }
1156
1157 if (bParseSuccess && !err) {
1158 if (projectFileIO.IsRecovered())
1159 {
1160 // PushState calls AutoSave(), so no longer need to do so here.
1161 history.PushState(XO("Project was recovered"), XO("Recover"));
1162 }
1163 return &project;
1164 }
1165 else {
1166 // Vaughan, 2011-10-30:
1167 // See first topic at http://bugzilla.audacityteam.org/show_bug.cgi?id=451#c16.
1168 // Calling mTracks->Clear() with deleteTracks true results in data loss.
1169
1170 // PRL 2014-12-19:
1171 // I made many changes for wave track memory management, but only now
1172 // read the above comment. I may have invalidated the fix above (which
1173 // may have spared the files at the expense of leaked memory). But
1174 // here is a better way to accomplish the intent, doing like what happens
1175 // when the project closes:
1176 for (auto pTrack : tracks.Any<WaveTrack>())
1178
1179 tracks.Clear(); //tracks.Clear(true);
1180
1181 wxLogError(wxT("Could not parse file \"%s\". \nError: %s"), fileName, errorStr.Debug());
1182
1183 projectFileIO.ShowError( *ProjectFramePlacement(&project),
1184 XO("Error Opening Project"),
1185 errorStr,
1186 results.helpUrl);
1187
1188 return nullptr;
1189 }
1190}
1191
1192void
1194 TrackHolders &&newTracks)
1195{
1196 auto &project = mProject;
1197 auto &history = ProjectHistory::Get( project );
1198 auto &projectFileIO = ProjectFileIO::Get( project );
1199 auto &tracks = TrackList::Get( project );
1200
1201 std::vector<Track*> results;
1202
1204
1205 wxFileName fn(fileName);
1206
1207 bool initiallyEmpty = tracks.empty();
1208 double newRate = 0;
1209 wxString trackNameBase = fn.GetName();
1210 int i = -1;
1211
1212 // Fix the bug 2109.
1213 // In case the project had soloed tracks before importing,
1214 // all newly imported tracks are muted.
1215 const bool projectHasSolo =
1216 !(tracks.Any<PlayableTrack>() + &PlayableTrack::GetSolo).empty();
1217 if (projectHasSolo) {
1218 for (auto &group : newTracks)
1219 if (auto pTrack = dynamic_cast<PlayableTrack*>(group.get()))
1220 pTrack->SetMute(true);
1221 }
1222
1223 for (auto &group : newTracks) {
1224 if (auto pTrack = dynamic_cast<WaveTrack*>(group.get()))
1225 results.push_back(pTrack);
1226 tracks.Add(group);
1227 }
1228 newTracks.clear();
1229
1230 // Now name them
1231
1232 // Add numbers to track names only if there is more than one (mono or stereo)
1233 // track (not necessarily, more than one channel)
1234 const bool useSuffix = results.size() > 1;
1235
1236 for (const auto &newTrack : results) {
1237 ++i;
1238 newTrack->SetSelected(true);
1239 if (useSuffix)
1240 //i18n-hint Name default name assigned to a clip on track import
1241 newTrack->SetName(XC("%s %d", "clip name template")
1242 .Format(trackNameBase, i + 1).Translation());
1243 else
1244 newTrack->SetName(trackNameBase);
1245
1246 newTrack->TypeSwitch([&](WaveTrack &wt) {
1247 if (newRate == 0)
1248 newRate = wt.GetRate();
1249 const auto trackName = wt.GetName();
1250 for (const auto &interval : wt.Intervals())
1251 interval->SetName(trackName);
1252 });
1253 }
1254
1255 history.PushState(XO("Imported '%s'").Format( fileName ),
1256 XO("Import"));
1257
1258#if defined(__WXGTK__)
1259 // See bug #1224
1260 // The track panel hasn't been fully created, so ZoomFitHorizontally() will not give
1261 // expected results due to a window width of zero. Should be safe to yield here to
1262 // allow the creation to complete. If this becomes a problem, it "might" be possible
1263 // to queue a dummy event to trigger ZoomFitHorizontally().
1264 wxEventLoopBase::GetActive()->YieldFor(wxEVT_CATEGORY_UI | wxEVT_CATEGORY_USER_INPUT);
1265#endif
1266
1267 // If the project was clean and temporary (not permanently saved), then set
1268 // the filename to the just imported path.
1269 if (initiallyEmpty && projectFileIO.IsTemporary()) {
1270 project.SetProjectName(fn.GetName());
1271 project.SetInitialImportPath(fn.GetPath());
1272 projectFileIO.SetProjectTitle();
1273 }
1274
1275 // Moved this call to higher levels to prevent flicker redrawing everything on each file.
1276 // HandleResize();
1277}
1278
1279namespace {
1280bool ImportProject(AudacityProject &dest, const FilePath &fileName)
1281{
1283 auto &project = temp.Project();
1284
1285 auto &projectFileIO = ProjectFileIO::Get(project);
1286 if (!projectFileIO.LoadProject(fileName, false))
1287 return false;
1288 auto &srcTracks = TrackList::Get(project);
1289 auto &destTracks = TrackList::Get(dest);
1290 for (const Track *pTrack : srcTracks)
1291 pTrack->PasteInto(dest, destTracks);
1293
1294 return true;
1295}
1296
1298 : public ImportProgressListener
1299{
1300 wxWeakRef<AudacityProject> mProject;
1301public:
1302
1304 : mProject(&project)
1305 {
1306
1307 }
1308
1309 bool OnImportFileOpened(ImportFileHandle& importFileHandle) override
1310 {
1311 mImportFileHandle = &importFileHandle;
1312 // File has more than one stream - display stream selector
1313 if (importFileHandle.GetStreamCount() > 1)
1314 {
1315 ImportStreamDialog ImportDlg(&importFileHandle, NULL, -1, XO("Select stream(s) to import"));
1316
1317 if (ImportDlg.ShowModal() == wxID_CANCEL)
1318 return false;
1319 }
1320 // One stream - import it by default
1321 else
1322 importFileHandle.SetStreamUsage(0,TRUE);
1323 return true;
1324 }
1325
1326 void OnImportProgress(double progress) override
1327 {
1328 constexpr double ProgressSteps { 1000.0 };
1329 if(!mProgressDialog)
1330 {
1331 wxFileName ff( mImportFileHandle->GetFilename() );
1332 auto title = XO("Importing %s").Format( mImportFileHandle->GetFileDescription() );
1333 mProgressDialog = BasicUI::MakeProgress(title, Verbatim(ff.GetFullName()));
1334 }
1335 auto result = mProgressDialog->Poll(progress * ProgressSteps, ProgressSteps);
1337 mImportFileHandle->Cancel();
1338 else if(result == BasicUI::ProgressResult::Stopped)
1339 mImportFileHandle->Stop();
1340 }
1341
1342 void OnImportResult(ImportResult result) override
1343 {
1344 mProgressDialog.reset();
1345 if(result == ImportResult::Error)
1346 {
1347 auto message = mImportFileHandle->GetErrorMessage();
1348 if(!message.empty())
1349 {
1350 AudacityMessageBox(message, XO("Import"), wxOK | wxCENTRE | wxICON_ERROR,
1351 mProject ? &GetProjectFrame(*mProject) : nullptr);
1352 }
1353 }
1354 }
1355
1356private:
1357
1358 ImportFileHandle* mImportFileHandle {nullptr};
1359 std::unique_ptr<BasicUI::ProgressDialog> mProgressDialog;
1360};
1361} // namespace
1362
1363bool ProjectFileManager::Import(const FilePath& fileName, bool addToHistory)
1364{
1365 return Import(std::vector<FilePath> { fileName }, addToHistory);
1366}
1367
1368namespace
1369{
1370std::vector<std::shared_ptr<MIR::AnalyzedAudioClip>> RunTempoDetection(
1371 const std::vector<std::shared_ptr<ClipMirAudioReader>>& readers,
1372 const MIR::ProjectInterface& project, bool projectWasEmpty)
1373{
1374 const auto isBeatsAndMeasures = project.ViewIsBeatsAndMeasures();
1375 const auto projectTempo = project.GetTempo();
1376
1377 using namespace BasicUI;
1378 auto progress = MakeProgress(
1379 XO("Music Information Retrieval"), XO("Analyzing imported audio"),
1381 auto count = 0;
1382 const auto reportProgress = [&](double progressFraction) {
1383 const auto result = progress->Poll(
1384 (count + progressFraction) / readers.size() * 1000, 1000);
1385 if (result != ProgressResult::Success)
1386 throw UserException {};
1387 };
1388
1389 std::vector<std::shared_ptr<MIR::AnalyzedAudioClip>> analyzedClips;
1390 analyzedClips.reserve(readers.size());
1391 std::transform(
1392 readers.begin(), readers.end(), std::back_inserter(analyzedClips),
1393 [&](const std::shared_ptr<ClipMirAudioReader>& reader) {
1394 const MIR::ProjectSyncInfoInput input {
1395 *reader, reader->filename, reader->tags, reportProgress,
1396 projectTempo, projectWasEmpty, isBeatsAndMeasures,
1397 };
1398 auto syncInfo = MIR::GetProjectSyncInfo(input);
1399 ++count;
1400 return std::make_shared<AnalyzedWaveClip>(reader, syncInfo);
1401 });
1402 return analyzedClips;
1403}
1404} // namespace
1405
1407 const std::vector<FilePath>& fileNames, bool addToHistory)
1408{
1409 const auto projectWasEmpty =
1411 std::vector<std::shared_ptr<ClipMirAudioReader>> resultingReaders;
1412 const auto success = std::all_of(
1413 fileNames.begin(), fileNames.end(), [&](const FilePath& fileName) {
1414 std::shared_ptr<ClipMirAudioReader> resultingReader;
1415 const auto success = Import(fileName, addToHistory, resultingReader);
1416 if (success && resultingReader)
1417 resultingReaders.push_back(std::move(resultingReader));
1418 return success;
1419 });
1420 if (success && !resultingReaders.empty())
1421 {
1422 const auto pProj = mProject.shared_from_this();
1423 BasicUI::CallAfter([=] {
1424 AudacityMirProject mirInterface { *pProj };
1425 const auto analyzedClips =
1426 RunTempoDetection(resultingReaders, mirInterface, projectWasEmpty);
1427 MIR::SynchronizeProject(analyzedClips, mirInterface, projectWasEmpty);
1428 });
1429 }
1430 return success;
1431}
1432
1433// If pNewTrackList is passed in non-NULL, it gets filled with the pointers to NEW tracks.
1435 const FilePath& fileName, bool addToHistory,
1436 std::shared_ptr<ClipMirAudioReader>& resultingReader)
1437{
1438 auto &project = mProject;
1439 auto &projectFileIO = ProjectFileIO::Get(project);
1440 auto oldTags = Tags::Get( project ).shared_from_this();
1441 bool initiallyEmpty = TrackList::Get(project).empty();
1442 TrackHolders newTracks;
1443 TranslatableString errorMessage;
1444
1445#ifdef EXPERIMENTAL_IMPORT_AUP3
1446 // Handle AUP3 ("project") files directly
1447 if (fileName.AfterLast('.').IsSameAs(wxT("aup3"), false)) {
1448 if (ImportProject(project, fileName)) {
1449 auto &history = ProjectHistory::Get(project);
1450
1451 // If the project was clean and temporary (not permanently saved), then set
1452 // the filename to the just imported path.
1453 if (initiallyEmpty && projectFileIO.IsTemporary()) {
1454 wxFileName fn(fileName);
1455 project.SetProjectName(fn.GetName());
1456 project.SetInitialImportPath(fn.GetPath());
1457 projectFileIO.SetProjectTitle();
1458 }
1459
1460 history.PushState(XO("Imported '%s'").Format(fileName), XO("Import"));
1461
1462 if (addToHistory) {
1463 FileHistory::Global().Append(fileName);
1464 }
1465 }
1466 else {
1467 errorMessage = projectFileIO.GetLastError();
1468 if (errorMessage.empty()) {
1469 errorMessage = XO("Failed to import project");
1470 }
1471
1472 // Additional help via a Help button links to the manual.
1474 XO("Error Importing"),
1475 errorMessage, wxT("Importing_Audio"));
1476 }
1477
1478 return false;
1479 }
1480#endif
1481
1482 {
1483 // Backup Tags, before the import. Be prepared to roll back changes.
1484 bool committed = false;
1485 auto cleanup = finally([&]{
1486 if ( !committed )
1487 Tags::Set( project, oldTags );
1488 });
1489 auto newTags = oldTags->Duplicate();
1490 Tags::Set( project, newTags );
1491
1492#ifndef EXPERIMENTAL_IMPORT_AUP3
1493 // Handle AUP3 ("project") files specially
1494 if (fileName.AfterLast('.').IsSameAs(wxT("aup3"), false)) {
1496 XO("Error Importing"),
1497 XO( "Cannot import AUP3 format. Use File > Open instead"),
1498 wxT("File_Menu"));
1499 return false;
1500 }
1501#endif
1502
1503 ImportProgress importProgress(project);
1504 std::optional<LibFileFormats::AcidizerTags> acidTags;
1505 bool success = Importer::Get().Import(
1506 project, fileName, &importProgress, &WaveTrackFactory::Get(project),
1507 newTracks, newTags.get(), acidTags, errorMessage);
1508 if (!errorMessage.empty()) {
1509 // Error message derived from Importer::Import
1510 // Additional help via a Help button links to the manual.
1512 XO("Error Importing"), errorMessage, wxT("Importing_Audio"));
1513 }
1514 if (!success)
1515 return false;
1516
1517 const auto projectTempo = ProjectTimeSignature::Get(project).GetTempo();
1518 for (auto track : newTracks)
1519 DoProjectTempoChange(*track, projectTempo);
1520
1521 if (newTracks.size() == 1)
1522 {
1523 if (const auto waveTrack = dynamic_cast<WaveTrack*>(newTracks[0].get()))
1524 resultingReader.reset(new ClipMirAudioReader {
1525 std::move(acidTags), fileName.ToStdString(),
1526 *waveTrack });
1527 }
1528
1529 if (addToHistory) {
1530 FileHistory::Global().Append(fileName);
1531 }
1532
1533 // no more errors, commit
1534 committed = true;
1535 }
1536
1537 // for LOF ("list of files") files, do not import the file as if it
1538 // were an audio file itself
1539 if (fileName.AfterLast('.').IsSameAs(wxT("lof"), false)) {
1540 // PRL: don't redundantly do the steps below, because we already
1541 // did it in case of LOF, because of some weird recursion back to this
1542 // same function. I think this should be untangled.
1543
1544 // So Undo history push is not bypassed, despite appearances.
1545 return false;
1546 }
1547
1548 // Handle AUP ("legacy project") files directly
1549 if (fileName.AfterLast('.').IsSameAs(wxT("aup"), false)) {
1550 // If the project was clean and temporary (not permanently saved), then set
1551 // the filename to the just imported path.
1552 if (initiallyEmpty && projectFileIO.IsTemporary()) {
1553 wxFileName fn(fileName);
1554 project.SetProjectName(fn.GetName());
1555 project.SetInitialImportPath(fn.GetPath());
1556 projectFileIO.SetProjectTitle();
1557 }
1558
1559 auto &history = ProjectHistory::Get( project );
1560
1561 history.PushState(XO("Imported '%s'").Format( fileName ), XO("Import"));
1562
1563 return true;
1564 }
1565
1566 // PRL: Undo history is incremented inside this:
1567 AddImportedTracks(fileName, std::move(newTracks));
1568
1569 return true;
1570}
1571
1572#include "Clipboard.h"
1573#include "ShuttleGui.h"
1574#include "HelpSystem.h"
1575
1576// Compact dialog
1577namespace {
1579{
1580public:
1582 : wxDialogWrapper(nullptr, wxID_ANY, XO("Compact Project"))
1583 {
1584 ShuttleGui S(this, eIsCreating);
1585
1586 S.StartVerticalLay(true);
1587 {
1588 S.AddFixedText(text, false, 500);
1589
1590 S.AddStandardButtons(eYesButton | eNoButton | eHelpButton);
1591 }
1592 S.EndVerticalLay();
1593
1594 FindWindowById(wxID_YES, this)->Bind(wxEVT_BUTTON, &CompactDialog::OnYes, this);
1595 FindWindowById(wxID_NO, this)->Bind(wxEVT_BUTTON, &CompactDialog::OnNo, this);
1596 FindWindowById(wxID_HELP, this)->Bind(wxEVT_BUTTON, &CompactDialog::OnGetURL, this);
1597
1598 Layout();
1599 Fit();
1600 Center();
1601 }
1602
1603 void OnYes(wxCommandEvent &WXUNUSED(evt))
1604 {
1605 EndModal(wxYES);
1606 }
1607
1608 void OnNo(wxCommandEvent &WXUNUSED(evt))
1609 {
1610 EndModal(wxNO);
1611 }
1612
1613 void OnGetURL(wxCommandEvent &WXUNUSED(evt))
1614 {
1615 HelpSystem::ShowHelp(this, L"File_Menu:_Compact_Project", true);
1616 }
1617};
1618}
1619
1621{
1622 auto &project = mProject;
1623 auto &undoManager = UndoManager::Get(project);
1624 auto &clipboard = Clipboard::Get();
1625 auto &projectFileIO = ProjectFileIO::Get(project);
1626 bool isBatch = project.mBatchMode > 0;
1627
1628 // Purpose of this is to remove the -wal file.
1629 projectFileIO.ReopenProject();
1630
1631 auto savedState = undoManager.GetSavedState();
1632 const auto currentState = undoManager.GetCurrentState();
1633 if (savedState < 0) {
1634 undoManager.StateSaved();
1635 savedState = undoManager.GetSavedState();
1636 if (savedState < 0) {
1637 wxASSERT(false);
1638 savedState = 0;
1639 }
1640 }
1641 const auto least = std::min<size_t>(savedState, currentState);
1642 const auto greatest = std::max<size_t>(savedState, currentState);
1643 std::vector<const TrackList*> trackLists;
1644 auto fn = [&](const UndoStackElem& elem) {
1645 if (auto pTracks = UndoTracks::Find(elem))
1646 trackLists.push_back(pTracks);
1647 };
1648 undoManager.VisitStates(fn, least, 1 + least);
1649 if (least != greatest)
1650 undoManager.VisitStates(fn, greatest, 1 + greatest);
1651
1652 int64_t total = projectFileIO.GetTotalUsage();
1653 int64_t used = projectFileIO.GetCurrentUsage(trackLists);
1654
1655 auto before = wxFileName::GetSize(projectFileIO.GetFileName());
1656
1657 CompactDialog dlg(
1658 XO("Compacting this project will free up disk space by removing unused bytes within the file.\n\n"
1659 "There is %s of free disk space and this project is currently using %s.\n"
1660 "\n"
1661 "If you proceed, the current Undo/Redo History and clipboard contents will be discarded "
1662 "and you will recover approximately %s of disk space.\n"
1663 "\n"
1664 "Do you want to continue?")
1665 .Format(Internat::FormatSize(projectFileIO.GetFreeDiskSpace()),
1666 Internat::FormatSize(before.GetValue()),
1667 Internat::FormatSize(total - used)));
1668 if (isBatch || dlg.ShowModal() == wxYES)
1669 {
1670 // We can remove redo states, if they are after the saved state.
1671 undoManager.RemoveStates(1 + greatest, undoManager.GetNumStates());
1672
1673 // We can remove all states between the current and the last saved.
1674 if (least < greatest)
1675 undoManager.RemoveStates(least + 1, greatest);
1676
1677 // We can remove all states before the current and the last saved.
1678 undoManager.RemoveStates(0, least);
1679
1680 // And clear the clipboard, if needed
1681 if (&mProject == clipboard.Project().lock().get())
1682 clipboard.Clear();
1683
1684 // Refresh the before space usage since it may have changed due to the
1685 // above actions.
1686 auto before = wxFileName::GetSize(projectFileIO.GetFileName());
1687
1688 projectFileIO.Compact(trackLists, true);
1689
1690 auto after = wxFileName::GetSize(projectFileIO.GetFileName());
1691
1692 if (!isBatch)
1693 {
1695 XO("Compacting actually freed %s of disk space.")
1696 .Format(Internat::FormatSize((before - after).GetValue())),
1697 XO("Compact Project"));
1698 }
1699
1700 undoManager.RenameState( undoManager.GetCurrentState(),
1701 XO("Compacted project file"),
1702 XO("Compact") );
1703 }
1704}
1705
1706static void RefreshAllTitles(bool bShowProjectNumbers )
1707{
1708 for ( auto pProject : AllProjects{} ) {
1709 if ( !GetProjectFrame( *pProject ).IsIconized() ) {
1711 bShowProjectNumbers ? pProject->GetProjectNumber() : -1 );
1712 }
1713 }
1714}
1715
1717 wxTopLevelWindow &window, AudacityProject &project )
1718{
1719 if( window.IsIconized() )
1720 window.Restore();
1721 window.Raise(); // May help identifying the window on Mac
1722
1723 // Construct this project's name and number.
1724 sProjName = project.GetProjectName();
1725 if ( sProjName.empty() ) {
1726 sProjName = _("<untitled>");
1727 UnnamedCount = std::count_if(
1729 []( const AllProjects::value_type &ptr ){
1730 return ptr->GetProjectName().empty();
1731 }
1732 );
1733 if ( UnnamedCount > 1 ) {
1734 sProjNumber.Printf(
1735 _("[Project %02i] "), project.GetProjectNumber() + 1 );
1736 RefreshAllTitles( true );
1737 }
1738 }
1739 else
1740 UnnamedCount = 0;
1741}
1742
1744 if( UnnamedCount > 1 )
1745 RefreshAllTitles( false );
1746}
wxT("CloseDown"))
int AudacityMessageBox(const TranslatableString &message, const TranslatableString &caption, long style, wxWindow *parent, int x, int y)
Toolkit-neutral facade for basic user interface services.
Declare functions to perform UTF-8 to std::wstring conversions.
XO("Cut/Copy/Paste")
The interface that all file import "plugins" (if you want to call them that) must implement....
std::vector< std::shared_ptr< Track > > TrackHolders
Definition: ImportRaw.h:24
#define XC(s, c)
Definition: Internat.h:37
#define _(s)
Definition: Internat.h:73
static const auto title
std::unique_ptr< const BasicUI::WindowPlacement > ProjectFramePlacement(AudacityProject *project)
Make a WindowPlacement object suitable for project (which may be null)
Definition: Project.cpp:129
wxString FilePath
Definition: Project.h:21
@ Cancel
Open was cancelled by the extension.
@ Handled
Save was handled by the extension.
@ Continue
Save was not handled by the extension.
@ Veto
Extension vetoed the close.
static const AudacityProject::AttachedObjects::RegisteredFactory sFileManagerKey
static void RefreshAllTitles(bool bShowProjectNumbers)
an object holding per-project preferred sample rate
AUDACITY_DLL_API wxFrame & GetProjectFrame(AudacityProject &project)
Get the top-level window associated with the project (as a wxFrame only, when you do not need to use ...
accessors for certain important windows associated with each project
FilePath SelectFile(FileNames::Operation op, const TranslatableString &message, const FilePath &default_path, const FilePath &default_filename, const FileExtension &default_extension, const FileTypes &fileTypes, int flags, wxWindow *parent)
Definition: SelectFile.cpp:17
@ eIsCreating
Definition: ShuttleGui.h:37
@ eYesButton
Definition: ShuttleGui.h:611
@ eHelpButton
Definition: ShuttleGui.h:613
@ eNoButton
Definition: ShuttleGui.h:612
void DoProjectTempoChange(ChannelGroup &group, double newTempo)
Definition: TempoChange.cpp:41
const auto tracks
const auto project
#define S(N)
Definition: ToChars.cpp:64
static Settings & settings()
Definition: TrackInfo.cpp:47
TranslatableString Verbatim(wxString str)
Require calls to the one-argument constructor to go through this distinct global function name.
An AudacityException with no visible message.
int ShowWarningDialog(wxWindow *parent, const wxString &internalDialogName, const TranslatableString &message, bool showCancelButton, const TranslatableString &footer)
Definition: Warning.cpp:90
static const auto fn
const_iterator end() const
Definition: Project.cpp:27
Container::value_type value_type
Definition: Project.h:57
const_iterator begin() const
Definition: Project.cpp:22
Wrap wxMessageDialog so that caption IS translatable.
The top-level handle to an Audacity project. It serves as a source of events that other objects can b...
Definition: Project.h:90
Client code makes static instance from a factory of attachments; passes it to Get or Find as a retrie...
Definition: ClientData.h:275
static Clipboard & Get()
Definition: Clipboard.cpp:28
virtual int GetFilterIndex() const
virtual wxString GetPath() const
virtual void GetPaths(wxArrayString &paths) const
virtual void SetFilterIndex(int filterIndex)
virtual int ShowModal()
static TranslatableString WriteFailureMessage(const wxFileName &fileName)
void Append(const FilePath &file)
Definition: FileHistory.h:46
static FileHistory & Global()
Definition: FileHistory.cpp:39
FILES_API const FileType AudacityProjects
Definition: FileNames.h:71
Abstract base class used in importing a file.
static void ShowHelp(wxWindow *parent, const FilePath &localFileName, const URLString &remoteURL, bool bModal=false, bool alwaysDefaultBrowser=false)
Definition: HelpSystem.cpp:231
const wxString & GET() const
Explicit conversion to wxString, meant to be ugly-looking and demanding of a comment why it's correct...
Definition: Identifier.h:66
Base class for FlacImportFileHandle, LOFImportFileHandle, MP3ImportFileHandle, OggImportFileHandle an...
Definition: ImportPlugin.h:111
virtual wxInt32 GetStreamCount()=0
virtual void SetStreamUsage(wxInt32 StreamID, bool Use)=0
Interface used to report on import state and progress.
static Importer & Get()
Definition: Import.cpp:102
static void SetLastOpenType(const FileNames::FileType &type)
Definition: Import.cpp:253
FileNames::FileTypes GetFileTypes(const FileNames::FileType &extraType={})
Definition: Import.cpp:208
bool Import(AudacityProject &project, const FilePath &fName, ImportProgressListener *importProgressListener, WaveTrackFactory *trackFactory, TrackHolders &tracks, Tags *tags, std::optional< LibFileFormats::AcidizerTags > &outAcidTags, TranslatableString &errorMessage)
Definition: Import.cpp:486
static size_t SelectDefaultOpenType(const FileNames::FileTypes &fileTypes)
Definition: Import.cpp:273
static void SetDefaultOpenType(const FileNames::FileType &type)
Definition: Import.cpp:263
static TranslatableString FormatSize(wxLongLong size)
Convert a number to a string while formatting it in bytes, KB, MB, GB.
Definition: Internat.cpp:179
Makes a temporary project that doesn't display on the screen.
AudacityProject & Project()
static void RebuildAllMenuBars()
static FilePath GetLongFileName(const FilePath &shortFileName)
AudioTrack subclass that can also be audibly replayed by the program.
Definition: PlayableTrack.h:40
bool GetSolo() const
Definition: PlayableTrack.h:48
static PluginManager & Get()
void SetProjectTitle(int number=-1)
static ProjectFileIO & Get(AudacityProject &project)
const FilePath & GetFileName() const
static void FixTracks(TrackList &tracks, const std::function< void(const TranslatableString &)> &onError, const std::function< void(const TranslatableString &)> &onUnlink)
Attempts to find and fix problems in tracks.
std::shared_ptr< TrackList > mLastSavedTracks
static bool IsAlreadyOpen(const FilePath &projPathName)
AudacityProject & mProject
bool SaveCopy(const FilePath &fileName=wxT(""))
bool Import(const std::vector< FilePath > &fileNames, bool addToHistory=true)
bool SaveAs(bool allowOverwrite=false)
static void DiscardAutosave(const FilePath &filename)
void AddImportedTracks(const FilePath &fileName, TrackHolders &&newTracks)
static wxArrayString ShowOpenDialog(FileNames::Operation op, const FileNames::FileType &extraType={})
Show an open dialogue for opening audio files, and possibly other sorts of files.
bool DoSave(const FilePath &fileName, bool fromSaveAs)
bool SaveFromTimerRecording(wxFileName fnFile)
static AudacityProject * OpenFile(const ProjectChooserFn &chooser, const FilePath &fileName, bool addtohistory=true)
ReadProjectResults ReadProjectFile(const FilePath &fileName, bool discardAutosave=false)
std::function< AudacityProject &(bool)> ProjectChooserFn
A function that returns a project to use for opening a file; argument is true if opening a project fi...
static ProjectFileManager & Get(AudacityProject &project)
ProjectFileManager(AudacityProject &project)
AudacityProject * OpenProjectFile(const FilePath &fileName, bool addtohistory)
static ProjectHistory & Get(AudacityProject &project)
static ProjectSettings & Get(AudacityProject &project)
static ProjectStatus & Get(AudacityProject &project)
void Set(const TranslatableString &msg, StatusBarField field=MainStatusBarField())
static ProjectTimeSignature & Get(AudacityProject &project)
static RealtimeEffectList & Get(AudacityProject &project)
void Clear()
Use only in the main thread. Sends Remove messages.
Derived from ShuttleGuiBase, an Audacity specific class for shuttling data to and from GUI.
Definition: ShuttleGui.h:640
void Merge(const Tags &other)
Definition: Tags.cpp:246
static Tags & Get(AudacityProject &project)
Definition: Tags.cpp:214
static Tags & Set(AudacityProject &project, const std::shared_ptr< Tags > &tags)
Definition: Tags.cpp:224
TitleRestorer(wxTopLevelWindow &window, AudacityProject &project)
Track * Get()
Definition: TrackFocus.cpp:156
Abstract base class for an object holding data associated with points on a time axis.
Definition: Track.h:110
std::shared_ptr< Track > Holder
Definition: Track.h:202
const wxString & GetName() const
Name is always the same for all channels of a group.
Definition: Track.cpp:64
virtual Holder Duplicate(DuplicateOptions={}) const
public nonvirtual duplication function that invokes Clone()
Definition: Track.cpp:109
A flat linked list of tracks supporting Add, Remove, Clear, and Contains, serialization of the list o...
Definition: Track.h:850
bool empty() const
Definition: Track.cpp:758
static TrackListHolder Create(AudacityProject *pOwner)
Definition: Track.cpp:330
auto Any() -> TrackIterRange< TrackType >
Definition: Track.h:950
static TrackList & Get(AudacityProject &project)
Definition: Track.cpp:314
static auto Channels(TrackType *pTrack) -> TrackIterRange< TrackType >
Definition: Track.h:1016
static TrackPanel & Get(AudacityProject &project)
Definition: TrackPanel.cpp:234
Holds a msgid for the translation catalog; may also bind format arguments.
Identifier MSGID() const
MSGID is the English lookup key in the catalog, not necessarily for user's eyes if locale is some oth...
static UndoManager & Get(AudacityProject &project)
Definition: UndoManager.cpp:71
void StateSaved()
bool UnsavedChanges() const
An error dialog about unwritable location, that allows to navigate to settings quickly.
Can be thrown when user cancels operations, as with a progress dialog. Delayed handler does nothing.
Definition: UserException.h:17
void ReinitScrollbars()
Definition: Viewport.h:172
void ZoomFitHorizontallyAndShowTrack(Track *pTrack)
Definition: Viewport.cpp:439
static Viewport & Get(AudacityProject &project)
Definition: Viewport.cpp:33
static WaveTrackFactory & Get(AudacityProject &project)
Definition: WaveTrack.cpp:3349
A Track that contains audio waveform data.
Definition: WaveTrack.h:203
void ZipClips(bool mustAlign=true)
Definition: WaveTrack.cpp:3290
double GetRate() const override
Definition: WaveTrack.cpp:798
auto Intervals()
Definition: WaveTrack.h:670
void OnImportResult(ImportResult result) override
Used to report on import result for file handle passed as argument to OnImportFileOpened.
bool OnImportFileOpened(ImportFileHandle &importFileHandle) override
std::unique_ptr< BasicUI::ProgressDialog > mProgressDialog
@ ProgressShowCancel
Definition: BasicUI.h:142
void CallAfter(Action action)
Schedule an action to be done later, and in the main thread.
Definition: BasicUI.cpp:213
void ShowErrorDialog(const WindowPlacement &placement, const TranslatableString &dlogTitle, const TranslatableString &message, const ManualPageID &helpPage, const ErrorDialogOptions &options={})
Show an error dialog with a link to the manual for further help.
Definition: BasicUI.h:264
MessageBoxResult ShowMessageBox(const TranslatableString &message, MessageBoxOptions options={})
Show a modal message box with either Ok or Yes and No, and optionally Cancel.
Definition: BasicUI.h:279
std::unique_ptr< ProgressDialog > MakeProgress(const TranslatableString &title, const TranslatableString &message, unsigned flags=(ProgressShowStop|ProgressShowCancel), const TranslatableString &remainingLabelText={})
Create and display a progress dialog.
Definition: BasicUI.h:294
FILES_API bool IsOnFATFileSystem(const FilePath &path)
FILES_API bool IsPathAvailable(const FilePath &Path)
FILES_API wxString AbbreviatePath(const wxFileName &fileName)
Give enough of the path to identify the device. (On Windows, drive letter plus ':')
FILES_API void UpdateDefaultPath(Operation op, const FilePath &path)
FILES_API FilePath FindDefaultPath(Operation op)
std::optional< ProjectSyncInfo > GetProjectSyncInfo(const ProjectSyncInfoInput &in)
void SynchronizeProject(const std::vector< std::shared_ptr< AnalyzedAudioClip > > &clips, ProjectInterface &project, bool projectWasEmpty)
void SelectNone(AudacityProject &project)
FILES_API bool FATFilesystemDenied(const FilePath &path, const TranslatableString &msg, const BasicUI::WindowPlacement &placement={})
FILES_API wxString UnsavedProjectFileName()
TRACK_API TrackList * Find(const UndoStackElem &state)
Definition: UndoTracks.cpp:47
WAVE_TRACK_API void CloseLock(WaveTrack &track) noexcept
Should be called upon project close. Not balanced by unlocking calls.
bool ImportProject(AudacityProject &dest, const FilePath &fileName)
std::pair< const char *, const char * > Pair
wxString FindHelpUrl(const TranslatableString &libraryError)
std::vector< std::shared_ptr< MIR::AnalyzedAudioClip > > RunTempoDetection(const std::vector< std::shared_ptr< ClipMirAudioReader > > &readers, const MIR::ProjectInterface &project, bool projectWasEmpty)
TranslatableString Message(unsigned trackCount)
std::string ToUTF8(const std::wstring &wstr)
wxString ToWXString(const std::string &str)
const char * end(const char *str) noexcept
Definition: StringUtils.h:106
const char * begin(const char *str) noexcept
Definition: StringUtils.h:101
Options for variations of error dialogs; the default is for modal dialogs.
Definition: BasicUI.h:52
static void OnLoad(AudacityProject &project)
static OnOpenAction OnOpen(AudacityProject &project, const std::string &path)
static OnSaveAction OnSave(AudacityProject &project, const ProjectSaveCallback &projectSaveCallback)
static OnCloseAction OnClose(AudacityProject &project)
Choices when duplicating a track.
Definition: Track.h:271
DuplicateOptions Backup() &&
Definition: Track.h:288
Holds one item with description and time range for the UndoManager.
Definition: UndoManager.h:117