audacia/src/Dependencies.cpp

642 lines
21 KiB
C++

/**********************************************************************
Audacity: A Digital Audio Editor
Audacity(R) is copyright (c) 1999-2008 Audacity Team.
License: GPL v2. See License.txt.
Dependencies.cpp
Dominic Mazzoni
Leland Lucius
Markus Meyer
LRN
Michael Chinen
Vaughan Johnson
The primary function provided in this source file is
ShowDependencyDialogIfNeeded. It checks a project to see if
any of its WaveTracks contain AliasBlockFiles; if so it
presents a dialog to the user and lets them copy those block
files into the project, making it self-contained.
********************************************************************//**
\class AliasedFile
\brief An audio file that is referenced (pointed into) directly from
an Audacity .aup file rather than Audacity having its own copies of the
data.
*//*****************************************************************//**
\class DependencyDialog
\brief DependencyDialog shows dependencies of an AudacityProject on
AliasedFile s.
*//********************************************************************/
#include "Audacity.h"
#include "Dependencies.h"
#include <wx/button.h>
#include <wx/defs.h>
#include <wx/dialog.h>
#include <wx/filename.h>
#include <wx/listctrl.h>
#include <wx/menu.h>
#include <wx/choice.h>
#include <wx/clipbrd.h>
#include <wx/dataobj.h>
#include <wx/frame.h>
#include <wx/stattext.h>
#include "blockfile/SimpleBlockFile.h"
#include "DirManager.h"
#include "FileFormats.h"
#include "Prefs.h"
#include "Project.h"
#include "Sequence.h"
#include "ShuttleGui.h"
#include "WaveTrack.h"
#include "WaveClip.h"
#include "prefs/QualityPrefs.h"
#include "widgets/AudacityMessageBox.h"
#include "widgets/ProgressDialog.h"
#include <unordered_map>
using AliasedFileHash = std::unordered_map<wxString, AliasedFile*>;
// These two hash types are used only inside short scopes
// so it is safe to key them by plain pointers.
using ReplacedBlockFileHash = std::unordered_map<BlockFile *, BlockFilePtr>;
using BoolBlockFileHash = std::unordered_map<BlockFile *, bool>;
// Given a project, returns a single array of all SeqBlocks
// in the current set of tracks. Enumerating that array allows
// you to process all block files in the current set.
static void GetAllSeqBlocks(AudacityProject *project,
BlockPtrArray *outBlocks)
{
for (auto waveTrack : TrackList::Get( *project ).Any< WaveTrack >()) {
for(const auto &clip : waveTrack->GetAllClips()) {
Sequence *sequence = clip->GetSequence();
BlockArray &blocks = sequence->GetBlockArray();
for (size_t i = 0; i < blocks.size(); i++)
outBlocks->push_back(&blocks[i]);
}
}
}
// Given an Audacity project and a hash mapping aliased block
// files to un-aliased block files, walk through all of the
// tracks and replace each aliased block file with its replacement.
// Note that this code respects reference-counting and thus the
// process of making a project self-contained is actually undoable.
static void ReplaceBlockFiles(BlockPtrArray &blocks,
ReplacedBlockFileHash &hash)
// NOFAIL-GUARANTEE
{
for (const auto &pBlock : blocks) {
auto &f = pBlock->f;
const auto src = &*f;
if (hash.count( src ) > 0) {
const auto &dst = hash[src];
f = dst;
}
}
}
void FindDependencies(AudacityProject *project,
AliasedFileArray &outAliasedFiles)
{
sampleFormat format = QualityPrefs::SampleFormatChoice();
BlockPtrArray blocks;
GetAllSeqBlocks(project, &blocks);
AliasedFileHash aliasedFileHash;
BoolBlockFileHash blockFileHash;
for (const auto &blockFile : blocks) {
const auto &f = blockFile->f;
if (f->IsAlias() && (blockFileHash.count( &*f ) == 0))
{
// f is an alias block we have not yet counted.
blockFileHash[ &*f ] = true; // Don't count the same blockfile twice.
auto aliasBlockFile = static_cast<AliasBlockFile*>( &*f );
const wxFileName &fileName = aliasBlockFile->GetAliasedFileName();
// In ProjectFSCK(), if the user has chosen to
// "Replace missing audio with silence", the code there puts in an empty wxFileName.
// Don't count those in dependencies.
if (!fileName.IsOk())
continue;
const wxString &fileNameStr = fileName.GetFullPath();
auto blockBytes = (SAMPLE_SIZE(format) *
aliasBlockFile->GetLength());
if (aliasedFileHash.count(fileNameStr) > 0)
// Already put this AliasBlockFile in aliasedFileHash.
// Update block count.
aliasedFileHash[fileNameStr]->mByteCount += blockBytes;
else
{
// Haven't counted this AliasBlockFile yet.
// Add to return array and internal hash.
// PRL: do this in two steps so that we move instead of copying.
// We don't have a moving push_back in all compilers.
outAliasedFiles.push_back(AliasedFile{});
outAliasedFiles.back() =
AliasedFile {
wxFileNameWrapper { fileName },
wxLongLong(blockBytes), fileName.FileExists()
};
aliasedFileHash[fileNameStr] = &outAliasedFiles.back();
}
}
}
}
// Given a project and a list of aliased files that should no
// longer be external dependencies (selected by the user), replace
// all of those alias block files with disk block files.
static void RemoveDependencies(AudacityProject *project,
AliasedFileArray &aliasedFiles)
// STRONG-GUARANTEE
{
auto &dirManager = DirManager::Get( *project );
ProgressDialog progress(
XO("Removing Dependencies"),
XO("Copying audio data into project..."));
auto updateResult = ProgressResult::Success;
// Hash aliasedFiles based on their full paths and
// count total number of bytes to process.
AliasedFileHash aliasedFileHash;
wxLongLong totalBytesToProcess = 0;
for (auto &aliasedFile : aliasedFiles) {
totalBytesToProcess += aliasedFile.mByteCount;
const wxString &fileNameStr = aliasedFile.mFileName.GetFullPath();
aliasedFileHash[fileNameStr] = &aliasedFile;
}
BlockPtrArray blocks;
GetAllSeqBlocks(project, &blocks);
const sampleFormat format = QualityPrefs::SampleFormatChoice();
ReplacedBlockFileHash blockFileHash;
wxLongLong completedBytes = 0;
for (const auto blockFile : blocks) {
const auto &f = blockFile->f;
if (f->IsAlias() && (blockFileHash.count( &*f ) == 0))
{
// f is an alias block we have not yet processed.
auto aliasBlockFile = static_cast<AliasBlockFile*>( &*f );
const wxFileName &fileName = aliasBlockFile->GetAliasedFileName();
const wxString &fileNameStr = fileName.GetFullPath();
if (aliasedFileHash.count(fileNameStr) == 0)
// This aliased file was not selected to be replaced. Skip it.
continue;
// Convert it from an aliased file to an actual file in the project.
auto len = aliasBlockFile->GetLength();
BlockFilePtr newBlockFile;
{
SampleBuffer buffer(len, format);
// We tolerate exceptions from NewBlockFile
// and so we can allow exceptions from ReadData too
f->ReadData(buffer.ptr(), format, 0, len);
newBlockFile =
dirManager.NewBlockFile( [&]( wxFileNameWrapper filePath ) {
return make_blockfile<SimpleBlockFile>(
std::move(filePath), buffer.ptr(), len, format);
} );
}
// Update our hash so we know what block files we've done
blockFileHash[ &*f ] = newBlockFile;
// Update the progress bar
completedBytes += SAMPLE_SIZE(format) * len;
updateResult = progress.Update(completedBytes, totalBytesToProcess);
if (updateResult != ProgressResult::Success)
// leave the project unchanged
return;
}
}
// COMMIT OPERATIONS needing NOFAIL-GUARANTEE:
// Above, we created a SimpleBlockFile contained in our project
// to go with each AliasBlockFile that we wanted to migrate.
// However, that didn't actually change any references to these
// blockfiles in the Sequences, so we do that next...
ReplaceBlockFiles(blocks, blockFileHash);
}
//
// DependencyDialog
//
class DependencyDialog final : public wxDialogWrapper
{
public:
DependencyDialog(wxWindow *parent,
wxWindowID id,
AudacityProject *project,
AliasedFileArray &aliasedFiles,
bool isSaving);
private:
void PopulateList();
void PopulateOrExchange(ShuttleGui & S);
// event handlers
void OnCancel(wxCommandEvent& evt);
void OnCopySelectedFiles(wxCommandEvent &evt);
void OnList(wxListEvent &evt);
void OnSize(wxSizeEvent &evt);
void OnNo(wxCommandEvent &evt);
void OnYes(wxCommandEvent &evt);
void OnRightClick(wxListEvent& evt);
void OnCopyToClipboard( wxCommandEvent& evt );
void SaveFutureActionChoice();
AudacityProject *mProject;
AliasedFileArray &mAliasedFiles;
bool mIsSaving;
bool mHasMissingFiles;
bool mHasNonMissingFiles;
wxStaticText *mMessageStaticText;
wxListCtrl *mFileListCtrl;
wxButton *mCopySelectedFilesButton;
wxButton *mCopyAllFilesButton;
wxChoice *mFutureActionChoice;
public:
DECLARE_EVENT_TABLE()
};
enum {
FileListID = 6000,
CopySelectedFilesButtonID,
CopyNamesToClipboardID,
FutureActionChoiceID
};
BEGIN_EVENT_TABLE(DependencyDialog, wxDialogWrapper)
EVT_LIST_ITEM_SELECTED(FileListID, DependencyDialog::OnList)
EVT_LIST_ITEM_DESELECTED(FileListID, DependencyDialog::OnList)
EVT_LIST_ITEM_RIGHT_CLICK(FileListID, DependencyDialog::OnRightClick )
EVT_BUTTON(CopySelectedFilesButtonID, DependencyDialog::OnCopySelectedFiles)
EVT_SIZE(DependencyDialog::OnSize)
EVT_BUTTON(wxID_NO, DependencyDialog::OnNo) // mIsSaving ? "Cancel Save" : "Save Without Copying"
EVT_BUTTON(wxID_YES, DependencyDialog::OnYes) // "Copy All Files (Safer)"
EVT_BUTTON(wxID_CANCEL, DependencyDialog::OnCancel) // "Cancel Save"
EVT_MENU(CopyNamesToClipboardID,DependencyDialog::OnCopyToClipboard)
END_EVENT_TABLE()
DependencyDialog::DependencyDialog(wxWindow *parent,
wxWindowID id,
AudacityProject *project,
AliasedFileArray &aliasedFiles,
bool isSaving)
: wxDialogWrapper(parent, id, XO("Project Depends on Other Audio Files"),
wxDefaultPosition, wxDefaultSize,
(isSaving ?
(wxDEFAULT_DIALOG_STYLE & ~wxCLOSE_BOX) : // no close box when saving
wxDEFAULT_DIALOG_STYLE) |
wxRESIZE_BORDER),
mProject(project),
mAliasedFiles(aliasedFiles),
mIsSaving(isSaving),
mHasMissingFiles(false),
mHasNonMissingFiles(false),
mMessageStaticText(NULL),
mFileListCtrl(NULL),
mCopySelectedFilesButton(NULL),
mCopyAllFilesButton(NULL),
mFutureActionChoice(NULL)
{
SetName();
ShuttleGui S(this, eIsCreating);
PopulateOrExchange(S);
}
static const TranslatableString kStdMsg()
{
return
XO("Copying these files into your project will remove this dependency.\
\nThis is safer, but needs more disk space.");
}
static const TranslatableString kExtraMsgForMissingFiles()
{
return
XO("\n\nFiles shown as MISSING have been moved or deleted and cannot be copied.\
\nRestore them to their original location to be able to copy into project.");
}
void DependencyDialog::PopulateOrExchange(ShuttleGui& S)
{
S.SetBorder(5);
S.StartVerticalLay();
{
mMessageStaticText = S.AddVariableText(kStdMsg(), false);
S.StartStatic(XO("Project Dependencies"),1);
{
mFileListCtrl = S.Id(FileListID).AddListControlReportMode({
{ XO("Audio File"), wxLIST_FORMAT_LEFT, 220 },
{ XO("Disk Space"), wxLIST_FORMAT_LEFT, 120 }
});
PopulateList();
mCopySelectedFilesButton =
S.Id(CopySelectedFilesButtonID)
.Focus()
.Disable(mFileListCtrl->GetSelectedItemCount() <= 0)
.AddButton(
XXO("Copy Selected Files"),
wxALIGN_LEFT, true);
}
S.EndStatic();
S.StartHorizontalLay(wxALIGN_CENTRE,0);
{
if (mIsSaving) {
S.Id(wxID_CANCEL).AddButton(XXO("Cancel Save"));
S.Id(wxID_NO).AddButton(XXO("Save Without Copying"));
}
else
S.Id(wxID_NO).AddButton(XXO("Do Not Copy"));
mCopyAllFilesButton =
S.Id(wxID_YES)
// Enabling mCopyAllFilesButton is also done in PopulateList,
// but at its call above, mCopyAllFilesButton does not yet exist.
.Disable(mHasMissingFiles)
.AddButton(XXO("Copy All Files (Safer)"));
}
S.EndHorizontalLay();
if (mIsSaving)
{
S.StartHorizontalLay(wxALIGN_LEFT,0);
{
mFutureActionChoice =
S.Id(FutureActionChoiceID).AddChoice(
XXO("Whenever a project depends on other files:"),
{
/*i18n-hint: One of the choices of what you want Audacity to do when
* Audacity finds a project depends on another file.*/
XO("Ask me") ,
/*i18n-hint: One of the choices of what you want Audacity to do when
* Audacity finds a project depends on another file.*/
XO("Always copy all files (safest)") ,
/*i18n-hint: One of the choices of what you want Audacity to do when
* Audacity finds a project depends on another file.*/
XO("Never copy any files") ,
},
0 // "Ask me"
);
}
S.EndHorizontalLay();
}
else
{
mFutureActionChoice = NULL;
}
}
S.EndVerticalLay();
Layout();
Fit();
SetMinSize(GetSize());
Center();
}
void DependencyDialog::PopulateList()
{
mFileListCtrl->DeleteAllItems();
mHasMissingFiles = false;
mHasNonMissingFiles = false;
long i = 0;
for (const auto &aliasedFile : mAliasedFiles) {
const wxFileName &fileName = aliasedFile.mFileName;
wxLongLong byteCount = (aliasedFile.mByteCount * 124) / 100;
bool bOriginalExists = aliasedFile.mbOriginalExists;
if (bOriginalExists)
{
mFileListCtrl->InsertItem(i, fileName.GetFullPath());
mHasNonMissingFiles = true;
mFileListCtrl->SetItemState(i, wxLIST_STATE_SELECTED, wxLIST_STATE_SELECTED);
}
else
{
mFileListCtrl->InsertItem(i,
wxString::Format( _("MISSING %s"), fileName.GetFullPath() ) );
mHasMissingFiles = true;
mFileListCtrl->SetItemState(i, 0, wxLIST_STATE_SELECTED); // Deselect.
mFileListCtrl->SetItemTextColour(i, *wxRED);
}
mFileListCtrl->SetItem(i, 1, Internat::FormatSize(byteCount).Translation());
mFileListCtrl->SetItemData(i, long(bOriginalExists));
++i;
}
auto msg = kStdMsg();
if (mHasMissingFiles)
msg += kExtraMsgForMissingFiles();
mMessageStaticText->SetLabel(msg.Translation());
if (mCopyAllFilesButton)
mCopyAllFilesButton->Enable(!mHasMissingFiles);
}
void DependencyDialog::OnList(wxListEvent &evt)
{
if (!mCopySelectedFilesButton || !mFileListCtrl)
return;
wxString itemStr = evt.GetText();
if (evt.GetData() == 0)
// This list item is one of mAliasedFiles for which
// the original is missing, i.e., moved or deleted.
// wxListCtrl does not provide for items that are not
// allowed to be selected, so always deselect these items.
mFileListCtrl->SetItemState(evt.GetIndex(), 0, wxLIST_STATE_SELECTED); // Deselect.
mCopySelectedFilesButton->Enable(
mFileListCtrl->GetSelectedItemCount() > 0);
}
void DependencyDialog::OnSize(wxSizeEvent &evt)
{
int fileListCtrlWidth, fileListCtrlHeight;
mFileListCtrl->GetSize(&fileListCtrlWidth, &fileListCtrlHeight);
// File path is column 0. File size is column 1.
// File size column is always 120 px wide.
// Also subtract 8 from file path column width for borders.
mFileListCtrl->SetColumnWidth(0, fileListCtrlWidth - 120 - 8);
mFileListCtrl->SetColumnWidth(1, 120);
wxDialogWrapper::OnSize(evt);
}
void DependencyDialog::OnNo(wxCommandEvent & WXUNUSED(event))
{
SaveFutureActionChoice();
EndModal(wxID_NO);
}
void DependencyDialog::OnYes(wxCommandEvent & WXUNUSED(event))
{
SaveFutureActionChoice();
EndModal(wxID_YES);
}
void DependencyDialog::OnCopySelectedFiles(wxCommandEvent & WXUNUSED(event))
{
AliasedFileArray aliasedFilesToDelete, remainingAliasedFiles;
long i = 0;
for( const auto &file : mAliasedFiles ) {
if (mFileListCtrl->GetItemState(i, wxLIST_STATE_SELECTED))
aliasedFilesToDelete.push_back( file );
else
remainingAliasedFiles.push_back( file );
++i;
}
// provides STRONG-GUARANTEE
RemoveDependencies(mProject, aliasedFilesToDelete);
// COMMIT OPERATIONS needing NOFAIL-GUARANTEE:
mAliasedFiles.swap( remainingAliasedFiles );
PopulateList();
if (mAliasedFiles.empty() || !mHasNonMissingFiles)
{
SaveFutureActionChoice();
EndModal(wxID_NO); // Don't need to remove dependencies
}
}
void DependencyDialog::OnRightClick( wxListEvent& event)
{
static_cast<void>(event);
wxMenu menu;
menu.Append(CopyNamesToClipboardID, _("&Copy Names to Clipboard"));
PopupMenu(&menu);
}
void DependencyDialog::OnCopyToClipboard( wxCommandEvent& )
{
TranslatableString Files;
for (const auto &aliasedFile : mAliasedFiles) {
const wxFileName & fileName = aliasedFile.mFileName;
wxLongLong byteCount = (aliasedFile.mByteCount * 124) / 100;
bool bOriginalExists = aliasedFile.mbOriginalExists;
// All fields quoted, as e.g. size may contain a comma in the number.
Files += XO( "\"%s\", \"%s\", \"%s\"\n").Format(
fileName.GetFullPath(),
Internat::FormatSize( byteCount),
bOriginalExists ? XO("OK") : XO("Missing") );
}
// copy data onto clipboard
if (wxTheClipboard->Open()) {
// Clipboard owns the data you give it
wxTheClipboard->SetData(safenew wxTextDataObject(Files.Translation()));
wxTheClipboard->Close();
}
}
void DependencyDialog::OnCancel(wxCommandEvent& WXUNUSED(event))
{
if (mIsSaving)
{
int ret = AudacityMessageBox(
XO(
"If you proceed, your project will not be saved to disk. Is this what you want?"),
XO("Cancel Save"),
wxICON_QUESTION | wxYES_NO | wxNO_DEFAULT, this);
if (ret != wxYES)
return;
}
EndModal(wxID_CANCEL);
}
void DependencyDialog::SaveFutureActionChoice()
{
if (mFutureActionChoice)
{
wxString savePref;
int sel = mFutureActionChoice->GetSelection();
switch (sel)
{
case 1: savePref = wxT("copy"); break;
case 2: savePref = wxT("never"); break;
default: savePref = wxT("ask");
}
FileFormatsSaveWithDependenciesSetting.Write( savePref );
gPrefs->Flush();
}
}
// Checks for alias block files, modifies the project if the
// user requests it, and returns true if the user continues.
// Returns false only if the user clicks Cancel.
bool ShowDependencyDialogIfNeeded(AudacityProject *project,
bool isSaving)
{
auto pWindow = FindProjectFrame( project );
AliasedFileArray aliasedFiles;
FindDependencies(project, aliasedFiles);
if (aliasedFiles.empty()) {
if (!isSaving)
{
auto msg =
XO("Your project is self-contained; it does not depend on any external audio files. \
\n\nSome older Audacity projects may not be self-contained, and care \n\
is needed to keep their external dependencies in the right place.\n\
New projects will be self-contained and are less risky.");
AudacityMessageBox(
msg,
XO("Dependency Check"),
wxOK | wxICON_INFORMATION,
pWindow);
}
return true; // Nothing to do.
}
if (isSaving)
{
RemoveDependencies(project, aliasedFiles);
return true;
}
DependencyDialog dlog(pWindow, -1, project, aliasedFiles, isSaving);
int returnCode = dlog.ShowModal();
if (returnCode == wxID_CANCEL)
return false;
else if (returnCode == wxID_YES)
RemoveDependencies(project, aliasedFiles);
return true;
}