diff --git a/src/AudacityApp.cpp b/src/AudacityApp.cpp index cb207e638..20f63cd4b 100644 --- a/src/AudacityApp.cpp +++ b/src/AudacityApp.cpp @@ -84,6 +84,7 @@ It handles initialization and termination by subclassing wxApp. #include "PluginManager.h" #include "Project.h" #include "ProjectAudioIO.h" +#include "ProjectFileManager.h" #include "ProjectHistory.h" #include "ProjectManager.h" #include "ProjectSettings.h" @@ -871,7 +872,7 @@ END_EVENT_TABLE() // - Inform the user if DefaultOpenPath not set. // - Switch focus to correct instance of project window, if already open. bool AudacityApp::MRUOpen(const FilePath &fullPathStr) { - // Most of the checks below are copied from AudacityProject::OpenFiles. + // Most of the checks below are copied from ProjectManager::OpenFiles. // - some rationalisation might be possible. AudacityProject *proj = GetActiveProject(); @@ -887,7 +888,7 @@ bool AudacityApp::MRUOpen(const FilePath &fullPathStr) { // Test here even though AudacityProject::OpenFile() also now checks, because // that method does not return the bad result. // That itself may be a FIXME. - if (ProjectManager::IsAlreadyOpen(fullPathStr)) + if (ProjectFileManager::IsAlreadyOpen(fullPathStr)) return false; // DMM: If the project is dirty, that means it's been touched at @@ -946,7 +947,7 @@ void AudacityApp::OnMRUFile(wxCommandEvent& event) { // PRL: Don't call SafeMRUOpen // -- if open fails for some exceptional reason of resource exhaustion that // the user can correct, leave the file in history. - if (!ProjectManager::IsAlreadyOpen(fullPathStr) && !MRUOpen(fullPathStr)) + if (!ProjectFileManager::IsAlreadyOpen(fullPathStr) && !MRUOpen(fullPathStr)) history.RemoveFileFromHistory(n); } diff --git a/src/BatchProcessDialog.cpp b/src/BatchProcessDialog.cpp index b8d17a44c..96f94acd8 100644 --- a/src/BatchProcessDialog.cpp +++ b/src/BatchProcessDialog.cpp @@ -42,6 +42,7 @@ #include "Menus.h" #include "Prefs.h" #include "Project.h" +#include "ProjectFileManager.h" #include "ProjectHistory.h" #include "ProjectManager.h" #include "ProjectWindow.h" @@ -464,7 +465,7 @@ void ApplyMacroDialog::OnApplyToFiles(wxCommandEvent & WXUNUSED(event)) fileList->EnsureVisible(i); auto success = GuardedCall< bool >( [&] { - ProjectManager::Get( *project ).Import(files[i]); + ProjectFileManager::Get( *project ).Import(files[i]); ProjectWindow::Get( *project ).ZoomAfterImport(nullptr); SelectActions::DoSelectAll(*project); if (!mMacroCommands.ApplyMacro(mCatalog)) diff --git a/src/ProjectFileManager.cpp b/src/ProjectFileManager.cpp index 0c7fab475..f99a3c5e5 100644 --- a/src/ProjectFileManager.cpp +++ b/src/ProjectFileManager.cpp @@ -13,22 +13,43 @@ Paul Licameli split from AudacityProject.cpp #include "Experimental.h" #include // for wxPrintf + +#if defined(__WXGTK__) +#include +#endif + #include #include "AutoRecovery.h" #include "Dependencies.h" #include "DirManager.h" #include "FileNames.h" +#include "Legacy.h" +#include "Menus.h" +#include "PlatformCompatibility.h" #include "Project.h" #include "ProjectFileIO.h" +#include "ProjectFileIORegistry.h" +#include "ProjectFSCK.h" +#include "ProjectHistory.h" +#include "ProjectSelectionManager.h" #include "ProjectSettings.h" +#include "ProjectWindow.h" #include "SelectionState.h" +#include "Sequence.h" +#include "Tags.h" +#include "TrackPanel.h" #include "UndoManager.h" +#include "WaveClip.h" #include "WaveTrack.h" #include "wxFileNameWrapper.h" +#include "effects/EffectManager.h" #include "export/Export.h" +#include "import/Import.h" +#include "commands/CommandContext.h" #include "ondemand/ODComputeSummaryTask.h" #include "ondemand/ODManager.h" #include "ondemand/ODTask.h" +#include "toolbars/SelectionBar.h" #include "widgets/AudacityMessageBox.h" #include "widgets/ErrorDialog.h" #include "widgets/FileHistory.h" @@ -914,3 +935,768 @@ void ProjectFileManager::CloseLock() mLastSavedTracks.reset(); } } + +// static method, can be called outside of a project +wxArrayString ProjectFileManager::ShowOpenDialog(const wxString &extraformat, const wxString &extrafilter) +{ + FormatList l; + wxString filter; ///< List of file format names and extensions, separated + /// by | characters between _formats_ and extensions for each _format_, i.e. + /// format1name | *.ext | format2name | *.ex1;*.ex2 + wxString all; ///< One long list of all supported file extensions, + /// semicolon separated + + if (!extraformat.empty()) + { // additional format specified + all = extrafilter + wxT(';'); + // add it to the "all supported files" filter string + } + + // Construct the filter + Importer::Get().GetSupportedImportFormats(&l); + + for (const auto &format : l) { + /* this loop runs once per supported _format_ */ + const Format *f = &format; + + wxString newfilter = f->formatName + wxT("|"); + // bung format name into string plus | separator + for (size_t i = 0; i < f->formatExtensions.size(); i++) { + /* this loop runs once per valid _file extension_ for file containing + * the current _format_ */ + if (!newfilter.Contains(wxT("*.") + f->formatExtensions[i] + wxT(";"))) + newfilter += wxT("*.") + f->formatExtensions[i] + wxT(";"); + if (!all.Contains(wxT("*.") + f->formatExtensions[i] + wxT(";"))) + all += wxT("*.") + f->formatExtensions[i] + wxT(";"); + } + newfilter.RemoveLast(1); + filter += newfilter; + filter += wxT("|"); + } + all.RemoveLast(1); + filter.RemoveLast(1); + + // For testing long filters +#if 0 + wxString test = wxT("*.aaa;*.bbb;*.ccc;*.ddd;*.eee"); + all = test + wxT(';') + test + wxT(';') + test + wxT(';') + + test + wxT(';') + test + wxT(';') + test + wxT(';') + + test + wxT(';') + test + wxT(';') + test + wxT(';') + + all; +#endif + + /* i18n-hint: The vertical bars and * are essential here.*/ + wxString mask = _("All files|*|All supported files|") + + all + wxT("|"); // "all" and "all supported" entries + if (!extraformat.empty()) + { // append caller-defined format if supplied + mask += extraformat + wxT("|") + extrafilter + wxT("|"); + } + mask += filter; // put the names and extensions of all the importer formats + // we built up earlier into the mask + + // Retrieve saved path and type + auto path = FileNames::FindDefaultPath(FileNames::Operation::Open); + wxString type = gPrefs->Read(wxT("/DefaultOpenType"),mask.BeforeFirst(wxT('|'))); + + // Convert the type to the filter index + int index = mask.First(type + wxT("|")); + if (index == wxNOT_FOUND) { + index = 0; + } + else { + index = mask.Left(index).Freq(wxT('|')) / 2; + if (index < 0) { + index = 0; + } + } + + // Construct and display the file dialog + wxArrayString selected; + + FileDialogWrapper dlog(NULL, + _("Select one or more files"), + path, + wxT(""), + mask, + wxFD_OPEN | wxFD_MULTIPLE | wxRESIZE_BORDER); + + dlog.SetFilterIndex(index); + + int dialogResult = dlog.ShowModal(); + + // Convert the filter index to type and save + index = dlog.GetFilterIndex(); + for (int i = 0; i < index; i++) { + mask = mask.AfterFirst(wxT('|')).AfterFirst(wxT('|')); + } + gPrefs->Write(wxT("/DefaultOpenType"), mask.BeforeFirst(wxT('|'))); + gPrefs->Write(wxT("/LastOpenType"), mask.BeforeFirst(wxT('|'))); + gPrefs->Flush(); + + if (dialogResult == wxID_OK) { + // Return the selected files + dlog.GetPaths(selected); + } + return selected; +} + +// static method, can be called outside of a project +bool ProjectFileManager::IsAlreadyOpen(const FilePath &projPathName) +{ + const wxFileName newProjPathName(projPathName); + auto start = AllProjects{}.begin(), finish = AllProjects{}.end(), + iter = std::find_if( start, finish, + [&]( const AllProjects::value_type &ptr ){ + return newProjPathName.SameAs(wxFileNameWrapper{ ptr->GetFileName() }); + } ); + if (iter != finish) { + wxString errMsg = + wxString::Format(_("%s is already open in another window."), + newProjPathName.GetName()); + wxLogError(errMsg); + AudacityMessageBox(errMsg, _("Error Opening Project"), wxOK | wxCENTRE); + return true; + } + return false; +} + +XMLTagHandler * +ProjectFileManager::RecordingRecoveryFactory( AudacityProject &project ) { + auto &ProjectFileManager = Get( project ); + auto &ptr = ProjectFileManager.mRecordingRecoveryHandler; + if (!ptr) + ptr = + std::make_unique( &project ); + return ptr.get(); +} + +ProjectFileIORegistry::Entry +ProjectFileManager::sRecoveryFactory{ + wxT("recordingrecovery"), RecordingRecoveryFactory +}; + +// XML handler for tag +class ImportXMLTagHandler final : public XMLTagHandler +{ + public: + ImportXMLTagHandler(AudacityProject* pProject) { mProject = pProject; } + + bool HandleXMLTag(const wxChar *tag, const wxChar **attrs) override; + XMLTagHandler *HandleXMLChild(const wxChar * WXUNUSED(tag)) override + { return NULL; } + + // Don't want a WriteXML method because ImportXMLTagHandler is not a WaveTrack. + // tags are instead written by AudacityProject::WriteXML. + // void WriteXML(XMLWriter &xmlFile) /* not override */ { wxASSERT(false); } + + private: + AudacityProject* mProject; +}; + +bool ImportXMLTagHandler::HandleXMLTag(const wxChar *tag, const wxChar **attrs) +{ + if (wxStrcmp(tag, wxT("import")) || attrs==NULL || (*attrs)==NULL || wxStrcmp(*attrs++, wxT("filename"))) + return false; + wxString strAttr = *attrs; + if (!XMLValueChecker::IsGoodPathName(strAttr)) + { + // Maybe strAttr is just a fileName, not the full path. Try the project data directory. + wxFileNameWrapper fileName{ + DirManager::Get( *mProject ).GetProjectDataDir(), strAttr }; + if (XMLValueChecker::IsGoodFileName(strAttr, fileName.GetPath(wxPATH_GET_VOLUME))) + strAttr = fileName.GetFullPath(); + else + { + wxLogWarning(wxT("Could not import file: %s"), strAttr); + return false; + } + } + + WaveTrackArray trackArray; + + // Guard this call so that C++ exceptions don't propagate through + // the expat library + GuardedCall( + [&] { + ProjectFileManager::Get( *mProject ).Import(strAttr, &trackArray); }, + [&] (AudacityException*) { trackArray.clear(); } + ); + + if (trackArray.empty()) + return false; + + // Handle other attributes, now that we have the tracks. + attrs++; + const wxChar** pAttr; + bool bSuccess = true; + + for (size_t i = 0; i < trackArray.size(); i++) + { + // Most of the "import" tag attributes are the same as for "wavetrack" tags, + // so apply them via WaveTrack::HandleXMLTag(). + bSuccess = trackArray[i]->HandleXMLTag(wxT("wavetrack"), attrs); + + // "offset" tag is ignored in WaveTrack::HandleXMLTag except for legacy projects, + // so handle it here. + double dblValue; + pAttr = attrs; + while (*pAttr) + { + const wxChar *attr = *pAttr++; + const wxChar *value = *pAttr++; + const wxString strValue = value; + if (!wxStrcmp(attr, wxT("offset")) && + XMLValueChecker::IsGoodString(strValue) && + Internat::CompatibleToDouble(strValue, &dblValue)) + trackArray[i]->SetOffset(dblValue); + } + } + return bSuccess; +}; + +XMLTagHandler * +ProjectFileManager::ImportHandlerFactory( AudacityProject &project ) { + auto &ProjectFileManager = Get( project ); + auto &ptr = ProjectFileManager.mImportXMLTagHandler; + if (!ptr) + ptr = + std::make_unique( &project ); + return ptr.get(); +} + +ProjectFileIORegistry::Entry +ProjectFileManager::sImportHandlerFactory{ + wxT("import"), ImportHandlerFactory +}; + +// FIXME:? TRAP_ERR This should return a result that is checked. +// See comment in AudacityApp::MRUOpen(). +void ProjectFileManager::OpenFile(const FilePath &fileNameArg, bool addtohistory) +{ + auto &project = mProject; + auto &history = ProjectHistory::Get( project ); + auto &projectFileIO = ProjectFileIO::Get( project ); + auto &tracks = TrackList::Get( project ); + auto &trackPanel = TrackPanel::Get( project ); + auto &dirManager = DirManager::Get( project ); + auto &window = ProjectWindow::Get( project ); + + // On Win32, we may be given a short (DOS-compatible) file name on rare + // occassions (e.g. stuff like "C:\PROGRA~1\AUDACI~1\PROJEC~1.AUP"). We + // convert these to long file name first. + auto fileName = PlatformCompatibility::ConvertSlashInFileName( + PlatformCompatibility::GetLongFileName(fileNameArg)); + + // Make sure it isn't already open. + // Vaughan, 2011-03-25: This was done previously in AudacityProject::OpenFiles() + // and AudacityApp::MRUOpen(), but if you open an aup file by double-clicking it + // from, e.g., Win Explorer, it would bypass those, get to here with no check, + // then open a NEW project from the same data with no warning. + // This was reported in http://bugzilla.audacityteam.org/show_bug.cgi?id=137#c17, + // but is not really part of that bug. Anyway, prevent it! + if (IsAlreadyOpen(fileName)) + return; + + + // Data loss may occur if users mistakenly try to open ".aup.bak" files + // left over from an unsuccessful save or by previous versions of Audacity. + // So we always refuse to open such files. + if (fileName.Lower().EndsWith(wxT(".aup.bak"))) + { + AudacityMessageBox( + _("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."), + _("Warning - Backup File Detected"), + wxOK | wxCENTRE, &window); + return; + } + + if (!::wxFileExists(fileName)) { + AudacityMessageBox( + wxString::Format( _("Could not open file: %s"), fileName ), + ("Error Opening File"), + wxOK | wxCENTRE, &window); + return; + } + + // We want to open projects using wxTextFile, but if it's NOT a project + // file (but actually a WAV file, for example), then wxTextFile will spin + // for a long time searching for line breaks. So, we look for our + // signature at the beginning of the file first: + + char buf[16]; + { + wxFFile ff(fileName, wxT("rb")); + if (!ff.IsOpened()) { + AudacityMessageBox( + wxString::Format( _("Could not open file: %s"), fileName ), + _("Error opening file"), + wxOK | wxCENTRE, &window); + return; + } + int numRead = ff.Read(buf, 15); + if (numRead != 15) { + AudacityMessageBox(wxString::Format(_("File may be invalid or corrupted: \n%s"), + fileName), _("Error Opening File or Project"), + wxOK | wxCENTRE, &window); + ff.Close(); + return; + } + buf[15] = 0; + } + + wxString temp = LAT1CTOWX(buf); + + if (temp == wxT("AudacityProject")) { + // It's an Audacity 1.0 (or earlier) project file. + // If they bail out, return and do no more. + if( !projectFileIO.WarnOfLegacyFile() ) + return; + // Convert to the NEW format. + bool success = ConvertLegacyProjectFile(wxFileName{ fileName }); + if (!success) { + AudacityMessageBox(_("Audacity was unable to convert an Audacity 1.0 project to the new project format."), + _("Error Opening Project"), + wxOK | wxCENTRE, &window); + return; + } + else { + temp = wxT("Clear(true); + mFileName = wxT(""); + SetProjectTitle(); + mTrackPanel->Refresh(true); + */ + closed = true; + SetMenuClose(true); + window.Close(); + return; + } + else if (status & FSCKstatus_CHANGED) + { + // Mark the wave tracks as changed and redraw. + for ( auto wt : tracks.Any() ) + // Only wave tracks have a notion of "changed". + for (const auto &clip: wt->GetClips()) + clip->MarkChanged(); + + trackPanel.Refresh(true); + + // Vaughan, 2010-08-20: This was bogus, as all the actions in ProjectFSCK + // that return FSCKstatus_CHANGED cannot be undone. + // this->PushState(_("Project checker repaired file"), _("Project Repair")); + + if (status & FSCKstatus_SAVE_AUP) + Save(), saved = true; + } + } + + if (mImportXMLTagHandler) { + if (!saved) + // We processed an tag, so save it as a normal project, + // with no tags. + Save(); + } + } + else { + // Vaughan, 2011-10-30: + // See first topic at http://bugzilla.audacityteam.org/show_bug.cgi?id=451#c16. + // Calling mTracks->Clear() with deleteTracks true results in data loss. + + // PRL 2014-12-19: + // I made many changes for wave track memory management, but only now + // read the above comment. I may have invalidated the fix above (which + // may have spared the files at the expense of leaked memory). But + // here is a better way to accomplish the intent, doing like what happens + // when the project closes: + for ( auto pTrack : tracks.Any< WaveTrack >() ) + pTrack->CloseLock(); + + tracks.Clear(); //tracks.Clear(true); + + project.SetFileName( wxT("") ); + projectFileIO.SetProjectTitle(); + + wxLogError(wxT("Could not parse file \"%s\". \nError: %s"), fileName, errorStr); + + wxString url = wxT("FAQ:Errors_on_opening_or_recovering_an_Audacity_project"); + + // Certain errors have dedicated help. + // On April-4th-2018, we did not request translation of the XML errors. + // If/when we do, we will need _() around the comparison strings. + if( errorStr.Contains( ("not well-formed (invalid token)") ) ) + url = "Error:_not_well-formed_(invalid_token)_at_line_x"; + else if( errorStr.Contains(("reference to invalid character number") )) + url = "Error_Opening_Project:_Reference_to_invalid_character_number_at_line_x"; + else if( errorStr.Contains(("mismatched tag") )) + url += "#mismatched"; + +// These two errors with FAQ entries are reported elsewhere, not here.... +//#[[#import-error|Error Importing: Aup is an Audacity Project file. Use the File > Open command]] +//#[[#corrupt|Error Opening File or Project: File may be invalid or corrupted]] + +// If we did want to handle every single parse error, these are they.... +/* + XML_L("out of memory"), + XML_L("syntax error"), + XML_L("no element found"), + XML_L("not well-formed (invalid token)"), + XML_L("unclosed token"), + XML_L("partial character"), + XML_L("mismatched tag"), + XML_L("duplicate attribute"), + XML_L("junk after document element"), + XML_L("illegal parameter entity reference"), + XML_L("undefined entity"), + XML_L("recursive entity reference"), + XML_L("asynchronous entity"), + XML_L("reference to invalid character number"), + XML_L("reference to binary entity"), + XML_L("reference to external entity in attribute"), + XML_L("XML or text declaration not at start of entity"), + XML_L("unknown encoding"), + XML_L("encoding specified in XML declaration is incorrect"), + XML_L("unclosed CDATA section"), + XML_L("error in processing external entity reference"), + XML_L("document is not standalone"), + XML_L("unexpected parser state - please send a bug report"), + XML_L("entity declared in parameter entity"), + XML_L("requested feature requires XML_DTD support in Expat"), + XML_L("cannot change setting once parsing has begun"), + XML_L("unbound prefix"), + XML_L("must not undeclare prefix"), + XML_L("incomplete markup in parameter entity"), + XML_L("XML declaration not well-formed"), + XML_L("text declaration not well-formed"), + XML_L("illegal character(s) in public id"), + XML_L("parser suspended"), + XML_L("parser not suspended"), + XML_L("parsing aborted"), + XML_L("parsing finished"), + XML_L("cannot suspend in external parameter entity"), + XML_L("reserved prefix (xml) must not be undeclared or bound to another namespace name"), + XML_L("reserved prefix (xmlns) must not be declared or undeclared"), + XML_L("prefix must not be bound to one of the reserved namespace names") +*/ + + ShowErrorDialog( + &window, + _("Error Opening Project"), + errorStr, + url); + } +} + +std::vector< std::shared_ptr< Track > > +ProjectFileManager::AddImportedTracks(const FilePath &fileName, + TrackHolders &&newTracks) + { + auto &project = mProject; + auto &history = ProjectHistory::Get( project ); + auto &projectFileIO = ProjectFileIO::Get( project ); + auto &tracks = TrackList::Get( project ); + + std::vector< std::shared_ptr< Track > > results; + + SelectActions::SelectNone( project ); + + bool initiallyEmpty = tracks.empty(); + double newRate = 0; + wxString trackNameBase = fileName.AfterLast(wxFILE_SEP_PATH).BeforeLast('.'); + int i = -1; + + // Must add all tracks first (before using Track::IsLeader) + for (auto &group : newTracks) { + if (group.empty()) { + wxASSERT(false); + continue; + } + auto first = group.begin()->get(); + auto nChannels = group.size(); + for (auto &uNewTrack : group) { + auto newTrack = tracks.Add( uNewTrack ); + results.push_back(newTrack->SharedPointer()); + } + tracks.GroupChannels(*first, nChannels); + } + newTracks.clear(); + + // Now name them + + // Add numbers to track names only if there is more than one (mono or stereo) + // track (not necessarily, more than one channel) + const bool useSuffix = + make_iterator_range( results.begin() + 1, results.end() ) + .any_of( []( decltype(*results.begin()) &pTrack ) + { return pTrack->IsLeader(); } ); + + for (const auto &newTrack : results) { + if ( newTrack->IsLeader() ) + // Count groups only + ++i; + + newTrack->SetSelected(true); + + if ( useSuffix ) + newTrack->SetName(trackNameBase + wxString::Format(wxT(" %d" ), i + 1)); + else + newTrack->SetName(trackNameBase); + + newTrack->TypeSwitch( [&](WaveTrack *wt) { + if (newRate == 0) + newRate = wt->GetRate(); + + // Check if NEW track contains aliased blockfiles and if yes, + // remember this to show a warning later + if(WaveClip* clip = wt->GetClipByIndex(0)) { + BlockArray &blocks = clip->GetSequence()->GetBlockArray(); + if (blocks.size()) + { + SeqBlock& block = blocks[0]; + if (block.f->IsAlias()) + SetImportedDependencies( true ); + } + } + }); + } + + // Automatically assign rate of imported file to whole project, + // if this is the first file that is imported + if (initiallyEmpty && newRate > 0) { + auto &settings = ProjectSettings::Get( project ); + settings.SetRate( newRate ); + SelectionBar::Get( project ).SetRate( newRate ); + } + + history.PushState(wxString::Format(_("Imported '%s'"), fileName), + _("Import")); + +#if defined(__WXGTK__) + // See bug #1224 + // The track panel hasn't we been fully created, so the DoZoomFit() will not give + // expected results due to a window width of zero. Should be safe to yield here to + // allow the creattion to complete. If this becomes a problem, it "might" be possible + // to queue a dummy event to trigger the DoZoomFit(). + wxEventLoopBase::GetActive()->YieldFor(wxEVT_CATEGORY_UI | wxEVT_CATEGORY_USER_INPUT); +#endif + + if (initiallyEmpty && !projectFileIO.IsProjectSaved() ) { + wxString name = fileName.AfterLast(wxFILE_SEP_PATH).BeforeLast(wxT('.')); + project.SetFileName( + ::wxPathOnly(fileName) + wxFILE_SEP_PATH + name + wxT(".aup") ); + projectFileIO.SetLoadedFromAup( false ); + projectFileIO.SetProjectTitle(); + } + + // Moved this call to higher levels to prevent flicker redrawing everything on each file. + // HandleResize(); + + return results; +} + +// If pNewTrackList is passed in non-NULL, it gets filled with the pointers to NEW tracks. +bool ProjectFileManager::Import( + const FilePath &fileName, WaveTrackArray* pTrackArray /*= NULL*/) +{ + auto &project = mProject; + auto &dirManager = DirManager::Get( project ); + auto oldTags = Tags::Get( project ).shared_from_this(); + TrackHolders newTracks; + wxString errorMessage; + + { + // Backup Tags, before the import. Be prepared to roll back changes. + bool committed = false; + auto cleanup = finally([&]{ + if ( !committed ) + Tags::Set( project, oldTags ); + }); + auto newTags = oldTags->Duplicate(); + Tags::Set( project, newTags ); + + bool success = Importer::Get().Import(fileName, + &TrackFactory::Get( project ), + newTracks, + newTags.get(), + errorMessage); + + if (!errorMessage.empty()) { + // Error message derived from Importer::Import + // Additional help via a Help button links to the manual. + ShowErrorDialog(&GetProjectFrame( project ), _("Error Importing"), + errorMessage, wxT("Importing_Audio")); + } + if (!success) + return false; + + FileHistory::Global().AddFileToHistory(fileName); + + // no more errors, commit + committed = true; + } + + // for LOF ("list of files") files, do not import the file as if it + // were an audio file itself + if (fileName.AfterLast('.').IsSameAs(wxT("lof"), false)) { + // PRL: don't redundantly do the steps below, because we already + // did it in case of LOF, because of some weird recursion back to this + // same function. I think this should be untangled. + + // So Undo history push is not bypassed, despite appearances. + return false; + } + + // PRL: Undo history is incremented inside this: + auto newSharedTracks = AddImportedTracks(fileName, std::move(newTracks)); + + if (pTrackArray) { + for (const auto &newTrack : newSharedTracks) { + newTrack->TypeSwitch( [&](WaveTrack *wt) { + pTrackArray->push_back( wt->SharedPointer< WaveTrack >() ); + }); + } + } + + int mode = gPrefs->Read(wxT("/AudioFiles/NormalizeOnLoad"), 0L); + if (mode == 1) { + //TODO: All we want is a SelectAll() + SelectActions::SelectNone( project ); + SelectActions::SelectAllIfNone( project ); + const CommandContext context( project ); + PluginActions::DoEffect( + EffectManager::Get().GetEffectByIdentifier(wxT("Normalize")), + context, + PluginActions::kConfigured); + } + + // This is a no-fail: + dirManager.FillBlockfilesCache(); + return true; +} diff --git a/src/ProjectFileManager.h b/src/ProjectFileManager.h index 694bf9e51..2109c45ee 100644 --- a/src/ProjectFileManager.h +++ b/src/ProjectFileManager.h @@ -12,13 +12,23 @@ Paul Licameli split from AudacityProject.h #define __AUDACITY_PROJECT_FILE_MANAGER__ #include +#include #include "ClientData.h" // to inherit +#include "import/ImportRaw.h" // defines TrackHolders class wxString; class wxFileName; class AudacityProject; +class ImportXMLTagHandler; +class RecordingRecoveryHandler; +class Track; class TrackList; +class WaveTrack; +class XMLTagHandler; +namespace ProjectFileIORegistry{ struct Entry; } + +using WaveTrackArray = std::vector < std::shared_ptr < WaveTrack > >; class ProjectFileManager final : public ClientData::Base @@ -54,20 +64,79 @@ public: void Reset(); - void SetImportedDependencies( bool value ) { mImportedDependencies = value; } + /** @brief Show an open dialogue for opening audio files, and possibly other + * sorts of files. + * + * The file type filter will automatically contain: + * - "All files" with any extension or none, + * - "All supported files" based on the file formats supported in this + * build of Audacity, + * - All of the individual formats specified by the importer plug-ins which + * are built into this build of Audacity, each with the relevant file + * extensions for that format. + * The dialogue will start in the DefaultOpenPath directory read from the + * preferences, failing that the working directory. The file format filter + * will be set to the DefaultOpenType from the preferences, failing that + * the first format specified in the dialogue. These two parameters will + * be saved to the preferences once the user has chosen a file to open. + * @param extraformat Specify the name of an additional format to allow + * opening in this dialogue. This string is free-form, but should be short + * enough to fit in the file dialogue filter drop-down. It should be + * translated. + * @param extrafilter Specify the file extension(s) for the additional format + * specified by extraformat. The patterns must include the wildcard (e.g. + * "*.aup" not "aup" or ".aup"), separate multiple patters with a semicolon, + * e.g. "*.aup;*.AUP" because patterns are case-sensitive. Do not add a + * trailing semicolon to the string. This string should not be translated + * @return Array of file paths which the user selected to open (multiple + * selections allowed). + */ + static wxArrayString ShowOpenDialog(const wxString &extraformat = {}, + const wxString &extrafilter = {}); -private: + static bool IsAlreadyOpen(const FilePath &projPathName); + + void OpenFile(const FilePath &fileName, bool addtohistory = true); + + // If pNewTrackList is passed in non-NULL, it gets filled with the pointers to NEW tracks. + bool Import(const FilePath &fileName, WaveTrackArray *pTrackArray = NULL); + + // Takes array of unique pointers; returns array of shared + std::vector< std::shared_ptr > + AddImportedTracks(const FilePath &fileName, + TrackHolders &&newTracks); + + bool GetMenuClose() const { return mMenuClose; } + void SetMenuClose(bool value) { mMenuClose = value; } + +private: + void SetImportedDependencies( bool value ) { mImportedDependencies = value; } + // Push names of NEW export files onto the path list bool SaveCopyWaveTracks(const FilePath & strProjectPathName, bool bLossless, FilePaths &strOtherNamesArray); bool DoSave(bool fromSaveAs, bool bWantSaveCopy, bool bLossless = false); + // Declared in this class so that they can have access to private members + static XMLTagHandler *RecordingRecoveryFactory( AudacityProject &project ); + static ProjectFileIORegistry::Entry sRecoveryFactory; + static XMLTagHandler *ImportHandlerFactory( AudacityProject &project ); + static ProjectFileIORegistry::Entry sImportHandlerFactory; + AudacityProject &mProject; std::shared_ptr mLastSavedTracks; + // The handler that handles recovery of tags + std::unique_ptr mRecordingRecoveryHandler; + + std::unique_ptr mImportXMLTagHandler; + // Dependencies have been imported and a warning should be shown on save bool mImportedDependencies{ false }; + + // Are we currently closing as the result of a menu command? + bool mMenuClose{ false }; }; #endif diff --git a/src/ProjectManager.cpp b/src/ProjectManager.cpp index ff84ddb79..c81cb243f 100644 --- a/src/ProjectManager.cpp +++ b/src/ProjectManager.cpp @@ -19,32 +19,22 @@ Paul Licameli split from AudacityProject.cpp #include "Clipboard.h" #include "DirManager.h" #include "FileNames.h" -#include "Legacy.h" #include "Menus.h" #include "MissingAliasFileDialog.h" #include "ModuleManager.h" -#include "PlatformCompatibility.h" #include "Project.h" #include "ProjectAudioIO.h" #include "ProjectAudioManager.h" #include "ProjectFileIO.h" -#include "ProjectFileIORegistry.h" #include "ProjectFileManager.h" -#include "ProjectFSCK.h" #include "ProjectHistory.h" #include "ProjectSelectionManager.h" #include "ProjectSettings.h" #include "ProjectWindow.h" -#include "Sequence.h" -#include "Tags.h" #include "TrackPanel.h" #include "UndoManager.h" #include "WaveTrack.h" -#include "WaveClip.h" #include "wxFileNameWrapper.h" -#include "commands/CommandContext.h" -#include "effects/EffectManager.h" -#include "import/Import.h" #include "ondemand/ODManager.h" #include "prefs/QualityPrefs.h" #include "toolbars/ControlToolBar.h" @@ -58,7 +48,6 @@ Paul Licameli split from AudacityProject.cpp #include #include -#include const int AudacityProjectTimerID = 5200; @@ -338,7 +327,7 @@ public: FileActions::DoImportMIDI(mProject, name); else #endif - ProjectManager::Get( *mProject ).Import(name); + ProjectFileManager::Get( *mProject ).Import(name); } auto &window = ProjectWindow::Get( *mProject ); @@ -539,7 +528,7 @@ void ProjectManager::OnCloseWindow(wxCloseEvent & event) #ifdef __WXMAC__ quitOnClose = false; #else - quitOnClose = !mMenuClose; + quitOnClose = !projectFileManager.GetMenuClose(); #endif // DanH: If we're definitely about to quit, clear the clipboard. @@ -645,136 +634,11 @@ void ProjectManager::OnOpenAudioFile(wxCommandEvent & event) const wxString &cmd = event.GetString(); if (!cmd.empty()) - OpenFile(cmd); + ProjectFileManager::Get( mProject ).OpenFile(cmd); window.RequestUserAttention(); } -// static method, can be called outside of a project -wxArrayString ProjectManager::ShowOpenDialog(const wxString &extraformat, const wxString &extrafilter) -{ - FormatList l; - wxString filter; ///< List of file format names and extensions, separated - /// by | characters between _formats_ and extensions for each _format_, i.e. - /// format1name | *.ext | format2name | *.ex1;*.ex2 - wxString all; ///< One long list of all supported file extensions, - /// semicolon separated - - if (!extraformat.empty()) - { // additional format specified - all = extrafilter + wxT(';'); - // add it to the "all supported files" filter string - } - - // Construct the filter - Importer::Get().GetSupportedImportFormats(&l); - - for (const auto &format : l) { - /* this loop runs once per supported _format_ */ - const Format *f = &format; - - wxString newfilter = f->formatName + wxT("|"); - // bung format name into string plus | separator - for (size_t i = 0; i < f->formatExtensions.size(); i++) { - /* this loop runs once per valid _file extension_ for file containing - * the current _format_ */ - if (!newfilter.Contains(wxT("*.") + f->formatExtensions[i] + wxT(";"))) - newfilter += wxT("*.") + f->formatExtensions[i] + wxT(";"); - if (!all.Contains(wxT("*.") + f->formatExtensions[i] + wxT(";"))) - all += wxT("*.") + f->formatExtensions[i] + wxT(";"); - } - newfilter.RemoveLast(1); - filter += newfilter; - filter += wxT("|"); - } - all.RemoveLast(1); - filter.RemoveLast(1); - - // For testing long filters -#if 0 - wxString test = wxT("*.aaa;*.bbb;*.ccc;*.ddd;*.eee"); - all = test + wxT(';') + test + wxT(';') + test + wxT(';') + - test + wxT(';') + test + wxT(';') + test + wxT(';') + - test + wxT(';') + test + wxT(';') + test + wxT(';') + - all; -#endif - - /* i18n-hint: The vertical bars and * are essential here.*/ - wxString mask = _("All files|*|All supported files|") + - all + wxT("|"); // "all" and "all supported" entries - if (!extraformat.empty()) - { // append caller-defined format if supplied - mask += extraformat + wxT("|") + extrafilter + wxT("|"); - } - mask += filter; // put the names and extensions of all the importer formats - // we built up earlier into the mask - - // Retrieve saved path and type - auto path = FileNames::FindDefaultPath(FileNames::Operation::Open); - wxString type = gPrefs->Read(wxT("/DefaultOpenType"),mask.BeforeFirst(wxT('|'))); - - // Convert the type to the filter index - int index = mask.First(type + wxT("|")); - if (index == wxNOT_FOUND) { - index = 0; - } - else { - index = mask.Left(index).Freq(wxT('|')) / 2; - if (index < 0) { - index = 0; - } - } - - // Construct and display the file dialog - wxArrayString selected; - - FileDialogWrapper dlog(NULL, - _("Select one or more files"), - path, - wxT(""), - mask, - wxFD_OPEN | wxFD_MULTIPLE | wxRESIZE_BORDER); - - dlog.SetFilterIndex(index); - - int dialogResult = dlog.ShowModal(); - - // Convert the filter index to type and save - index = dlog.GetFilterIndex(); - for (int i = 0; i < index; i++) { - mask = mask.AfterFirst(wxT('|')).AfterFirst(wxT('|')); - } - gPrefs->Write(wxT("/DefaultOpenType"), mask.BeforeFirst(wxT('|'))); - gPrefs->Write(wxT("/LastOpenType"), mask.BeforeFirst(wxT('|'))); - gPrefs->Flush(); - - if (dialogResult == wxID_OK) { - // Return the selected files - dlog.GetPaths(selected); - } - return selected; -} - -// static method, can be called outside of a project -bool ProjectManager::IsAlreadyOpen(const FilePath &projPathName) -{ - const wxFileName newProjPathName(projPathName); - auto start = AllProjects{}.begin(), finish = AllProjects{}.end(), - iter = std::find_if( start, finish, - [&]( const AllProjects::value_type &ptr ){ - return newProjPathName.SameAs(wxFileNameWrapper{ ptr->GetFileName() }); - } ); - if (iter != finish) { - wxString errMsg = - wxString::Format(_("%s is already open in another window."), - newProjPathName.GetName()); - wxLogError(errMsg); - AudacityMessageBox(errMsg, _("Error Opening Project"), wxOK | wxCENTRE); - return true; - } - return false; -} - // static method, can be called outside of a project void ProjectManager::OpenFiles(AudacityProject *proj) { @@ -783,7 +647,7 @@ void ProjectManager::OpenFiles(AudacityProject *proj) * with Audacity. Do not include pipe symbols or .aup (this extension will * now be added automatically for the Save Projects dialogues).*/ auto selectedFiles = - ProjectManager::ShowOpenDialog(_("Audacity projects"), wxT("*.aup")); + ProjectFileManager::ShowOpenDialog(_("Audacity projects"), wxT("*.aup")); if (selectedFiles.size() == 0) { gPrefs->Write(wxT("/LastOpenType"),wxT("")); gPrefs->Flush(); @@ -805,7 +669,7 @@ void ProjectManager::OpenFiles(AudacityProject *proj) const wxString &fileName = selectedFiles[ff]; // Make sure it isn't already open. - if (IsAlreadyOpen(fileName)) + if (ProjectFileManager::IsAlreadyOpen(fileName)) continue; // Skip ones that are already open. FileNames::UpdateDefaultPath(FileNames::Operation::Open, fileName); @@ -843,7 +707,7 @@ AudacityProject *ProjectManager::OpenProject( if( pNewProject ) GetProjectFrame( *pNewProject ).Close(true); } ); - Get( *pProject ).OpenFile( fileNameArg, addtohistory ); + ProjectFileManager::Get( *pProject ).OpenFile( fileNameArg, addtohistory ); pNewProject = nullptr; auto &projectFileIO = ProjectFileIO::Get( *pProject ); if( projectFileIO.IsRecovered() ) @@ -853,648 +717,6 @@ AudacityProject *ProjectManager::OpenProject( return pProject; } -XMLTagHandler * -ProjectManager::RecordingRecoveryFactory( AudacityProject &project ) { - auto &projectManager = Get( project ); - auto &ptr = projectManager.mRecordingRecoveryHandler; - if (!ptr) - ptr = - std::make_unique( &project ); - return ptr.get(); -} - -ProjectFileIORegistry::Entry -ProjectManager::sRecoveryFactory{ - wxT("recordingrecovery"), RecordingRecoveryFactory -}; - -// XML handler for tag -class ImportXMLTagHandler final : public XMLTagHandler -{ - public: - ImportXMLTagHandler(AudacityProject* pProject) { mProject = pProject; } - - bool HandleXMLTag(const wxChar *tag, const wxChar **attrs) override; - XMLTagHandler *HandleXMLChild(const wxChar * WXUNUSED(tag)) override - { return NULL; } - - // Don't want a WriteXML method because ImportXMLTagHandler is not a WaveTrack. - // tags are instead written by AudacityProject::WriteXML. - // void WriteXML(XMLWriter &xmlFile) /* not override */ { wxASSERT(false); } - - private: - AudacityProject* mProject; -}; - -bool ImportXMLTagHandler::HandleXMLTag(const wxChar *tag, const wxChar **attrs) -{ - if (wxStrcmp(tag, wxT("import")) || attrs==NULL || (*attrs)==NULL || wxStrcmp(*attrs++, wxT("filename"))) - return false; - wxString strAttr = *attrs; - if (!XMLValueChecker::IsGoodPathName(strAttr)) - { - // Maybe strAttr is just a fileName, not the full path. Try the project data directory. - wxFileNameWrapper fileName{ - DirManager::Get( *mProject ).GetProjectDataDir(), strAttr }; - if (XMLValueChecker::IsGoodFileName(strAttr, fileName.GetPath(wxPATH_GET_VOLUME))) - strAttr = fileName.GetFullPath(); - else - { - wxLogWarning(wxT("Could not import file: %s"), strAttr); - return false; - } - } - - WaveTrackArray trackArray; - - // Guard this call so that C++ exceptions don't propagate through - // the expat library - GuardedCall( - [&] { - ProjectManager::Get( *mProject ).Import(strAttr, &trackArray); }, - [&] (AudacityException*) { trackArray.clear(); } - ); - - if (trackArray.empty()) - return false; - - // Handle other attributes, now that we have the tracks. - attrs++; - const wxChar** pAttr; - bool bSuccess = true; - - for (size_t i = 0; i < trackArray.size(); i++) - { - // Most of the "import" tag attributes are the same as for "wavetrack" tags, - // so apply them via WaveTrack::HandleXMLTag(). - bSuccess = trackArray[i]->HandleXMLTag(wxT("wavetrack"), attrs); - - // "offset" tag is ignored in WaveTrack::HandleXMLTag except for legacy projects, - // so handle it here. - double dblValue; - pAttr = attrs; - while (*pAttr) - { - const wxChar *attr = *pAttr++; - const wxChar *value = *pAttr++; - const wxString strValue = value; - if (!wxStrcmp(attr, wxT("offset")) && - XMLValueChecker::IsGoodString(strValue) && - Internat::CompatibleToDouble(strValue, &dblValue)) - trackArray[i]->SetOffset(dblValue); - } - } - return bSuccess; -}; - -XMLTagHandler * -ProjectManager::ImportHandlerFactory( AudacityProject &project ) { - auto &projectManager = Get( project ); - auto &ptr = projectManager.mImportXMLTagHandler; - if (!ptr) - ptr = - std::make_unique( &project ); - return ptr.get(); -} - -ProjectFileIORegistry::Entry -ProjectManager::sImportHandlerFactory{ - wxT("import"), ImportHandlerFactory -}; - -// FIXME:? TRAP_ERR This should return a result that is checked. -// See comment in AudacityApp::MRUOpen(). -void ProjectManager::OpenFile(const FilePath &fileNameArg, bool addtohistory) -{ - auto &project = mProject; - auto &history = ProjectHistory::Get( project ); - auto &projectFileIO = ProjectFileIO::Get( project ); - auto &projectFileManager = ProjectFileManager::Get( project ); - auto &tracks = TrackList::Get( project ); - auto &trackPanel = TrackPanel::Get( project ); - auto &dirManager = DirManager::Get( project ); - auto &window = ProjectWindow::Get( project ); - - // On Win32, we may be given a short (DOS-compatible) file name on rare - // occassions (e.g. stuff like "C:\PROGRA~1\AUDACI~1\PROJEC~1.AUP"). We - // convert these to long file name first. - auto fileName = PlatformCompatibility::ConvertSlashInFileName( - PlatformCompatibility::GetLongFileName(fileNameArg)); - - // Make sure it isn't already open. - // Vaughan, 2011-03-25: This was done previously in AudacityProject::OpenFiles() - // and AudacityApp::MRUOpen(), but if you open an aup file by double-clicking it - // from, e.g., Win Explorer, it would bypass those, get to here with no check, - // then open a NEW project from the same data with no warning. - // This was reported in http://bugzilla.audacityteam.org/show_bug.cgi?id=137#c17, - // but is not really part of that bug. Anyway, prevent it! - if (IsAlreadyOpen(fileName)) - return; - - - // Data loss may occur if users mistakenly try to open ".aup.bak" files - // left over from an unsuccessful save or by previous versions of Audacity. - // So we always refuse to open such files. - if (fileName.Lower().EndsWith(wxT(".aup.bak"))) - { - AudacityMessageBox( - _("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."), - _("Warning - Backup File Detected"), - wxOK | wxCENTRE, &window); - return; - } - - if (!::wxFileExists(fileName)) { - AudacityMessageBox( - wxString::Format( _("Could not open file: %s"), fileName ), - ("Error Opening File"), - wxOK | wxCENTRE, &window); - return; - } - - // We want to open projects using wxTextFile, but if it's NOT a project - // file (but actually a WAV file, for example), then wxTextFile will spin - // for a long time searching for line breaks. So, we look for our - // signature at the beginning of the file first: - - char buf[16]; - { - wxFFile ff(fileName, wxT("rb")); - if (!ff.IsOpened()) { - AudacityMessageBox( - wxString::Format( _("Could not open file: %s"), fileName ), - _("Error opening file"), - wxOK | wxCENTRE, &window); - return; - } - int numRead = ff.Read(buf, 15); - if (numRead != 15) { - AudacityMessageBox(wxString::Format(_("File may be invalid or corrupted: \n%s"), - fileName), _("Error Opening File or Project"), - wxOK | wxCENTRE, &window); - ff.Close(); - return; - } - buf[15] = 0; - } - - wxString temp = LAT1CTOWX(buf); - - if (temp == wxT("AudacityProject")) { - // It's an Audacity 1.0 (or earlier) project file. - // If they bail out, return and do no more. - if( !projectFileIO.WarnOfLegacyFile() ) - return; - // Convert to the NEW format. - bool success = ConvertLegacyProjectFile(wxFileName{ fileName }); - if (!success) { - AudacityMessageBox(_("Audacity was unable to convert an Audacity 1.0 project to the new project format."), - _("Error Opening Project"), - wxOK | wxCENTRE, &window); - return; - } - else { - temp = wxT("Clear(true); - mFileName = wxT(""); - SetProjectTitle(); - mTrackPanel->Refresh(true); - */ - closed = true; - SetMenuClose(true); - window.Close(); - return; - } - else if (status & FSCKstatus_CHANGED) - { - // Mark the wave tracks as changed and redraw. - for ( auto wt : tracks.Any() ) - // Only wave tracks have a notion of "changed". - for (const auto &clip: wt->GetClips()) - clip->MarkChanged(); - - trackPanel.Refresh(true); - - // Vaughan, 2010-08-20: This was bogus, as all the actions in ProjectFSCK - // that return FSCKstatus_CHANGED cannot be undone. - // this->PushState(_("Project checker repaired file"), _("Project Repair")); - - if (status & FSCKstatus_SAVE_AUP) - projectFileManager.Save(), saved = true; - } - } - - if (mImportXMLTagHandler) { - if (!saved) - // We processed an tag, so save it as a normal project, - // with no tags. - projectFileManager.Save(); - } - } - else { - // Vaughan, 2011-10-30: - // See first topic at http://bugzilla.audacityteam.org/show_bug.cgi?id=451#c16. - // Calling mTracks->Clear() with deleteTracks true results in data loss. - - // PRL 2014-12-19: - // I made many changes for wave track memory management, but only now - // read the above comment. I may have invalidated the fix above (which - // may have spared the files at the expense of leaked memory). But - // here is a better way to accomplish the intent, doing like what happens - // when the project closes: - for ( auto pTrack : tracks.Any< WaveTrack >() ) - pTrack->CloseLock(); - - tracks.Clear(); //tracks.Clear(true); - - project.SetFileName( wxT("") ); - projectFileIO.SetProjectTitle(); - - wxLogError(wxT("Could not parse file \"%s\". \nError: %s"), fileName, errorStr); - - wxString url = wxT("FAQ:Errors_on_opening_or_recovering_an_Audacity_project"); - - // Certain errors have dedicated help. - // On April-4th-2018, we did not request translation of the XML errors. - // If/when we do, we will need _() around the comparison strings. - if( errorStr.Contains( ("not well-formed (invalid token)") ) ) - url = "Error:_not_well-formed_(invalid_token)_at_line_x"; - else if( errorStr.Contains(("reference to invalid character number") )) - url = "Error_Opening_Project:_Reference_to_invalid_character_number_at_line_x"; - else if( errorStr.Contains(("mismatched tag") )) - url += "#mismatched"; - -// These two errors with FAQ entries are reported elsewhere, not here.... -//#[[#import-error|Error Importing: Aup is an Audacity Project file. Use the File > Open command]] -//#[[#corrupt|Error Opening File or Project: File may be invalid or corrupted]] - -// If we did want to handle every single parse error, these are they.... -/* - XML_L("out of memory"), - XML_L("syntax error"), - XML_L("no element found"), - XML_L("not well-formed (invalid token)"), - XML_L("unclosed token"), - XML_L("partial character"), - XML_L("mismatched tag"), - XML_L("duplicate attribute"), - XML_L("junk after document element"), - XML_L("illegal parameter entity reference"), - XML_L("undefined entity"), - XML_L("recursive entity reference"), - XML_L("asynchronous entity"), - XML_L("reference to invalid character number"), - XML_L("reference to binary entity"), - XML_L("reference to external entity in attribute"), - XML_L("XML or text declaration not at start of entity"), - XML_L("unknown encoding"), - XML_L("encoding specified in XML declaration is incorrect"), - XML_L("unclosed CDATA section"), - XML_L("error in processing external entity reference"), - XML_L("document is not standalone"), - XML_L("unexpected parser state - please send a bug report"), - XML_L("entity declared in parameter entity"), - XML_L("requested feature requires XML_DTD support in Expat"), - XML_L("cannot change setting once parsing has begun"), - XML_L("unbound prefix"), - XML_L("must not undeclare prefix"), - XML_L("incomplete markup in parameter entity"), - XML_L("XML declaration not well-formed"), - XML_L("text declaration not well-formed"), - XML_L("illegal character(s) in public id"), - XML_L("parser suspended"), - XML_L("parser not suspended"), - XML_L("parsing aborted"), - XML_L("parsing finished"), - XML_L("cannot suspend in external parameter entity"), - XML_L("reserved prefix (xml) must not be undeclared or bound to another namespace name"), - XML_L("reserved prefix (xmlns) must not be declared or undeclared"), - XML_L("prefix must not be bound to one of the reserved namespace names") -*/ - - ShowErrorDialog( - &window, - _("Error Opening Project"), - errorStr, - url); - } -} - -std::vector< std::shared_ptr< Track > > -ProjectManager::AddImportedTracks(const FilePath &fileName, - TrackHolders &&newTracks) -{ - auto &project = mProject; - auto &history = ProjectHistory::Get( project ); - auto &projectFileIO = ProjectFileIO::Get( project ); - auto &projectFileManager = ProjectFileManager::Get( project ); - auto &tracks = TrackList::Get( project ); - - std::vector< std::shared_ptr< Track > > results; - - SelectActions::SelectNone( project ); - - bool initiallyEmpty = tracks.empty(); - double newRate = 0; - wxString trackNameBase = fileName.AfterLast(wxFILE_SEP_PATH).BeforeLast('.'); - int i = -1; - - // Must add all tracks first (before using Track::IsLeader) - for (auto &group : newTracks) { - if (group.empty()) { - wxASSERT(false); - continue; - } - auto first = group.begin()->get(); - auto nChannels = group.size(); - for (auto &uNewTrack : group) { - auto newTrack = tracks.Add( uNewTrack ); - results.push_back(newTrack->SharedPointer()); - } - tracks.GroupChannels(*first, nChannels); - } - newTracks.clear(); - - // Now name them - - // Add numbers to track names only if there is more than one (mono or stereo) - // track (not necessarily, more than one channel) - const bool useSuffix = - make_iterator_range( results.begin() + 1, results.end() ) - .any_of( []( decltype(*results.begin()) &pTrack ) - { return pTrack->IsLeader(); } ); - - for (const auto &newTrack : results) { - if ( newTrack->IsLeader() ) - // Count groups only - ++i; - - newTrack->SetSelected(true); - - if ( useSuffix ) - newTrack->SetName(trackNameBase + wxString::Format(wxT(" %d" ), i + 1)); - else - newTrack->SetName(trackNameBase); - - newTrack->TypeSwitch( [&](WaveTrack *wt) { - if (newRate == 0) - newRate = wt->GetRate(); - - // Check if NEW track contains aliased blockfiles and if yes, - // remember this to show a warning later - if(WaveClip* clip = wt->GetClipByIndex(0)) { - BlockArray &blocks = clip->GetSequence()->GetBlockArray(); - if (blocks.size()) - { - SeqBlock& block = blocks[0]; - if (block.f->IsAlias()) - projectFileManager.SetImportedDependencies( true ); - } - } - }); - } - - // Automatically assign rate of imported file to whole project, - // if this is the first file that is imported - if (initiallyEmpty && newRate > 0) { - auto &settings = ProjectSettings::Get( project ); - settings.SetRate( newRate ); - SelectionBar::Get( project ).SetRate( newRate ); - } - - history.PushState(wxString::Format(_("Imported '%s'"), fileName), - _("Import")); - -#if defined(__WXGTK__) - // See bug #1224 - // The track panel hasn't we been fully created, so the DoZoomFit() will not give - // expected results due to a window width of zero. Should be safe to yield here to - // allow the creattion to complete. If this becomes a problem, it "might" be possible - // to queue a dummy event to trigger the DoZoomFit(). - wxEventLoopBase::GetActive()->YieldFor(wxEVT_CATEGORY_UI | wxEVT_CATEGORY_USER_INPUT); -#endif - - if (initiallyEmpty && !projectFileIO.IsProjectSaved() ) { - wxString name = fileName.AfterLast(wxFILE_SEP_PATH).BeforeLast(wxT('.')); - project.SetFileName( - ::wxPathOnly(fileName) + wxFILE_SEP_PATH + name + wxT(".aup") ); - projectFileIO.SetLoadedFromAup( false ); - projectFileIO.SetProjectTitle(); - } - - // Moved this call to higher levels to prevent flicker redrawing everything on each file. - // HandleResize(); - - return results; -} - -// If pNewTrackList is passed in non-NULL, it gets filled with the pointers to NEW tracks. -bool ProjectManager::Import( - const FilePath &fileName, WaveTrackArray* pTrackArray /*= NULL*/) -{ - auto &project = mProject; - auto &dirManager = DirManager::Get( project ); - auto oldTags = Tags::Get( project ).shared_from_this(); - TrackHolders newTracks; - wxString errorMessage; - - { - // Backup Tags, before the import. Be prepared to roll back changes. - bool committed = false; - auto cleanup = finally([&]{ - if ( !committed ) - Tags::Set( project, oldTags ); - }); - auto newTags = oldTags->Duplicate(); - Tags::Set( project, newTags ); - - bool success = Importer::Get().Import(fileName, - &TrackFactory::Get( project ), - newTracks, - newTags.get(), - errorMessage); - - if (!errorMessage.empty()) { - // Error message derived from Importer::Import - // Additional help via a Help button links to the manual. - ShowErrorDialog(&GetProjectFrame( project ), _("Error Importing"), - errorMessage, wxT("Importing_Audio")); - } - if (!success) - return false; - - FileHistory::Global().AddFileToHistory(fileName); - - // no more errors, commit - committed = true; - } - - // for LOF ("list of files") files, do not import the file as if it - // were an audio file itself - if (fileName.AfterLast('.').IsSameAs(wxT("lof"), false)) { - // PRL: don't redundantly do the steps below, because we already - // did it in case of LOF, because of some weird recursion back to this - // same function. I think this should be untangled. - - // So Undo history push is not bypassed, despite appearances. - return false; - } - - // PRL: Undo history is incremented inside this: - auto newSharedTracks = AddImportedTracks(fileName, std::move(newTracks)); - - if (pTrackArray) { - for (const auto &newTrack : newSharedTracks) { - newTrack->TypeSwitch( [&](WaveTrack *wt) { - pTrackArray->push_back( wt->SharedPointer< WaveTrack >() ); - }); - } - } - - int mode = gPrefs->Read(wxT("/AudioFiles/NormalizeOnLoad"), 0L); - if (mode == 1) { - //TODO: All we want is a SelectAll() - SelectActions::SelectNone( project ); - SelectActions::SelectAllIfNone( project ); - const CommandContext context( project ); - PluginActions::DoEffect( - EffectManager::Get().GetEffectByIdentifier(wxT("Normalize")), - context, - PluginActions::kConfigured); - } - - // This is a no-fail: - dirManager.FillBlockfilesCache(); - return true; -} - // This is done to empty out the tracks, but without creating a new project. void ProjectManager::ResetProjectToEmpty() { auto &project = mProject; diff --git a/src/ProjectManager.h b/src/ProjectManager.h index 2cc4dab97..fb7948a9f 100644 --- a/src/ProjectManager.h +++ b/src/ProjectManager.h @@ -12,25 +12,15 @@ Paul Licameli split from AudacityProject.h #define __AUDACITY_PROJECT_MANAGER__ #include -#include #include // to inherit #include "ClientData.h" // to inherit -#include "import/ImportRaw.h" // defines TrackHolders class wxTimer; class wxTimerEvent; class AudacityProject; struct AudioIOStartStreamOptions; -class ImportXMLTagHandler; -class RecordingRecoveryHandler; -class Track; -class WaveTrack; -class XMLTagHandler; -namespace ProjectFileIORegistry{ struct Entry; } - -using WaveTrackArray = std::vector < std::shared_ptr < WaveTrack > >; ///\brief Object associated with a project for high-level management of the /// project's lifetime, including creation, destruction, opening from file, @@ -49,45 +39,9 @@ public: // This is the factory for projects: static AudacityProject *New(); - // File I/O - - /** @brief Show an open dialogue for opening audio files, and possibly other - * sorts of files. - * - * The file type filter will automatically contain: - * - "All files" with any extension or none, - * - "All supported files" based on the file formats supported in this - * build of Audacity, - * - All of the individual formats specified by the importer plug-ins which - * are built into this build of Audacity, each with the relevant file - * extensions for that format. - * The dialogue will start in the DefaultOpenPath directory read from the - * preferences, failing that the working directory. The file format filter - * will be set to the DefaultOpenType from the preferences, failing that - * the first format specified in the dialogue. These two parameters will - * be saved to the preferences once the user has chosen a file to open. - * @param extraformat Specify the name of an additional format to allow - * opening in this dialogue. This string is free-form, but should be short - * enough to fit in the file dialogue filter drop-down. It should be - * translated. - * @param extrafilter Specify the file extension(s) for the additional format - * specified by extraformat. The patterns must include the wildcard (e.g. - * "*.aup" not "aup" or ".aup"), separate multiple patters with a semicolon, - * e.g. "*.aup;*.AUP" because patterns are case-sensitive. Do not add a - * trailing semicolon to the string. This string should not be translated - * @return Array of file paths which the user selected to open (multiple - * selections allowed). - */ - static wxArrayString ShowOpenDialog(const wxString &extraformat = {}, - const wxString &extrafilter = {}); - - static bool IsAlreadyOpen(const FilePath &projPathName); - - // The functions that open and import files can act as factories too, and - // they set projects to initial state. + // The function that imports files can act as a factory too, and for that + // reason remains in this class, not in ProjectFileManager static void OpenFiles(AudacityProject *proj); - - void OpenFile(const FilePath &fileName, bool addtohistory = true); // Return the given project if that is not NULL, else create a project. // Then open the given project path. @@ -96,14 +50,6 @@ public: AudacityProject *pProject, const FilePath &fileNameArg, bool addtohistory = true); - // If pNewTrackList is passed in non-NULL, it gets filled with the pointers to NEW tracks. - bool Import(const FilePath &fileName, WaveTrackArray *pTrackArray = NULL); - - // Takes array of unique pointers; returns array of shared - std::vector< std::shared_ptr > - AddImportedTracks(const FilePath &fileName, - TrackHolders &&newTracks); - void ResetProjectToEmpty(); static void SaveWindowSize(); @@ -113,8 +59,6 @@ public: // Converts number of minutes to human readable format wxString GetHoursMinsString(int iMinutes); - void SetMenuClose(bool value) { mMenuClose = value; } - private: void OnCloseWindow(wxCloseEvent & event); void OnTimer(wxTimerEvent & event); @@ -123,28 +67,14 @@ private: void RestartTimer(); - // Declared in this class so that they can have access to private members - static XMLTagHandler *RecordingRecoveryFactory( AudacityProject &project ); - static ProjectFileIORegistry::Entry sRecoveryFactory; - static XMLTagHandler *ImportHandlerFactory( AudacityProject &project ); - static ProjectFileIORegistry::Entry sImportHandlerFactory; - // non-static data members AudacityProject &mProject; - // The handler that handles recovery of tags - std::unique_ptr mRecordingRecoveryHandler; - - std::unique_ptr mImportXMLTagHandler; - std::unique_ptr mTimer; // See explanation in OnCloseWindow bool mIsBeingDeleted{ false }; - // Are we currently closing as the result of a menu command? - bool mMenuClose{ false }; - DECLARE_EVENT_TABLE() static bool sbWindowRectAlreadySaved; diff --git a/src/commands/ImportExportCommands.cpp b/src/commands/ImportExportCommands.cpp index bbb455567..3ba5d09cc 100644 --- a/src/commands/ImportExportCommands.cpp +++ b/src/commands/ImportExportCommands.cpp @@ -17,7 +17,7 @@ #include "../Audacity.h" #include "ImportExportCommands.h" -#include "../ProjectManager.h" +#include "../ProjectFileManager.h" #include "../ViewInfo.h" #include "../export/Export.h" #include "../Shuttle.h" @@ -41,7 +41,7 @@ void ImportCommand::PopulateOrExchange(ShuttleGui & S) } bool ImportCommand::Apply(const CommandContext & context){ - return ProjectManager::Get( context.project ).Import(mFileName); + return ProjectFileManager::Get( context.project ).Import(mFileName); } diff --git a/src/commands/OpenSaveCommands.cpp b/src/commands/OpenSaveCommands.cpp index b919451a1..309411981 100644 --- a/src/commands/OpenSaveCommands.cpp +++ b/src/commands/OpenSaveCommands.cpp @@ -54,7 +54,7 @@ bool OpenProjectCommand::Apply(const CommandContext & context){ } else { - ProjectManager::Get( context.project ) + ProjectFileManager::Get( context.project ) .OpenFile(mFileName, mbAddToHistory); } const auto &newFileName = context.project.GetFileName(); diff --git a/src/menus/FileMenus.cpp b/src/menus/FileMenus.cpp index cd3123b5a..87a21699c 100644 --- a/src/menus/FileMenus.cpp +++ b/src/menus/FileMenus.cpp @@ -180,7 +180,7 @@ void OnClose(const CommandContext &context ) { auto &project = context.project; auto &window = ProjectWindow::Get( project ); - ProjectManager::Get( project ).SetMenuClose(true); + ProjectFileManager::Get( project ).SetMenuClose(true); window.Close(); } @@ -414,7 +414,7 @@ void OnImport(const CommandContext &context) // this serves to track the file if the users zooms in and such. MissingAliasFilesDialog::SetShouldShow(true); - wxArrayString selectedFiles = ProjectManager::ShowOpenDialog(wxT("")); + wxArrayString selectedFiles = ProjectFileManager::ShowOpenDialog(wxT("")); if (selectedFiles.size() == 0) { gPrefs->Write(wxT("/LastOpenType"),wxT("")); gPrefs->Flush(); @@ -444,7 +444,7 @@ void OnImport(const CommandContext &context) FileNames::UpdateDefaultPath(FileNames::Operation::Open, fileName); - ProjectManager::Get( project ).Import(fileName); + ProjectFileManager::Get( project ).Import(fileName); } window.ZoomAfterImport(nullptr); @@ -541,7 +541,8 @@ void OnImportRaw(const CommandContext &context) if (newTracks.size() <= 0) return; - ProjectManager::Get( project ).AddImportedTracks(fileName, std::move(newTracks)); + ProjectFileManager::Get( project ) + .AddImportedTracks(fileName, std::move(newTracks)); window.HandleResize(); // Adjust scrollers for NEW track sizes. }