1
0
mirror of https://github.com/cookiengineer/audacity synced 2025-08-01 00:19:27 +02:00

AUP3: AutoRecoveryDialog experiment

Looking for feedback...
This commit is contained in:
Leland Lucius 2020-07-23 01:19:29 -05:00
parent a3fcd611b5
commit a1e83c141a
7 changed files with 3126 additions and 86 deletions

98
src/ActiveProjects.cpp Normal file
View File

@ -0,0 +1,98 @@
/**********************************************************************
Audacity: A Digital Audio Editor
ActiveProjects.cpp
********************************************************************//**
\class ActiveProjects
\brief Manages a list of active projects
*//********************************************************************/
#include "Audacity.h"
#include "ActiveProjects.h"
#include "prefs.h"
#include <wx/filename.h>
FilePaths ActiveProjects::GetAll()
{
FilePaths files;
wxString key;
long ndx;
wxString configPath = gPrefs->GetPath();
gPrefs->SetPath(wxT("/ActiveProjects"));
bool more = gPrefs->GetFirstEntry(key, ndx);
while (more)
{
wxFileName path = gPrefs->Read(key, wxT(""));
files.Add(path.GetFullPath());
more = gPrefs->GetNextEntry(key, ndx);
}
gPrefs->SetPath(configPath);
return files;
}
void ActiveProjects::Add(const FilePath &path)
{
wxString key = Find(path);
if (key.empty())
{
int i = 0;
do
{
key.Printf(wxT("/ActiveProjects/%d"), ++i);
} while (gPrefs->HasEntry(key));
gPrefs->Write(key, path);
gPrefs->Flush();
}
}
void ActiveProjects::Remove(const FilePath &path)
{
wxString key = Find(path);
if (!key.empty())
{
gPrefs->DeleteEntry(wxT("/ActiveProjects/" + key));
gPrefs->Flush();
}
}
wxString ActiveProjects::Find(const FilePath &path)
{
bool found = false;
wxString key;
long ndx;
wxString configPath = gPrefs->GetPath();
gPrefs->SetPath(wxT("/ActiveProjects"));
bool more = gPrefs->GetFirstEntry(key, ndx);
while (more)
{
if (gPrefs->Read(key, wxT("")).IsSameAs(path))
{
found = true;
break;
}
more = gPrefs->GetNextEntry(key, ndx);
}
gPrefs->SetPath(configPath);
return found ? key : wxT("");
}

25
src/ActiveProjects.h Normal file
View File

@ -0,0 +1,25 @@
/**********************************************************************
Audacity: A Digital Audio Editor
ActiveProjects.h
**********************************************************************/
#ifndef __AUDACITY_ACTIVE_PROJECTS__
#define __AUDACITY_ACTIVE_PROJECTS__
#include "Audacity.h"
#include "audacity/Types.h"
#include <wx/string.h>
namespace ActiveProjects
{
FilePaths GetAll();
void Add(const FilePath &path);
void Remove(const FilePath &path);
wxString Find(const FilePath &path);
};
#endif

View File

