audacia/src/AutoRecoveryDialog.cpp

509 lines
12 KiB
C++

/**********************************************************************
Audacity: A Digital Audio Editor
AutoRecoveryDialog.cpp
Paul Licameli split from AutoRecovery.cpp
**********************************************************************/
#include "AutoRecoveryDialog.h"
#include "ActiveProjects.h"
#include "ProjectManager.h"
#include "ProjectFileIO.h"
#include "ProjectFileManager.h"
#include "ShuttleGui.h"
#include "TempDirectory.h"
#include "widgets/AudacityMessageBox.h"
#include "widgets/wxPanelWrapper.h"
#include <wx/dir.h>
#include <wx/evtloop.h>
#include <wx/filefn.h>
#include <wx/filename.h>
#include <wx/listctrl.h>
enum {
ID_QUIT_AUDACITY = 10000,
ID_DISCARD_SELECTED,
ID_RECOVER_SELECTED,
ID_SKIP,
ID_FILE_LIST
};
class AutoRecoveryDialog final : public wxDialogWrapper
{
public:
explicit AutoRecoveryDialog(AudacityProject *proj);
bool HasRecoverables() const;
FilePaths GetRecoverables();
private:
void PopulateOrExchange(ShuttleGui &S);
void PopulateList();
bool HaveChecked();
void OnQuitAudacity(wxCommandEvent &evt);
void OnDiscardSelected(wxCommandEvent &evt);
void OnRecoverSelected(wxCommandEvent &evt);
void OnSkip(wxCommandEvent &evt);
void OnColumnClicked(wxListEvent &evt);
void OnItemActivated(wxListEvent &evt);
void OnListKeyDown(wxKeyEvent &evt);
FilePaths mFiles;
wxListCtrl *mFileList;
AudacityProject *mProject;
public:
DECLARE_EVENT_TABLE()
};
BEGIN_EVENT_TABLE(AutoRecoveryDialog, wxDialogWrapper)
EVT_BUTTON(ID_QUIT_AUDACITY, AutoRecoveryDialog::OnQuitAudacity)
EVT_BUTTON(ID_DISCARD_SELECTED, AutoRecoveryDialog::OnDiscardSelected)
EVT_BUTTON(ID_RECOVER_SELECTED, AutoRecoveryDialog::OnRecoverSelected)
EVT_BUTTON(ID_SKIP, AutoRecoveryDialog::OnSkip)
EVT_LIST_COL_CLICK(ID_FILE_LIST, AutoRecoveryDialog::OnColumnClicked)
EVT_LIST_ITEM_ACTIVATED(ID_FILE_LIST, AutoRecoveryDialog::OnItemActivated)
END_EVENT_TABLE()
AutoRecoveryDialog::AutoRecoveryDialog(AudacityProject *project)
: wxDialogWrapper(nullptr, wxID_ANY, XO("Automatic Crash Recovery"),
wxDefaultPosition, wxDefaultSize,
(wxDEFAULT_DIALOG_STYLE & (~wxCLOSE_BOX)) | wxRESIZE_BORDER), // no close box
mProject(project)
{
SetName();
ShuttleGui S(this, eIsCreating);
PopulateOrExchange(S);
}
bool AutoRecoveryDialog::HasRecoverables() const
{
return mFiles.size() > 0;
}
FilePaths AutoRecoveryDialog::GetRecoverables()
{
return mFiles;
}
void AutoRecoveryDialog::PopulateOrExchange(ShuttleGui &S)
{
S.SetBorder(5);
S.StartVerticalLay(wxEXPAND, 1);
{
S.AddFixedText(
XO("The following projects were not saved properly the last time Audacity was run and "
"can be automatically recovered.\n\n"
"After recovery, save the projects to ensure changes are written to disk."),
false,
500);
S.StartStatic(XO("Recoverable &projects"), 1);
{
mFileList = S.Id(ID_FILE_LIST)
.ConnectRoot(wxEVT_KEY_DOWN, &AutoRecoveryDialog::OnListKeyDown)
.AddListControlReportMode(
{
/*i18n-hint: (verb). It instruct the user to select items.*/
XO("Select"),
/*i18n-hint: (noun). It's the name of the project to recover.*/
XO("Name")
});
mFileList->EnableCheckBoxes();
PopulateList();
}
S.EndStatic();
S.StartHorizontalLay(wxALIGN_CENTRE, 0);
{
S.Id(ID_QUIT_AUDACITY).AddButton(XXO("&Quit Audacity"));
S.Id(ID_DISCARD_SELECTED).AddButton(XXO("&Discard Selected"));
S.Id(ID_RECOVER_SELECTED).AddButton(XXO("&Recover Selected"), wxALIGN_CENTRE, true);
S.Id(ID_SKIP).AddButton(XXO("&Skip"));
SetAffirmativeId(ID_RECOVER_SELECTED);
SetEscapeId(ID_SKIP);
}
S.EndHorizontalLay();
}
S.EndVerticalLay();
Layout();
Fit();
SetMinSize(GetSize());
// Sometimes it centers on wxGTK and sometimes it doesn't.
// Yielding before centering seems to be a good workaround,
// but will leave to implement on a rainy day.
Center();
}
void AutoRecoveryDialog::PopulateList()
{
wxString tempdir = TempDirectory::TempDir();
wxString pattern = wxT("*.") + FileNames::UnsavedProjectExtension();
FilePaths files;
wxDir::GetAllFiles(tempdir, &files, pattern, wxDIR_FILES);
FilePaths active = ActiveProjects::GetAll();
for (auto file : active)
{
wxFileName fn = file;
if (fn.FileExists())
{
FilePath fullPath = fn.GetFullPath();
if (files.Index(fullPath) == wxNOT_FOUND)
{
files.push_back(fullPath);
}
}
else
{
ActiveProjects::Remove(file);
}
}
FilePath activeFile;
if (mProject)
{
auto &projectFileIO = ProjectFileIO::Get(*mProject);
activeFile = projectFileIO.GetFileName();
}
mFiles.clear();
mFileList->DeleteAllItems();
long item = 0;
for (auto file : files)
{
wxFileName fn = file;
if (fn != activeFile)
{
mFiles.push_back(fn.GetFullPath());
mFileList->InsertItem(item, wxT(""));
mFileList->SetItem(item, 1, fn.GetName());
mFileList->CheckItem(item, true);
item++;
}
}
mFileList->SetMinSize(mFileList->GetBestSize());
mFileList->SetColumnWidth(0, wxLIST_AUTOSIZE_USEHEADER);
mFileList->SetColumnWidth(1, 500);
if (item)
{
mFileList->SetItemState(0,
wxLIST_STATE_FOCUSED | wxLIST_STATE_SELECTED,
wxLIST_STATE_FOCUSED | wxLIST_STATE_SELECTED);
mFileList->SetFocus();
}
}
bool AutoRecoveryDialog::HaveChecked()
{
long item = -1;
while (true)
{
item = mFileList->GetNextItem(item, wxLIST_NEXT_ALL, wxLIST_STATE_DONTCARE);
if (item == wxNOT_FOUND)
{
break;
}
if (mFileList->IsItemChecked(item))
{
return true;
}
}
AudacityMessageBox(XO("No projects selected"), XO("Automatic Crash Recovery"));
return false;
}
void AutoRecoveryDialog::OnQuitAudacity(wxCommandEvent &WXUNUSED(evt))
{
EndModal(ID_QUIT_AUDACITY);
}
void AutoRecoveryDialog::OnDiscardSelected(wxCommandEvent &WXUNUSED(evt))
{
if (!HaveChecked())
{
return;
}
long item = -1;
bool selectedTemporary = false;
while (!selectedTemporary)
{
item = mFileList->GetNextItem(item, wxLIST_NEXT_ALL, wxLIST_STATE_DONTCARE);
if (item == wxNOT_FOUND)
break;
if (!mFileList->IsItemChecked(item))
continue;
FilePath fileName = mFiles[item];
wxFileName file(fileName);
if (file.GetExt().IsSameAs(FileNames::UnsavedProjectExtension()))
selectedTemporary = true;
}
// Don't give this warning message if all of the checked items are
// previously saved, non-temporary projects.
if (selectedTemporary) {
int ret = AudacityMessageBox(
XO("Are you sure you want to discard the selected projects?\n\n"
"Choosing \"Yes\" permanently deletes the selected projects immediately."),
XO("Automatic Crash Recovery"),
wxICON_QUESTION | wxYES_NO | wxNO_DEFAULT, this);
if (ret == wxNO)
return;
}
item = -1;
FilePaths files;
while (true)
{
item = mFileList->GetNextItem(item, wxLIST_NEXT_ALL, wxLIST_STATE_DONTCARE);
if (item == wxNOT_FOUND)
{
break;
}
if (!mFileList->IsItemChecked(item))
{
// Keep in list
files.push_back(mFiles[item]);
continue;
}
FilePath fileName = mFiles[item];
// Only remove it from disk if it appears to be a temporary file.
wxFileName file(fileName);
if (file.GetExt().IsSameAs(FileNames::UnsavedProjectExtension()))
{
file.SetFullName(wxT(""));
wxFileName temp(TempDirectory::TempDir(), wxT(""));
if (file == temp)
ProjectFileIO::RemoveProject(fileName);
}
else
// Don't remove from disk, but do (later) open the database
// of this saved file, and discard edits
files.push_back(fileName);
// Forget all about it
ActiveProjects::Remove(fileName);
}
PopulateList();
mFiles = files;
if (mFileList->GetItemCount() == 0)
{
EndModal(ID_DISCARD_SELECTED);
}
}
void AutoRecoveryDialog::OnRecoverSelected(wxCommandEvent &WXUNUSED(evt))
{
if (!HaveChecked())
{
return;
}
FilePaths files;
bool selected = false;
long item = -1;
while (true)
{
item = mFileList->GetNextItem(item, wxLIST_NEXT_ALL, wxLIST_STATE_DONTCARE);
if (item == wxNOT_FOUND)
{
if (!selected)
{
AudacityMessageBox(XO("No projects selected"), XO("Automatic Crash Recovery"));
}
break;
}
selected = true;
if (!mFileList->IsItemChecked(item))
{
continue;
}
files.push_back(mFiles[item]);
}
mFiles = files;
EndModal(ID_RECOVER_SELECTED);
}
void AutoRecoveryDialog::OnSkip(wxCommandEvent &WXUNUSED(evt))
{
EndModal(ID_SKIP);
}
void AutoRecoveryDialog::OnColumnClicked(wxListEvent &evt)
{
if (evt.GetColumn() != 0)
{
return;
}
long item = -1;
while (true)
{
item = mFileList->GetNextItem(item, wxLIST_NEXT_ALL, wxLIST_STATE_DONTCARE);
if (item == wxNOT_FOUND)
{
break;
}
mFileList->CheckItem(item, !mFileList->IsItemChecked(item));
}
}
void AutoRecoveryDialog::OnItemActivated(wxListEvent &evt)
{
long item = evt.GetIndex();
mFileList->CheckItem(item, !mFileList->IsItemChecked(item));
}
void AutoRecoveryDialog::OnListKeyDown(wxKeyEvent &evt)
{
switch (evt.GetKeyCode())
{
case WXK_SPACE:
{
bool selected = false;
long item = -1;
while (true)
{
item = mFileList->GetNextItem(item, wxLIST_NEXT_ALL, wxLIST_STATE_SELECTED);
if (item == wxNOT_FOUND)
{
break;
}
mFileList->CheckItem(item, !mFileList->IsItemChecked(item));
}
}
break;
case WXK_RETURN:
// Don't know why wxListCtrls prevent default dialog action,
// but they do, so handle it.
EmulateButtonClickIfPresent(GetAffirmativeId());
break;
default:
evt.Skip();
break;
}
}
////////////////////////////////////////////////////////////////////////////
static bool RecoverAllProjects(const FilePaths &files,
AudacityProject *&pproj)
{
// Open a project window for each auto save file
wxString filename;
bool result = true;
for (auto &file: files)
{
AudacityProject *proj = nullptr;
// Reuse any existing project window, which will be the empty project
// created at application startup
std::swap(proj, pproj);
// Open project.
if (ProjectManager::OpenProject(proj, file, false, true) == nullptr)
{
result = false;
}
}
return result;
}
static void DiscardAllProjects(const FilePaths &files)
{
// Open and close each file, invisibly, removing its Autosave blob
for (auto &file: files)
ProjectFileManager::DiscardAutosave(file);
}
bool ShowAutoRecoveryDialogIfNeeded(AudacityProject *&pproj, bool *didRecoverAnything)
{
if (didRecoverAnything)
{
*didRecoverAnything = false;
}
bool success = true;
// Under wxGTK3, the auto recovery dialog will not get
// the focus since the project window hasn't been allowed
// to completely initialize.
//
// Yielding seems to allow the initialization to complete.
//
// Additionally, it also corrects a sizing issue in the dialog
// related to wxWidgets bug:
//
// http://trac.wxwidgets.org/ticket/16440
//
// This must be done before "dlg" is declared.
wxEventLoopBase::GetActive()->YieldFor(wxEVT_CATEGORY_UI);
AutoRecoveryDialog dialog(pproj);
if (dialog.HasRecoverables())
{
int ret = dialog.ShowModal();
switch (ret)
{
case ID_SKIP:
success = true;
break;
case ID_DISCARD_SELECTED:
DiscardAllProjects(dialog.GetRecoverables());
success = true;
break;
case ID_RECOVER_SELECTED:
success = RecoverAllProjects(dialog.GetRecoverables(), pproj);
if (success)
{
if (didRecoverAnything)
{
*didRecoverAnything = true;
}
}
break;
default:
// This includes ID_QUIT_AUDACITY
return false;
}
}
return success;
}