436 lines
10 KiB
C++
436 lines
10 KiB
C++
/**********************************************************************
|
|
|
|
Audacity: A Digital Audio Editor
|
|
|
|
UndoManager.cpp
|
|
|
|
Dominic Mazzoni
|
|
|
|
*******************************************************************//**
|
|
|
|
\class UndoManager
|
|
\brief Works with HistoryDialog to provide the Undo functionality.
|
|
|
|
*//****************************************************************//**
|
|
|
|
\class UndoStackElem
|
|
\brief Holds one item with description and time range for the
|
|
UndoManager
|
|
|
|
*//*******************************************************************/
|
|
|
|
|
|
#include "Audacity.h"
|
|
#include "UndoManager.h"
|
|
|
|
#include <wx/hashset.h>
|
|
|
|
#include "BlockFile.h"
|
|
#include "Clipboard.h"
|
|
#include "Diags.h"
|
|
#include "Project.h"
|
|
#include "Sequence.h"
|
|
#include "WaveClip.h"
|
|
#include "WaveTrack.h" // temp
|
|
#include "NoteTrack.h" // for Sonify* function declarations
|
|
#include "Diags.h"
|
|
#include "Tags.h"
|
|
|
|
|
|
#include <unordered_set>
|
|
|
|
wxDEFINE_EVENT(EVT_UNDO_PUSHED, wxCommandEvent);
|
|
wxDEFINE_EVENT(EVT_UNDO_MODIFIED, wxCommandEvent);
|
|
wxDEFINE_EVENT(EVT_UNDO_OR_REDO, wxCommandEvent);
|
|
wxDEFINE_EVENT(EVT_UNDO_RESET, wxCommandEvent);
|
|
|
|
using ConstBlockFilePtr = const BlockFile*;
|
|
using Set = std::unordered_set<ConstBlockFilePtr>;
|
|
|
|
struct UndoStackElem {
|
|
|
|
UndoStackElem(std::shared_ptr<TrackList> &&tracks_,
|
|
const wxString &description_,
|
|
const wxString &shortDescription_,
|
|
const SelectedRegion &selectedRegion_,
|
|
const std::shared_ptr<Tags> &tags_)
|
|
: state(std::move(tracks_), tags_, selectedRegion_)
|
|
, description(description_)
|
|
, shortDescription(shortDescription_)
|
|
{
|
|
}
|
|
|
|
UndoState state;
|
|
wxString description;
|
|
wxString shortDescription;
|
|
};
|
|
|
|
static const AudacityProject::AttachedObjects::RegisteredFactory key{
|
|
[](AudacityProject &project)
|
|
{ return std::make_unique<UndoManager>( project ); }
|
|
};
|
|
|
|
UndoManager &UndoManager::Get( AudacityProject &project )
|
|
{
|
|
return project.AttachedObjects::Get< UndoManager >( key );
|
|
}
|
|
|
|
const UndoManager &UndoManager::Get( const AudacityProject &project )
|
|
{
|
|
return Get( const_cast< AudacityProject & >( project ) );
|
|
}
|
|
|
|
UndoManager::UndoManager( AudacityProject &project )
|
|
: mProject{ project }
|
|
{
|
|
current = -1;
|
|
saved = -1;
|
|
ResetODChangesFlag();
|
|
}
|
|
|
|
UndoManager::~UndoManager()
|
|
{
|
|
ClearStates();
|
|
}
|
|
|
|
namespace {
|
|
SpaceArray::value_type
|
|
CalculateUsage(const TrackList &tracks, Set *seen)
|
|
{
|
|
SpaceArray::value_type result = 0;
|
|
|
|
//TIMER_START( "CalculateSpaceUsage", space_calc );
|
|
for (auto wt : tracks.Any< const WaveTrack >())
|
|
{
|
|
// Scan all clips within current track
|
|
for(const auto &clip : wt->GetAllClips())
|
|
{
|
|
// Scan all blockfiles within current clip
|
|
auto blocks = clip->GetSequenceBlockArray();
|
|
for (const auto &block : *blocks)
|
|
{
|
|
const auto &file = block.f;
|
|
|
|
// Accumulate space used by the file if the file was not
|
|
// yet seen
|
|
if ( !seen || (seen->count( &*file ) == 0 ) )
|
|
{
|
|
unsigned long long usage{ file->GetSpaceUsage() };
|
|
result += usage;
|
|
}
|
|
|
|
// Add file to current set
|
|
if (seen)
|
|
seen->insert( &*file );
|
|
}
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
}
|
|
|
|
void UndoManager::CalculateSpaceUsage()
|
|
{
|
|
space.clear();
|
|
space.resize(stack.size(), 0);
|
|
|
|
Set seen;
|
|
|
|
// After copies and pastes, a block file may be used in more than
|
|
// one place in one undo history state, and it may be used in more than
|
|
// one undo history state. It might even be used in two states, but not
|
|
// in another state that is between them -- as when you have state A,
|
|
// then make a cut to get state B, but then paste it back into state C.
|
|
|
|
// So be sure to count each block file once only, in the last undo item that
|
|
// contains it.
|
|
|
|
// Why the last and not the first? Because the user of the History dialog
|
|
// may DELETE undo states, oldest first. To reclaim disk space you must
|
|
// DELETE all states containing the block file. So the block file's
|
|
// contribution to space usage should be counted only in that latest state.
|
|
|
|
for (size_t nn = stack.size(); nn--;)
|
|
{
|
|
// Scan all tracks at current level
|
|
auto &tracks = *stack[nn]->state.tracks;
|
|
space[nn] = CalculateUsage(tracks, &seen);
|
|
}
|
|
|
|
mClipboardSpaceUsage = CalculateUsage(
|
|
Clipboard::Get().GetTracks(), nullptr);
|
|
|
|
//TIMER_STOP( space_calc );
|
|
}
|
|
|
|
wxLongLong_t UndoManager::GetLongDescription(unsigned int n, wxString *desc,
|
|
wxString *size)
|
|
{
|
|
n -= 1; // 1 based to zero based
|
|
|
|
wxASSERT(n < stack.size());
|
|
wxASSERT(space.size() == stack.size());
|
|
|
|
*desc = stack[n]->description;
|
|
|
|
*size = Internat::FormatSize(space[n]);
|
|
|
|
return space[n];
|
|
}
|
|
|
|
void UndoManager::GetShortDescription(unsigned int n, wxString *desc)
|
|
{
|
|
n -= 1; // 1 based to zero based
|
|
|
|
wxASSERT(n < stack.size());
|
|
|
|
*desc = stack[n]->shortDescription;
|
|
}
|
|
|
|
void UndoManager::SetLongDescription(unsigned int n, const wxString &desc)
|
|
{
|
|
n -= 1;
|
|
|
|
wxASSERT(n < stack.size());
|
|
|
|
stack[n]->description = desc;
|
|
}
|
|
|
|
void UndoManager::RemoveStateAt(int n)
|
|
{
|
|
stack.erase(stack.begin() + n);
|
|
}
|
|
|
|
|
|
void UndoManager::RemoveStates(int num)
|
|
{
|
|
for (int i = 0; i < num; i++) {
|
|
RemoveStateAt(0);
|
|
|
|
current -= 1;
|
|
saved -= 1;
|
|
}
|
|
}
|
|
|
|
void UndoManager::ClearStates()
|
|
{
|
|
RemoveStates(stack.size());
|
|
current = -1;
|
|
saved = -1;
|
|
}
|
|
|
|
unsigned int UndoManager::GetNumStates()
|
|
{
|
|
return stack.size();
|
|
}
|
|
|
|
unsigned int UndoManager::GetCurrentState()
|
|
{
|
|
return current + 1; // the array is 0 based, the abstraction is 1 based
|
|
}
|
|
|
|
bool UndoManager::UndoAvailable()
|
|
{
|
|
return (current > 0);
|
|
}
|
|
|
|
bool UndoManager::RedoAvailable()
|
|
{
|
|
return (current < (int)stack.size() - 1);
|
|
}
|
|
|
|
void UndoManager::ModifyState(const TrackList * l,
|
|
const SelectedRegion &selectedRegion,
|
|
const std::shared_ptr<Tags> &tags)
|
|
{
|
|
if (current == wxNOT_FOUND) {
|
|
return;
|
|
}
|
|
|
|
SonifyBeginModifyState();
|
|
// Delete current -- not necessary, but let's reclaim space early
|
|
stack[current]->state.tracks.reset();
|
|
|
|
// Duplicate
|
|
auto tracksCopy = TrackList::Create();
|
|
for (auto t : *l) {
|
|
if ( t->GetId() == TrackId{} )
|
|
// Don't copy a pending added track
|
|
continue;
|
|
tracksCopy->Add(t->Duplicate());
|
|
}
|
|
|
|
// Replace
|
|
stack[current]->state.tracks = std::move(tracksCopy);
|
|
stack[current]->state.tags = tags;
|
|
|
|
stack[current]->state.selectedRegion = selectedRegion;
|
|
SonifyEndModifyState();
|
|
|
|
// wxWidgets will own the event object
|
|
mProject.QueueEvent( safenew wxCommandEvent{ EVT_UNDO_MODIFIED } );
|
|
}
|
|
|
|
void UndoManager::PushState(const TrackList * l,
|
|
const SelectedRegion &selectedRegion,
|
|
const std::shared_ptr<Tags> &tags,
|
|
const wxString &longDescription,
|
|
const wxString &shortDescription,
|
|
UndoPush flags)
|
|
{
|
|
unsigned int i;
|
|
|
|
if ( ((flags & UndoPush::CONSOLIDATE) != UndoPush::MINIMAL) &&
|
|
lastAction == longDescription &&
|
|
mayConsolidate ) {
|
|
ModifyState(l, selectedRegion, tags);
|
|
// MB: If the "saved" state was modified by ModifyState, reset
|
|
// it so that UnsavedChanges returns true.
|
|
if (current == saved) {
|
|
saved = -1;
|
|
}
|
|
return;
|
|
}
|
|
|
|
auto tracksCopy = TrackList::Create();
|
|
for (auto t : *l) {
|
|
if ( t->GetId() == TrackId{} )
|
|
// Don't copy a pending added track
|
|
continue;
|
|
tracksCopy->Add(t->Duplicate());
|
|
}
|
|
|
|
mayConsolidate = true;
|
|
|
|
i = current + 1;
|
|
while (i < stack.size()) {
|
|
RemoveStateAt(i);
|
|
}
|
|
|
|
// Assume tags was duplicted before any changes.
|
|
// Just save a NEW shared_ptr to it.
|
|
stack.push_back(
|
|
std::make_unique<UndoStackElem>
|
|
(std::move(tracksCopy),
|
|
longDescription, shortDescription, selectedRegion, tags)
|
|
);
|
|
|
|
current++;
|
|
|
|
if (saved >= current) {
|
|
saved = -1;
|
|
}
|
|
|
|
lastAction = longDescription;
|
|
|
|
// wxWidgets will own the event object
|
|
mProject.QueueEvent( safenew wxCommandEvent{ EVT_UNDO_PUSHED } );
|
|
}
|
|
|
|
void UndoManager::SetStateTo(unsigned int n, const Consumer &consumer)
|
|
{
|
|
n -= 1;
|
|
|
|
wxASSERT(n < stack.size());
|
|
|
|
current = n;
|
|
|
|
lastAction = wxT("");
|
|
mayConsolidate = false;
|
|
|
|
consumer( stack[current]->state );
|
|
|
|
// wxWidgets will own the event object
|
|
mProject.QueueEvent( safenew wxCommandEvent{ EVT_UNDO_RESET } );
|
|
}
|
|
|
|
void UndoManager::Undo(const Consumer &consumer)
|
|
{
|
|
wxASSERT(UndoAvailable());
|
|
|
|
current--;
|
|
|
|
lastAction = wxT("");
|
|
mayConsolidate = false;
|
|
|
|
consumer( stack[current]->state );
|
|
|
|
// wxWidgets will own the event object
|
|
mProject.QueueEvent( safenew wxCommandEvent{ EVT_UNDO_OR_REDO } );
|
|
}
|
|
|
|
void UndoManager::Redo(const Consumer &consumer)
|
|
{
|
|
wxASSERT(RedoAvailable());
|
|
|
|
current++;
|
|
|
|
/*
|
|
if (!RedoAvailable()) {
|
|
*sel0 = stack[current]->sel0;
|
|
*sel1 = stack[current]->sel1;
|
|
}
|
|
else {
|
|
current++;
|
|
*sel0 = stack[current]->sel0;
|
|
*sel1 = stack[current]->sel1;
|
|
current--;
|
|
}
|
|
*/
|
|
|
|
lastAction = wxT("");
|
|
mayConsolidate = false;
|
|
|
|
consumer( stack[current]->state );
|
|
|
|
// wxWidgets will own the event object
|
|
mProject.QueueEvent( safenew wxCommandEvent{ EVT_UNDO_OR_REDO } );
|
|
}
|
|
|
|
bool UndoManager::UnsavedChanges() const
|
|
{
|
|
return (saved != current) || HasODChangesFlag();
|
|
}
|
|
|
|
void UndoManager::StateSaved()
|
|
{
|
|
saved = current;
|
|
ResetODChangesFlag();
|
|
}
|
|
|
|
// currently unused
|
|
//void UndoManager::Debug()
|
|
//{
|
|
// for (unsigned int i = 0; i < stack.size(); i++) {
|
|
// for (auto t : stack[i]->tracks->Any())
|
|
// wxPrintf(wxT("*%d* %s %f\n"),
|
|
// i, (i == (unsigned int)current) ? wxT("-->") : wxT(" "),
|
|
// t ? t->GetEndTime()-t->GetStartTime() : 0);
|
|
// }
|
|
//}
|
|
|
|
///to mark as unsaved changes without changing the state/tracks.
|
|
void UndoManager::SetODChangesFlag()
|
|
{
|
|
mODChangesMutex.Lock();
|
|
mODChanges=true;
|
|
mODChangesMutex.Unlock();
|
|
}
|
|
|
|
bool UndoManager::HasODChangesFlag() const
|
|
{
|
|
bool ret;
|
|
mODChangesMutex.Lock();
|
|
ret=mODChanges;
|
|
mODChangesMutex.Unlock();
|
|
return ret;
|
|
}
|
|
|
|
void UndoManager::ResetODChangesFlag()
|
|
{
|
|
mODChangesMutex.Lock();
|
|
mODChanges=false;
|
|
mODChangesMutex.Unlock();
|
|
}
|