@ -10,60 +10,76 @@ Paul Licameli split from AutoRecovery.cpp
#include "AutoRecoveryDialog.h"
#include "ActiveProjects.h"
#include "FileNames.h"
#include "ProjectManager.h"
#include "ShuttleGui.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>
#define USE_CHECKBOXES
enum {
ID_RECOVER_ALL = 10000,
ID_RECOVER_NONE,
ID_QUIT_AUDACITY,
ID_QUIT_AUDACITY = 10000,
ID_DISCARD_SELECTED,
ID_RECOVER_SELECTED,
ID_FILE_LIST
};
class AutoRecoveryDialog final : public wxDialogWrapper
{
public:
AutoRecoveryDialog(const FilePaths &files);
AutoRecoveryDialog();
bool HasRecoverables() const;
FilePaths GetRecoverables();
private:
void PopulateOrExchange(ShuttleGui &S);
void PopulateList();
void OnQuitAudacity(wxCommandEvent &evt);
void OnRecoverNone(wxCommandEvent &evt);
void OnRecoverAll(wxCommandEvent &evt);
void OnDiscardSelected(wxCommandEvent &evt);
void OnRecoverSelected(wxCommandEvent &evt);
const FilePaths & mFiles;
FilePaths mFiles;
wxListCtrl *mFileList;
public:
DECLARE_EVENT_TABLE()
};
AutoRecoveryDialog::AutoRecoveryDialog(const FilePaths &files)
: wxDialogWrapper(nullptr, -1, XO("Automatic Crash Recovery"),
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)
END_EVENT_TABLE()
AutoRecoveryDialog::AutoRecoveryDialog()
: wxDialogWrapper(nullptr, wxID_ANY, XO("Automatic Crash Recovery"),
wxDefaultPosition, wxDefaultSize,
wxDEFAULT_DIALOG_STYLE & (~wxCLOSE_BOX)), // no close box
mFiles(files)
wxDEFAULT_DIALOG_STYLE & (~wxCLOSE_BOX)) // no close box
{
SetName();
ShuttleGui S(this, eIsCreating);
PopulateOrExchange(S);
}
BEGIN_EVENT_TABLE(AutoRecoveryDialog, wxDialogWrapper)
EVT_BUTTON(ID_RECOVER_ALL, AutoRecoveryDialog::OnRecoverAll)
EVT_BUTTON(ID_RECOVER_NONE, AutoRecoveryDialog::OnRecoverNone)
EVT_BUTTON(ID_QUIT_AUDACITY, AutoRecoveryDialog::OnQuitAudacity)
END_EVENT_TABLE()
bool AutoRecoveryDialog::HasRecoverables() const
{
return mFiles.size() > 0;
}
FilePaths AutoRecoveryDialog::GetRecoverables()
{
return mFiles;
}
void AutoRecoveryDialog::PopulateOrExchange(ShuttleGui &S)
{
@ -76,9 +92,18 @@ void AutoRecoveryDialog::PopulateOrExchange(ShuttleGui &S)
S.StartStatic(XO("Recoverable projects"));
{
mFileList = S.Id(ID_FILE_LIST)
/*i18n-hint: (noun). It's the name of the project to recover.*/
.AddListControlReportMode( { XO("Name") } );
mFileList = S.Id(ID_FILE_LIST).AddListControlReportMode(
{
#if defined(USE_CHECKBOXES)
/*i18n-hint: (verb). It instruct the user to select items.*/
XO("Select"),
#endif
/*i18n-hint: (noun). It's the name of the project to recover.*/
XO("Name")
});
#if defined(USE_CHECKBOXES)
mFileList->EnableCheckBoxes();
#endif
PopulateList();
}
S.EndStatic();
@ -90,8 +115,8 @@ void AutoRecoveryDialog::PopulateOrExchange(ShuttleGui &S)
S.StartHorizontalLay();
{
S.Id(ID_QUIT_AUDACITY).AddButton(XXO("Quit Audacity"));
S.Id(ID_RECOVER_NONE).AddButton(XXO("Discard Projects"));
S.Id(ID_RECOVER_ALL).AddButton(XXO("Recover Projects"));
S.Id(ID_DISCARD_SELECTED).AddButton(XXO("Discard Selected"));
S.Id(ID_RECOVER_SELECTED).AddButton(XXO("Recover Selected"));
}
S.EndHorizontalLay();
}
@ -109,14 +134,46 @@ void AutoRecoveryDialog::PopulateOrExchange(ShuttleGui &S)
void AutoRecoveryDialog::PopulateList()
{
wxString tempdir = FileNames::TempDir();
wxString pattern = wxT("*.") + FileNames::UnsavedProjectExtension();
FilePaths files;
wxDir::GetAllFiles(tempdir, &files, pattern, wxDIR_FILES);
FilePaths active = ActiveProjects::GetAll();
mFileList->DeleteAllItems();
for (int i = 0, cnt = mFiles.size(); i < cnt; ++i)
long item = 0;
for (auto file : active)
{
mFileList->InsertItem(i, wxFileName{ mFiles[i] }.GetName());
wxFileName fn = file;
if (fn.FileExists())
{
FilePath fullPath = fn.GetFullPath();
if (files.Index(fullPath) == wxNOT_FOUND)
{
files.push_back(fullPath);
}
}
}
for (auto file : files)
{
wxFileName fn = file;
mFiles.push_back(fn.GetFullPath());
mFileList->InsertItem(item, wxT(""));
mFileList->SetItem(item, 1, fn.GetName());
item++;
}
#if defined(USE_CHECKBOXES)
mFileList->SetColumnWidth(0, wxLIST_AUTOSIZE_USEHEADER);
mFileList->SetColumnWidth(1, wxLIST_AUTOSIZE);
#else
mFileList->SetColumnWidth(0, wxLIST_AUTOSIZE);
#endif
}
void AutoRecoveryDialog::OnQuitAudacity(wxCommandEvent & WXUNUSED(event))
@ -124,40 +181,41 @@ void AutoRecoveryDialog::OnQuitAudacity(wxCommandEvent & WXUNUSED(event))
EndModal(ID_QUIT_AUDACITY);
}
void AutoRecoveryDialog::OnRecoverNone(wxCommandEvent & WXUNUSED(event))
void AutoRecoveryDialog::OnDiscardSelected(wxCommandEvent & WXUNUSED(event))
{
int ret = AudacityMessageBox(
XO("Are you sure you want to discard all recoverable projects?\n\nChoosing \"Yes\" discards all recoverable projects immediately."),
XO("Are you sure you want to discard the selected projects?\n\n"
"Choosing \"Yes\" permanently deletes the selected projects immediately."),
XO("Confirm Discard Projects"),
wxICON_QUESTION | wxYES_NO | wxNO_DEFAULT, this);
if (ret == wxYES)
EndModal(ID_RECOVER_NONE);
}
void AutoRecoveryDialog::OnRecoverAll(wxCommandEvent & WXUNUSED(event))
{
EndModal(ID_RECOVER_ALL);
}
////////////////////////////////////////////////////////////////////////////
static FilePaths HaveFilesToRecover()
{
wxString tempdir = FileNames::TempDir();
wxString pattern = wxT("*.") + FileNames::UnsavedProjectExtension();
FilePaths files;
wxDir::GetAllFiles(tempdir, &files, pattern, wxDIR_FILES);
return files;
}
static bool RemoveAllAutoSaveFiles(const FilePaths &files)
{
for (int i = 0, cnt = files.size(); i < cnt; ++i)
if (ret == wxNO)
{
FilePath file = files[i];
return;
}
#define USE_CHECKBOXES
#if defined(USE_CHECKBOXES)
#define state wxLIST_STATE_DONTCARE
#else
#define state wxLIST_STATE_SELECTED
#endif
long item = -1;
while (true)
{
item = mFileList->GetNextItem(item, wxLIST_NEXT_ALL, state);
if (item == wxNOT_FOUND)
{
break;
}
#if defined(USE_CHECKBOXES)
if (!mFileList->IsItemChecked(item))
{
continue;
}
#endif
FilePath file = mFiles[item];
if (wxRemoveFile(file))
{
@ -175,12 +233,55 @@ static bool RemoveAllAutoSaveFiles(const FilePaths &files)
{
wxRemoveFile(file + wxT("-journal"));
}
ActiveProjects::Remove(file);
}
}
return true;
PopulateList();
if (mFileList->GetItemCount() == 0)
{
EndModal(ID_DISCARD_SELECTED);
}
}
void AutoRecoveryDialog::OnRecoverSelected(wxCommandEvent & WXUNUSED(event))
{
#define USE_CHECKBOXES
#if defined(USE_CHECKBOXES)
#define state wxLIST_STATE_DONTCARE
#else
#define state wxLIST_STATE_SELECTED
#endif
FilePaths files;
long item = -1;
while (true)
{
item = mFileList->GetNextItem(item, wxLIST_NEXT_ALL, state);
if (item == wxNOT_FOUND)
{
break;
}
#if defined(USE_CHECKBOXES)
if (!mFileList->IsItemChecked(item))
{
continue;
}
#endif
files.push_back(mFiles[item]);
}
mFiles = files;
EndModal(ID_RECOVER_SELECTED);
}
////////////////////////////////////////////////////////////////////////////
static bool RecoverAllProjects(const FilePaths &files,
AudacityProject **pproj)
{
@ -197,57 +298,69 @@ static bool RecoverAllProjects(const FilePaths &files,
*pproj = NULL;
}
// Open project. When an auto-save file has been opened successfully,
// the opened auto-save file is automatically deleted and a NEW one
// is created.
(void) ProjectManager::OpenProject(proj, files[i], false);
// Open project.
if (ProjectManager::OpenProject(proj, files[i], false) == nullptr)
{
return false;
}
ActiveProjects::Remove(files[i]);
}
return true;
}
bool ShowAutoRecoveryDialogIfNeeded(AudacityProject **pproj,
bool *didRecoverAnything)
bool ShowAutoRecoveryDialogIfNeeded(AudacityProject **pproj, bool *didRecoverAnything)
{
if (didRecoverAnything)
*didRecoverAnything = false;
FilePaths files = HaveFilesToRecover();
if (files.size())
{
// 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);
*didRecoverAnything = false;
}
int ret = AutoRecoveryDialog(files).ShowModal();
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;
if (dialog.HasRecoverables())
{
int ret = dialog.ShowModal();
switch (ret)
{
case ID_RECOVER_NONE:
return RemoveAllAutoSaveFiles(files);
case ID_DISCARD_SELECTED:
success = true;
break;
case ID_RECOVER_ALL:
if (didRecoverAnything)
*didRecoverAnything = true;
return RecoverAllProjects(files, pproj);
case ID_RECOVER_SELECTED:
success = RecoverAllProjects(dialog.GetRecoverables(), pproj);
if (success)
{
if (didRecoverAnything)
{
*didRecoverAnything = true;
}
}
break;
default:
// This includes ID_QUIT_AUDACITY
return false;
}
} else
{
// Nothing to recover, move along
return true;
}
return success;
}

