493 lines
13 KiB
C++
493 lines
13 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 "UndoManager.h"
|
|
|
|
#include <wx/hashset.h>
|
|
|
|
#include "Clipboard.h"
|
|
#include "DBConnection.h"
|
|
#include "Diags.h"
|
|
#include "Project.h"
|
|
#include "SampleBlock.h"
|
|
#include "Sequence.h"
|
|
#include "WaveTrack.h" // temp
|
|
//#include "NoteTrack.h" // for Sonify* function declarations
|
|
#include "Diags.h"
|
|
#include "Tags.h"
|
|
#include "widgets/ProgressDialog.h"
|
|
|
|
|
|
#include <unordered_set>
|
|
|
|
wxDEFINE_EVENT(EVT_UNDO_PUSHED, wxCommandEvent);
|
|
wxDEFINE_EVENT(EVT_UNDO_MODIFIED, wxCommandEvent);
|
|
wxDEFINE_EVENT(EVT_UNDO_RENAMED, wxCommandEvent);
|
|
wxDEFINE_EVENT(EVT_UNDO_OR_REDO, wxCommandEvent);
|
|
wxDEFINE_EVENT(EVT_UNDO_RESET, wxCommandEvent);
|
|
wxDEFINE_EVENT(EVT_UNDO_PURGE, wxCommandEvent);
|
|
|
|
using SampleBlockID = long long;
|
|
|
|
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;
|
|
}
|
|
|
|
UndoManager::~UndoManager()
|
|
{
|
|
wxASSERT( stack.empty() );
|
|
}
|
|
|
|
namespace {
|
|
SpaceArray::value_type
|
|
CalculateUsage(const TrackList &tracks, SampleBlockIDSet &seen)
|
|
{
|
|
SpaceArray::value_type result = 0;
|
|
//TIMER_START( "CalculateSpaceUsage", space_calc );
|
|
InspectBlocks(
|
|
tracks,
|
|
BlockSpaceUsageAccumulator( result ),
|
|
&seen
|
|
);
|
|
return result;
|
|
}
|
|
}
|
|
|
|
void UndoManager::CalculateSpaceUsage()
|
|
{
|
|
space.clear();
|
|
space.resize(stack.size(), 0);
|
|
|
|
SampleBlockIDSet 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);
|
|
}
|
|
|
|
// Count the usage of the clipboard separately, using another set. Do not
|
|
// multiple-count any block occurring multiple times within the clipboard.
|
|
seen.clear();
|
|
mClipboardSpaceUsage = CalculateUsage(
|
|
Clipboard::Get().GetTracks(), seen);
|
|
|
|
//TIMER_STOP( space_calc );
|
|
}
|
|
|
|
wxLongLong_t UndoManager::GetLongDescription(
|
|
unsigned int n, TranslatableString *desc, TranslatableString *size)
|
|
{
|
|
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, TranslatableString *desc)
|
|
{
|
|
wxASSERT(n < stack.size());
|
|
|
|
*desc = stack[n]->shortDescription;
|
|
}
|
|
|
|
void UndoManager::SetLongDescription(
|
|
unsigned int n, const TranslatableString &desc)
|
|
{
|
|
n -= 1;
|
|
|
|
wxASSERT(n < stack.size());
|
|
|
|
stack[n]->description = desc;
|
|
}
|
|
|
|
void UndoManager::RemoveStateAt(int n)
|
|
{
|
|
// Remove the state from the array first, and destroy it at function exit.
|
|
// Because in case of callbacks from destruction of Sample blocks, there
|
|
// might be a yield to GUI and other events might inspect the undo stack
|
|
// (such as history window update). Don't expose an inconsistent stack
|
|
// state.
|
|
auto iter = stack.begin() + n;
|
|
auto state = std::move(*iter);
|
|
stack.erase(iter);
|
|
}
|
|
|
|
|
|
//! Just to find a denominator for a progress indicator.
|
|
/*! This estimate procedure should in fact be exact */
|
|
size_t UndoManager::EstimateRemovedBlocks(size_t begin, size_t end)
|
|
{
|
|
if (begin == end)
|
|
return 0;
|
|
|
|
// Collect ids that survive
|
|
SampleBlockIDSet wontDelete;
|
|
auto f = [&](const auto &p){
|
|
InspectBlocks(*p->state.tracks, {}, &wontDelete);
|
|
};
|
|
auto first = stack.begin(), last = stack.end();
|
|
std::for_each( first, first + begin, f );
|
|
std::for_each( first + end, last, f );
|
|
if (saved >= 0)
|
|
std::for_each( first + saved, first + saved + 1, f );
|
|
InspectBlocks(TrackList::Get(mProject), {}, &wontDelete);
|
|
|
|
// Collect ids that won't survive (and are not negative pseudo ids)
|
|
SampleBlockIDSet seen, mayDelete;
|
|
std::for_each( first + begin, first + end, [&](const auto &p){
|
|
auto &tracks = *p->state.tracks;
|
|
InspectBlocks(tracks, [&]( const SampleBlock &block ){
|
|
auto id = block.GetBlockID();
|
|
if ( id > 0 && !wontDelete.count( id ) )
|
|
mayDelete.insert( id );
|
|
},
|
|
&seen);
|
|
} );
|
|
return mayDelete.size();
|
|
}
|
|
|
|
void UndoManager::RemoveStates(size_t begin, size_t end)
|
|
{
|
|
// Install a callback function that updates a progress indicator
|
|
unsigned long long nToDelete = EstimateRemovedBlocks(begin, end),
|
|
nDeleted = 0;
|
|
ProgressDialog dialog{ XO("Progress"), XO("Discarding undo/redo history"),
|
|
pdlgHideStopButton | pdlgHideCancelButton
|
|
};
|
|
auto callback = [&](const SampleBlock &){
|
|
dialog.Update(++nDeleted, nToDelete);
|
|
};
|
|
auto &trackFactory = WaveTrackFactory::Get( mProject );
|
|
auto &pSampleBlockFactory = trackFactory.GetSampleBlockFactory();
|
|
auto prevCallback =
|
|
pSampleBlockFactory->SetBlockDeletionCallback(callback);
|
|
auto cleanup = finally([&]{ pSampleBlockFactory->SetBlockDeletionCallback( prevCallback ); });
|
|
|
|
// Wrap the whole in a savepoint for better performance
|
|
Optional<TransactionScope> pTrans;
|
|
auto pConnection = ConnectionPtr::Get(mProject).mpConnection.get();
|
|
if (pConnection)
|
|
pTrans.emplace(*pConnection, "DiscardingUndoStates");
|
|
|
|
for (size_t ii = begin; ii < end; ++ii) {
|
|
RemoveStateAt(begin);
|
|
|
|
if (current > begin)
|
|
--current;
|
|
if (saved > static_cast<int>(begin))
|
|
--saved;
|
|
}
|
|
|
|
// Success, commit the savepoint
|
|
if (pTrans)
|
|
pTrans->Commit();
|
|
|
|
if (begin != end)
|
|
// wxWidgets will own the event object
|
|
mProject.QueueEvent( safenew wxCommandEvent{ EVT_UNDO_PURGE } );
|
|
|
|
// Check sanity
|
|
wxASSERT_MSG(
|
|
nDeleted == 0 || // maybe bypassing all deletions
|
|
nDeleted == nToDelete, "Block count was misestimated");
|
|
}
|
|
|
|
void UndoManager::ClearStates()
|
|
{
|
|
RemoveStates(0, stack.size());
|
|
current = -1;
|
|
saved = -1;
|
|
}
|
|
|
|
unsigned int UndoManager::GetNumStates()
|
|
{
|
|
return stack.size();
|
|
}
|
|
|
|
unsigned int UndoManager::GetCurrentState()
|
|
{
|
|
return current;
|
|
}
|
|
|
|
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( nullptr );
|
|
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::RenameState( int state,
|
|
const TranslatableString &longDescription,
|
|
const TranslatableString &shortDescription)
|
|
{
|
|
if (state >= 0 && state < stack.size() ) {
|
|
auto &theState = *stack[state];
|
|
theState.description = longDescription;
|
|
theState.shortDescription = shortDescription;
|
|
|
|
// wxWidgets will own the event object
|
|
mProject.QueueEvent( safenew wxCommandEvent{ EVT_UNDO_RENAMED } );
|
|
}
|
|
}
|
|
|
|
void UndoManager::PushState(const TrackList * l,
|
|
const SelectedRegion &selectedRegion,
|
|
const std::shared_ptr<Tags> &tags,
|
|
const TranslatableString &longDescription,
|
|
const TranslatableString &shortDescription,
|
|
UndoPush flags)
|
|
{
|
|
if ( (flags & UndoPush::CONSOLIDATE) != UndoPush::NONE &&
|
|
// compare full translations not msgids!
|
|
lastAction.Translation() == longDescription.Translation() &&
|
|
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( nullptr );
|
|
for (auto t : *l) {
|
|
if ( t->GetId() == TrackId{} )
|
|
// Don't copy a pending added track
|
|
continue;
|
|
tracksCopy->Add(t->Duplicate());
|
|
}
|
|
|
|
mayConsolidate = true;
|
|
|
|
AbandonRedo();
|
|
|
|
// Assume tags was duplicated 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++;
|
|
|
|
lastAction = longDescription;
|
|
|
|
// wxWidgets will own the event object
|
|
mProject.QueueEvent( safenew wxCommandEvent{ EVT_UNDO_PUSHED } );
|
|
}
|
|
|
|
void UndoManager::AbandonRedo()
|
|
{
|
|
if (saved > current) {
|
|
saved = -1;
|
|
}
|
|
RemoveStates( current + 1, stack.size() );
|
|
}
|
|
|
|
void UndoManager::SetStateTo(unsigned int n, const Consumer &consumer)
|
|
{
|
|
wxASSERT(n < stack.size());
|
|
|
|
current = n;
|
|
|
|
lastAction = {};
|
|
mayConsolidate = false;
|
|
|
|
consumer( *stack[current] );
|
|
|
|
// wxWidgets will own the event object
|
|
mProject.QueueEvent( safenew wxCommandEvent{ EVT_UNDO_RESET } );
|
|
}
|
|
|
|
void UndoManager::Undo(const Consumer &consumer)
|
|
{
|
|
wxASSERT(UndoAvailable());
|
|
|
|
current--;
|
|
|
|
lastAction = {};
|
|
mayConsolidate = false;
|
|
|
|
consumer( *stack[current] );
|
|
|
|
// 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 = {};
|
|
mayConsolidate = false;
|
|
|
|
consumer( *stack[current] );
|
|
|
|
// wxWidgets will own the event object
|
|
mProject.QueueEvent( safenew wxCommandEvent{ EVT_UNDO_OR_REDO } );
|
|
}
|
|
|
|
void UndoManager::VisitStates( const Consumer &consumer, bool newestFirst )
|
|
{
|
|
auto fn = [&]( decltype(stack[0]) &ptr ){ consumer( *ptr ); };
|
|
if (newestFirst)
|
|
std::for_each(stack.rbegin(), stack.rend(), fn);
|
|
else
|
|
std::for_each(stack.begin(), stack.end(), fn);
|
|
}
|
|
|
|
void UndoManager::VisitStates(
|
|
const Consumer &consumer, size_t begin, size_t end )
|
|
{
|
|
auto size = stack.size();
|
|
if (begin < end) {
|
|
end = std::min(end, size);
|
|
for (auto ii = begin; ii < end; ++ii)
|
|
consumer(*stack[ii]);
|
|
}
|
|
else {
|
|
if (size == 0)
|
|
return;
|
|
begin = std::min(begin, size - 1);
|
|
for (auto ii = begin; ii > end; --ii)
|
|
consumer(*stack[ii]);
|
|
}
|
|
}
|
|
|
|
bool UndoManager::UnsavedChanges() const
|
|
{
|
|
return (saved != current);
|
|
}
|
|
|
|
void UndoManager::StateSaved()
|
|
{
|
|
saved = current;
|
|
}
|
|
|
|
int UndoManager::GetSavedState() const
|
|
{
|
|
return saved;
|
|
}
|
|
|
|
// 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);
|
|
// }
|
|
//}
|
|
|