/********************************************************************** 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 #include #include #include #include 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; }