View File

@ -63,6 +63,8 @@ list( APPEND SOURCES
PRIVATE
AColor.cpp
AColor.h
ActiveProjects.cpp
ActiveProjects.h
AboutDialog.cpp
AboutDialog.h
AdornedRulerPanel.cpp

View File

@ -18,6 +18,7 @@ Paul Licameli split from AudacityProject.cpp
#include <wx/sstream.h>
#include <wx/xml/xml.h>
#include "ActiveProjects.h"
#include "DBConnection.h"
#include "FileNames.h"
#include "Project.h"
@ -1178,8 +1179,18 @@ void ProjectFileIO::SetFileName(const FilePath &fileName)
{
auto &project = mProject;
if (!mFileName.empty())
{
ActiveProjects::Remove(mFileName);
}
mFileName = fileName;
if (!mFileName.empty())
{
ActiveProjects::Add(mFileName);
}
if (mTemporary)
{
project.SetProjectName({});

File diff suppressed because it is too large Load Diff

278
src/ProjectFileIO.h.bkp.h Normal file
View File

@ -0,0 +1,278 @@
/**********************************************************************
Audacity: A Digital Audio Editor
ProjectFileIO.h
Paul Licameli split from AudacityProject.h
**********************************************************************/
#ifndef __AUDACITY_PROJECT_FILE_IO__
#define __AUDACITY_PROJECT_FILE_IO__
#include <atomic>
#include <condition_variable>
#include <memory>
#include <mutex>
#include <thread>
#include <set>
#include "ClientData.h" // to inherit
#include "Prefs.h" // to inherit
#include "xml/XMLTagHandler.h" // to inherit
struct sqlite3;
struct sqlite3_context;
struct sqlite3_value;
class AudacityProject;
class AutoCommitTransaction;
class ProjectSerializer;
class SqliteSampleBlock;
class TrackList;
class WaveTrack;
using WaveTrackArray = std::vector < std::shared_ptr < WaveTrack > >;
// From SampleBlock.h
using SampleBlockID = long long;
///\brief Object associated with a project that manages reading and writing
/// of Audacity project file formats, and autosave
class ProjectFileIO final
: public ClientData::Base
, public XMLTagHandler
, private PrefsListener
, public std::enable_shared_from_this<ProjectFileIO>
{
public:
// Call this static function once before constructing any instances of this
// class. Reinvocations have no effect. Return value is true for success.
static bool InitializeSQL();
static ProjectFileIO &Get( AudacityProject &project );
static const ProjectFileIO &Get( const AudacityProject &project );
explicit ProjectFileIO( AudacityProject &project );
// unfortunate two-step construction needed because of
// enable_shared_from_this
void Init( AudacityProject &project );
ProjectFileIO( const ProjectFileIO & ) PROHIBITED;
ProjectFileIO &operator=( const ProjectFileIO & ) PROHIBITED;
~ProjectFileIO();
// It seems odd to put this method in this class, but the results do depend
// on what is discovered while opening the file, such as whether it is a
// recovery file
void SetProjectTitle(int number = -1);
// Should be empty or a fully qualified file name
const FilePath &GetFileName() const;
void SetFileName( const FilePath &fileName );
bool IsModified() const;
bool IsTemporary() const;
bool IsRecovered() const;
void Reset();
bool AutoSave(bool recording = false);
bool AutoSaveDelete(sqlite3 *db = nullptr);
bool ImportProject(const FilePath &fileName);
bool LoadProject(const FilePath &fileName);
bool SaveProject(const FilePath &fileName);
bool SaveCopy(const FilePath& fileName);
bool CloseProject();
wxLongLong GetFreeDiskSpace();
const TranslatableString &GetLastError() const;
const TranslatableString &GetLibraryError() const;
// Provides a means to bypass "DELETE"s at shutdown if the database
// is just going to be deleted anyway. This prevents a noticable
// delay caused by SampleBlocks being deleted when the Sequences that
// own them are deleted.
//
// This is definitely hackage territory. While this ability would
// still be needed, I think handling it in a DB abstraction might be
// a tad bit cleaner.
//
// For it's usage, see:
// SqliteSampleBlock::~SqliteSampleBlock()
// ProjectManager::OnCloseWindow()
void SetBypass();
bool ShouldBypass();
// Remove all unused space within a project file
void Vacuum(const std::shared_ptr<TrackList> &tracks);
// The last vacuum check did actually vacuum the project file if true
bool WasVacuumed();
// The last vacuum check found unused blocks in the project file
bool HadUnused();
bool TransactionStart(const wxString &name);
bool TransactionCommit(const wxString &name);
bool TransactionRollback(const wxString &name);
private:
void WriteXMLHeader(XMLWriter &xmlFile) const;
void WriteXML(XMLWriter &xmlFile, bool recording = false, const std::shared_ptr<TrackList> &tracks = nullptr) /* not override */;
// XMLTagHandler callback methods
bool HandleXMLTag(const wxChar *tag, const wxChar **attrs) override;
XMLTagHandler *HandleXMLChild(const wxChar *tag) override;
void UpdatePrefs() override;
using ExecResult = std::vector<std::vector<wxString>>;
using ExecCB = std::function<int(ExecResult &result, int cols, char **vals, char **names)>;
struct ExecParm
{
ExecCB func;
ExecResult &result;
};
static int ExecCallback(void *data, int cols, char **vals, char **names);
int Exec(const char *query, ExecCB callback, ExecResult &result);
// The opening of the database may be delayed until demanded.
// Returns a non-null pointer to an open database, or throws an exception
// if opening fails.
sqlite3 *DB();
// Put the current database connection aside, keeping it open, so that
// another may be opened with OpenDB()
void SaveConnection();
// Close any set-aside connection
void DiscardConnection();
// Close any current connection and switch back to using the saved
void RestoreConnection();
// Use a connection that is already open rather than invoke OpenDB
void UseConnection(sqlite3 *db, const FilePath &filePath);
// Make sure the connection/schema combo is configured the way we want
void Config(sqlite3 *db, const char *config, const wxString &schema = wxT("main"));
sqlite3 *OpenDB(FilePath fileName = {});
bool CloseDB();
bool DeleteDB();
bool Query(const char *sql, ExecResult &result);
bool GetValue(const char *sql, wxString &value);
bool GetBlob(const char *sql, wxMemoryBuffer &buffer);
bool CheckVersion();
bool InstallSchema(sqlite3 *db, const char *schema = "main");
bool UpgradeSchema();
// Write project or autosave XML (binary) documents
bool WriteDoc(const char *table, const ProjectSerializer &autosave, sqlite3 *db = nullptr);
// Application defined function to verify blockid exists is in set of blockids
using BlockIDs = std::set<SampleBlockID>;
static void InSet(sqlite3_context *context, int argc, sqlite3_value **argv);
// Checks for orphan blocks. This will go away at a future date
bool CheckForOrphans(BlockIDs &blockids);
// Return a database connection if successful, which caller must close
sqlite3 *CopyTo(const FilePath &destpath,
const TranslatableString &msg,
bool prune = false,
const std::shared_ptr<TrackList> &tracks = nullptr);
void SetError(const TranslatableString & msg);
void SetDBError(const TranslatableString & msg);
bool ShouldVacuum(const std::shared_ptr<TrackList> &tracks);
void CheckpointThread();
static int CheckpointHook(void *that, sqlite3 *db, const char *schema, int pages);
private:
// non-static data members
std::weak_ptr<AudacityProject> mpProject;
// The project's file path
FilePath mFileName;
// Has this project been recovered from an auto-saved version
bool mRecovered;
// Has this project been modified
bool mModified;
// Is this project still a temporary/unsaved project
bool mTemporary;
// Bypass transactions if database will be deleted after close
bool mBypass;
// Project was vacuumed last time Vacuum() ran
bool mWasVacuumed;
// Project had unused blocks during last Vacuum()
bool mHadUnused;
sqlite3 *mPrevDB;
FilePath mPrevFileName;
sqlite3 *mDB;
TranslatableString mLastError;
TranslatableString mLibraryError;
std::thread mCheckpointThread;
std::condition_variable mCheckpointCondition;
std::mutex mCheckpointMutex;
std::mutex mCheckpointActive;
std::mutex mCheckpointClose;
std::atomic_bool mCheckpointStop;
uint64_t mCheckpointWaitingPages;
uint64_t mCheckpointCurrentPages;
friend SqliteSampleBlock;
friend AutoCommitTransaction;
};
class AutoCommitTransaction
{
public:
AutoCommitTransaction(ProjectFileIO &projectFileIO, const char *name);
~AutoCommitTransaction();
bool Commit();
bool Rollback();
private:
ProjectFileIO &mIO;
bool mInTrans;
wxString mName;
};
class wxTopLevelWindow;
// TitleRestorer restores project window titles to what they were, in its destructor.
class TitleRestorer{
public:
TitleRestorer( wxTopLevelWindow &window, AudacityProject &project );
~TitleRestorer();
wxString sProjNumber;
wxString sProjName;
size_t UnnamedCount;
};
// This event is emitted by the project when there is a change
// in its title
wxDECLARE_EXPORTED_EVENT(AUDACITY_DLL_API,
EVT_PROJECT_TITLE_CHANGE, wxCommandEvent);
#endif