audacia/src/ProjectFileIO.cpp
Paul Licameli 98f322d685 Move writing and reading of common Track fields into functions...
... also now writing selected state of TimeTrack as for other tracks, fixing
an omission, with no harm to forward compatibility
2019-06-18 11:36:50 -04:00

660 lines
21 KiB
C++

/**********************************************************************
Audacity: A Digital Audio Editor
ProjectFileIO.cpp
Paul Licameli split from AudacityProject.cpp
**********************************************************************/
#include "ProjectFileIO.h"
#include <wx/frame.h>
#include "AutoRecovery.h"
#include "DirManager.h"
#include "FileNames.h"
#include "Project.h"
#include "ProjectFileIORegistry.h"
#include "ProjectSettings.h"
#include "Tags.h"
#include "ViewInfo.h"
#include "WaveTrack.h"
#include "widgets/AudacityMessageBox.h"
#include "widgets/NumericTextCtrl.h"
static void RefreshAllTitles(bool bShowProjectNumbers )
{
for ( auto pProject : AllProjects{} ) {
if ( !GetProjectFrame( *pProject ).IsIconized() ) {
ProjectFileIO::Get( *pProject ).SetProjectTitle(
bShowProjectNumbers ? pProject->GetProjectNumber() : -1 );
}
}
}
TitleRestorer::TitleRestorer(
wxTopLevelWindow &window, AudacityProject &project )
{
if( window.IsIconized() )
window.Restore();
window.Raise(); // May help identifying the window on Mac
// Construct this project's name and number.
sProjName = project.GetProjectName();
if ( sProjName.empty() ) {
sProjName = _("<untitled>");
UnnamedCount = std::count_if(
AllProjects{}.begin(), AllProjects{}.end(),
[]( const AllProjects::value_type &ptr ){
return ptr->GetProjectName().empty();
}
);
if ( UnnamedCount > 1 ) {
sProjNumber.Printf(
"[Project %02i] ", project.GetProjectNumber() + 1 );
RefreshAllTitles( true );
}
}
else
UnnamedCount = 0;
}
TitleRestorer::~TitleRestorer() {
if( UnnamedCount > 1 )
RefreshAllTitles( false );
}
static const AudacityProject::AttachedObjects::RegisteredFactory sFileIOKey{
[]( AudacityProject &parent ){
auto result = std::make_shared< ProjectFileIO >( parent );
return result;
}
};
ProjectFileIO &ProjectFileIO::Get( AudacityProject &project )
{
return project.AttachedObjects::Get< ProjectFileIO >( sFileIOKey );
}
const ProjectFileIO &ProjectFileIO::Get( const AudacityProject &project )
{
return Get( const_cast< AudacityProject & >( project ) );
}
// PRL: I preserve this handler function for an event that was never sent, but
// I don't know the intention.
ProjectFileIO::ProjectFileIO( AudacityProject &project )
: mProject{ project }
{
UpdatePrefs();
}
ProjectFileIO::~ProjectFileIO() = default;
void ProjectFileIO::UpdatePrefs()
{
SetProjectTitle();
}
// Pass a number in to show project number, or -1 not to.
void ProjectFileIO::SetProjectTitle( int number)
{
auto &project = mProject;
auto pWindow = project.GetFrame();
if ( !pWindow )
return;
auto &window = *pWindow;
wxString name = project.GetProjectName();
// If we are showing project numbers, then we also explicitly show "<untitled>" if there
// is none.
if( number >= 0 ){
/* i18n-hint: The %02i is the project number, the %s is the project name.*/
name = wxString::Format( _TS("[Project %02i] Audacity \"%s\""), number+1 ,
name.empty() ? "<untitled>" : (const char *)name );
}
// If we are not showing numbers, then <untitled> shows as 'Audacity'.
else if( name.empty() )
{
SetLoadedFromAup( false );
name = _TS("Audacity");
}
if ( IsRecovered() )
{
name += wxT(" ");
/* i18n-hint: E.g this is recovered audio that had been lost.*/
name += _("(Recovered)");
}
window.SetTitle( name );
window.SetName(name); // to make the nvda screen reader read the correct title
}
// Most of this string was duplicated 3 places. Made the warning consistent in this global.
// The %s is to be filled with the version string.
// PRL: Do not statically allocate a string in _() !
static wxString gsLegacyFileWarning() { return
_("This file was saved by Audacity version %s. The format has changed. \
\n\nAudacity can try to open and save this file, but saving it in this \
\nversion will then prevent any 1.2 or earlier version opening it. \
\n\nAudacity might corrupt the file in opening it, so you should \
back it up first. \
\n\nOpen this file now?");
}
bool ProjectFileIO::WarnOfLegacyFile( )
{
auto &project = mProject;
auto &window = GetProjectFrame( project );
wxString msg;
msg.Printf(gsLegacyFileWarning(), _("1.0 or earlier"));
// Stop icon, and choose 'NO' by default.
int action =
AudacityMessageBox(msg,
_("Warning - Opening Old Project File"),
wxYES_NO | wxICON_STOP | wxNO_DEFAULT | wxCENTRE,
&window);
return (action != wxNO);
}
bool ProjectFileIO::HandleXMLTag(const wxChar *tag, const wxChar **attrs)
{
auto &project = mProject;
auto &window = GetProjectFrame( project );
auto &viewInfo = ViewInfo::Get( project );
auto &dirManager = DirManager::Get( project );
auto &settings = ProjectSettings::Get( project );
bool bFileVersionFound = false;
wxString fileVersion = _("<unrecognized version -- possibly corrupt project file>");
wxString audacityVersion = _("<unrecognized version -- possibly corrupt project file>");
int requiredTags = 0;
long longVpos = 0;
// The auto-save data dir the project has been recovered from
FilePath recoveryAutoSaveDataDir;
// loop through attrs, which is a null-terminated list of
// attribute-value pairs
while(*attrs) {
const wxChar *attr = *attrs++;
const wxChar *value = *attrs++;
if (!value || !XMLValueChecker::IsGoodString(value))
break;
if (viewInfo.ReadXMLAttribute(attr, value)) {
// We need to save vpos now and restore it below
longVpos = std::max(longVpos, long(viewInfo.vpos));
continue;
}
if (!wxStrcmp(attr, wxT("datadir")))
{
//
// This is an auto-saved version whose data is in another directory
//
// Note: This attribute must currently be written and parsed before
// any other attributes
//
if ((value[0] != 0) && XMLValueChecker::IsGoodPathString(value))
{
// Remember that this is a recovered project
mIsRecovered = true;
recoveryAutoSaveDataDir = value;
}
}
else if (!wxStrcmp(attr, wxT("version")))
{
fileVersion = value;
bFileVersionFound = true;
requiredTags++;
}
else if (!wxStrcmp(attr, wxT("audacityversion"))) {
audacityVersion = value;
requiredTags++;
}
else if (!wxStrcmp(attr, wxT("projname"))) {
FilePath projName;
FilePath projPath;
if (mIsRecovered) {
// Fake the filename as if we had opened the original file
// (which was lost by the crash) rather than the one in the
// auto save folder
wxFileName realFileDir;
realFileDir.AssignDir(recoveryAutoSaveDataDir);
realFileDir.RemoveLastDir();
wxString realFileName = value;
if (realFileName.length() >= 5 &&
realFileName.Right(5) == wxT("_data"))
{
realFileName = realFileName.Left(realFileName.length() - 5);
}
if (realFileName.empty())
{
// A previously unsaved project has been recovered, so fake
// an unsaved project. The data files just stay in the temp
// directory
dirManager.SetLocalTempDir(recoveryAutoSaveDataDir);
project.SetFileName( wxT("") );
projName = wxT("");
projPath = wxT("");
}
else
{
realFileName += wxT(".aup");
projPath = realFileDir.GetFullPath();
project.SetFileName(
wxFileName{ projPath, realFileName }.GetFullPath() );
mbLoadedFromAup = true;
projName = value;
}
SetProjectTitle();
}
else {
projName = value;
projPath = wxPathOnly( project.GetFileName() );
}
if (!projName.empty())
{
// First try to load the data files based on the _data dir given in the .aup file
// If this fails then try to use the filename of the .aup as the base directory
// This is because unzipped projects e.g. those that get transfered between mac-pc
// have encoding issues and end up expanding the wrong filenames for certain
// international characters (such as capital 'A' with an umlaut.)
if (!dirManager.SetProject(projPath, projName, false))
{
projName = project.GetProjectName() + wxT("_data");
if (!dirManager.SetProject(projPath, projName, false)) {
AudacityMessageBox(wxString::Format(_("Couldn't find the project data folder: \"%s\""),
projName),
_("Error Opening Project"),
wxOK | wxCENTRE, &window);
return false;
}
}
}
requiredTags++;
}
else if (!wxStrcmp(attr, wxT("rate"))) {
double rate;
Internat::CompatibleToDouble(value, &rate);
settings.SetRate( rate );
}
else if (!wxStrcmp(attr, wxT("snapto"))) {
settings.SetSnapTo(wxString(value) == wxT("on") ? true : false);
}
else if (!wxStrcmp(attr, wxT("selectionformat")))
settings.SetSelectionFormat(
NumericConverter::LookupFormat( NumericConverter::TIME, value) );
else if (!wxStrcmp(attr, wxT("frequencyformat")))
settings.SetFrequencySelectionFormatName(
NumericConverter::LookupFormat( NumericConverter::FREQUENCY, value ) );
else if (!wxStrcmp(attr, wxT("bandwidthformat")))
settings.SetBandwidthSelectionFormatName(
NumericConverter::LookupFormat( NumericConverter::BANDWIDTH, value ) );
} // while
if (longVpos != 0) {
// PRL: It seems this must happen after SetSnapTo
viewInfo.vpos = longVpos;
}
// Specifically detect newer versions of Audacity
// WARNING: This will need review/revision if we ever have a version string
// such as 1.5.10, i.e. with 2 digit numbers.
// We're able to do a shortcut and use string comparison because we know
// that does not happen.
// TODO: Um. We actually have released 0.98 and 1.3.14 so the comment
// above is inaccurate.
if (!bFileVersionFound ||
(fileVersion.length() != 5) || // expecting '1.1.0', for example
// JKC: I commentted out next line. IsGoodInt is not for
// checking dotted numbers.
//!XMLValueChecker::IsGoodInt(fileVersion) ||
(fileVersion > wxT(AUDACITY_FILE_FORMAT_VERSION)))
{
wxString msg;
/* i18n-hint: %s will be replaced by the version number.*/
msg.Printf(_("This file was saved using Audacity %s.\nYou are using Audacity %s. You may need to upgrade to a newer version to open this file."),
audacityVersion,
AUDACITY_VERSION_STRING);
AudacityMessageBox(msg,
_("Can't open project file"),
wxOK | wxICON_EXCLAMATION | wxCENTRE, &window);
return false;
}
// NOTE: It looks as if there was some confusion about fileversion and audacityversion.
// fileversion NOT being increased when file formats changed, so unfortunately we're
// using audacityversion to rescue the situation.
// KLUDGE: guess the true 'fileversion' by stripping away any '-beta-Rc' stuff on
// audacityVersion.
// It's fairly safe to do this as it has already been established that the
// puported file version was five chars long.
fileVersion = audacityVersion.Mid(0,5);
bool bIsOld = fileVersion < wxT(AUDACITY_FILE_FORMAT_VERSION);
bool bIsVeryOld = fileVersion < wxT("1.1.9" );
// Very old file versions could even have the file version starting
// with text: 'AudacityProject Version 0.95'
// Atoi return zero in this case.
bIsVeryOld |= wxAtoi( fileVersion )==0;
// Specifically detect older versions of Audacity
if ( bIsOld | bIsVeryOld ) {
wxString msg;
msg.Printf(gsLegacyFileWarning(), audacityVersion);
int icon_choice = wxICON_EXCLAMATION;
if( bIsVeryOld )
// Stop icon, and choose 'NO' by default.
icon_choice = wxICON_STOP | wxNO_DEFAULT;
int action =
AudacityMessageBox(msg,
_("Warning - Opening Old Project File"),
wxYES_NO | icon_choice | wxCENTRE,
&window);
if (action == wxNO)
return false;
}
if (wxStrcmp(tag, wxT("audacityproject")) &&
wxStrcmp(tag, wxT("project"))) {
// If the tag name is not one of these two (the NEW name is
// "project" with an Audacity namespace, but we don't detect
// the namespace yet), then we don't know what the error is
return false;
}
if (requiredTags < 3)
return false;
// All other tests passed, so we succeed
return true;
}
XMLTagHandler *ProjectFileIO::HandleXMLChild(const wxChar *tag)
{
auto &project = mProject;
auto fn = ProjectFileIORegistry::Lookup( tag );
if (fn)
return fn( project );
return nullptr;
}
void ProjectFileIO::WriteXMLHeader(XMLWriter &xmlFile) const
{
xmlFile.Write(wxT("<?xml "));
xmlFile.Write(wxT("version=\"1.0\" "));
xmlFile.Write(wxT("standalone=\"no\" "));
xmlFile.Write(wxT("?>\n"));
xmlFile.Write(wxT("<!DOCTYPE "));
xmlFile.Write(wxT("project "));
xmlFile.Write(wxT("PUBLIC "));
xmlFile.Write(wxT("\"-//audacityproject-1.3.0//DTD//EN\" "));
xmlFile.Write(wxT("\"http://audacity.sourceforge.net/xml/audacityproject-1.3.0.dtd\" "));
xmlFile.Write(wxT(">\n"));
}
void ProjectFileIO::WriteXML(
XMLWriter &xmlFile, FilePaths *strOtherNamesArray)
// may throw
{
auto &proj = mProject;
auto &tracks = TrackList::Get( proj );
auto &viewInfo = ViewInfo::Get( proj );
auto &dirManager = DirManager::Get( proj );
auto &tags = Tags::Get( proj );
const auto &settings = ProjectSettings::Get( proj );
bool bWantSaveCopy = (strOtherNamesArray != nullptr);
//TIMER_START( "AudacityProject::WriteXML", xml_writer_timer );
// Warning: This block of code is duplicated in Save, for now...
wxFileName project { proj.GetFileName() };
if (project.GetExt() == wxT("aup"))
project.SetExt( {} );
auto projName = project.GetFullName() + wxT("_data");
// End Warning -DMM
xmlFile.StartTag(wxT("project"));
xmlFile.WriteAttr(wxT("xmlns"), wxT("http://audacity.sourceforge.net/xml/"));
if (mAutoSaving)
{
//
// When auto-saving, remember full path to data files directory
//
// Note: This attribute must currently be written and parsed before
// all other attributes
//
xmlFile.WriteAttr(wxT("datadir"), dirManager.GetDataFilesDir());
// Note that the code at the start assumes that if mFileName has a value
// then the file has been saved. This is not neccessarily true when
// autosaving as it gets set by AddImportedTracks (presumably as a proposal).
// I don't think that mDirManager.projName gets set without a save so check that.
if( !IsProjectSaved() )
projName = wxT("_data");
}
xmlFile.WriteAttr(wxT("projname"), projName);
xmlFile.WriteAttr(wxT("version"), wxT(AUDACITY_FILE_FORMAT_VERSION));
xmlFile.WriteAttr(wxT("audacityversion"), AUDACITY_VERSION_STRING);
viewInfo.WriteXMLAttributes(xmlFile);
xmlFile.WriteAttr(wxT("rate"), settings.GetRate());
xmlFile.WriteAttr(wxT("snapto"), settings.GetSnapTo() ? wxT("on") : wxT("off"));
xmlFile.WriteAttr(wxT("selectionformat"),
settings.GetSelectionFormat().Internal());
xmlFile.WriteAttr(wxT("frequencyformat"),
settings.GetFrequencySelectionFormatName().Internal());
xmlFile.WriteAttr(wxT("bandwidthformat"),
settings.GetBandwidthSelectionFormatName().Internal());
tags.WriteXML(xmlFile);
unsigned int ndx = 0;
tracks.Any().Visit(
[&](WaveTrack *pWaveTrack) {
if (bWantSaveCopy) {
if (!pWaveTrack->IsLeader())
return;
//vvv This should probably be a method, WaveTrack::WriteCompressedTrackXML().
xmlFile.StartTag(wxT("import"));
xmlFile.WriteAttr(wxT("filename"),
(*strOtherNamesArray)[ndx]); // Assumes mTracks order hasn't changed!
// Don't store "channel" and "linked" tags because the importer can figure that out,
// e.g., from stereo Ogg files.
// xmlFile.WriteAttr(wxT("channel"), t->GetChannel());
// xmlFile.WriteAttr(wxT("linked"), t->GetLinked());
const auto offset =
TrackList::Channels( pWaveTrack ).min( &WaveTrack::GetOffset );
xmlFile.WriteAttr(wxT("offset"), offset, 8);
xmlFile.WriteAttr(wxT("mute"), pWaveTrack->GetMute());
xmlFile.WriteAttr(wxT("solo"), pWaveTrack->GetSolo());
pWaveTrack->Track::WriteCommonXMLAttributes( xmlFile, false );
// Don't store "rate" tag because the importer can figure that out.
// xmlFile.WriteAttr(wxT("rate"), pWaveTrack->GetRate());
xmlFile.WriteAttr(wxT("gain"), (double)pWaveTrack->GetGain());
xmlFile.WriteAttr(wxT("pan"), (double)pWaveTrack->GetPan());
xmlFile.EndTag(wxT("import"));
ndx++;
}
else {
pWaveTrack->SetAutoSaveIdent(mAutoSaving ? ++ndx : 0);
pWaveTrack->WriteXML(xmlFile);
}
},
[&](Track *t) {
t->WriteXML(xmlFile);
}
);
if (!mAutoSaving)
{
// Only write closing bracket when not auto-saving, since we may add
// recording log data to the end of the file later
xmlFile.EndTag(wxT("project"));
}
//TIMER_STOP( xml_writer_timer );
}
static wxString CreateUniqueName()
{
static int count = 0;
return wxDateTime::Now().Format(wxT("%Y-%m-%d %H-%M-%S")) +
wxString::Format(wxT(" N-%i"), ++count);
}
//
// This small template class resembles a try-finally block
//
// It sets var to val_entry in the constructor and
// var to val_exit in the destructor.
//
template <typename T>
class VarSetter
{
public:
VarSetter(T* var, T val_entry, T val_exit)
{
mVar = var;
mValExit = val_exit;
*var = val_entry;
}
~VarSetter()
{
*mVar = mValExit;
}
private:
T* mVar;
T mValExit;
};
void ProjectFileIO::AutoSave()
{
auto &project = mProject;
auto &window = GetProjectFrame( project );
// SonifyBeginAutoSave(); // part of RBD's r10680 stuff now backed out
// To minimize the possibility of race conditions, we first write to a
// file with the extension ".tmp", then rename the file to .autosave
wxString projName;
auto fileName = project.GetFileName();
if (fileName.empty())
projName = wxT("New Project");
else
projName = wxFileName{ fileName }.GetName();
wxString fn = wxFileName(FileNames::AutoSaveDir(),
projName + wxString(wxT(" - ")) + CreateUniqueName()).GetFullPath();
// PRL: I found a try-catch and rewrote it,
// but this guard is unnecessary because AutoSaveFile does not throw
bool success = GuardedCall< bool >( [&]
{
VarSetter<bool> setter(&mAutoSaving, true, false);
AutoSaveFile buffer;
WriteXMLHeader( buffer );
WriteXML( buffer, nullptr );
wxFFile saveFile;
saveFile.Open(fn + wxT(".tmp"), wxT("wb"));
return buffer.Write(saveFile);
} );
if (!success)
return;
// Now that we have a NEW auto-save file, DELETE the old one
DeleteCurrentAutoSaveFile();
if (!mAutoSaveFileName.empty())
return; // could not remove auto-save file
if (!wxRenameFile(fn + wxT(".tmp"), fn + wxT(".autosave")))
{
AudacityMessageBox(
wxString::Format( _("Could not create autosave file: %s"),
fn + wxT(".autosave") ),
_("Error"), wxICON_STOP, &window);
return;
}
mAutoSaveFileName += fn + wxT(".autosave");
// no-op cruft that's not #ifdefed for NoteTrack
// See above for further comments.
// SonifyEndAutoSave();
}
void ProjectFileIO::DeleteCurrentAutoSaveFile()
{
auto &project = mProject;
auto &window = GetProjectFrame( project );
if (!mAutoSaveFileName.empty())
{
if (wxFileExists(mAutoSaveFileName))
{
if (!wxRemoveFile(mAutoSaveFileName))
{
AudacityMessageBox(
wxString::Format(
_("Could not remove old autosave file: %s"), mAutoSaveFileName ),
_("Error"), wxICON_STOP, &window);
return;
}
}
mAutoSaveFileName = wxT("");
}
}
bool ProjectFileIO::IsProjectSaved() const {
auto &project = mProject;
auto &dirManager = DirManager::Get( project );
// This is true if a project was opened from an .aup
// Otherwise it becomes true only when a project is first saved successfully
// in DirManager::SetProject
return (!dirManager.GetProjectName().empty());
}
void ProjectFileIO::Reset()
{
mProject.SetFileName( {} );
mIsRecovered = false;
mbLoadedFromAup = false;
SetProjectTitle();
}