/********************************************************************** 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 #include "Clipboard.h" #include "DBConnection.h" #include "Diags.h" #include "Project.h" #include "SampleBlock.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 "widgets/ProgressDialog.h" #include 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 SampleBlockID = long long; struct UndoStackElem { UndoStackElem(std::shared_ptr &&tracks_, const TranslatableString &description_, const TranslatableString &shortDescription_, const SelectedRegion &selectedRegion_, const std::shared_ptr &tags_) : state(std::move(tracks_), tags_, selectedRegion_) , description(description_) , shortDescription(shortDescription_) { } UndoState state; TranslatableString description; TranslatableString shortDescription; }; static const AudacityProject::AttachedObjects::RegisteredFactory key{ [](AudacityProject &project) { return std::make_unique( 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) { stack.erase(stack.begin() + n); } //! 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) { // 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 ); // 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 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 > begin) --saved; } // Success, commit the savepoint if (pTrans) pTrans->Commit(); // 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) { 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::PushState(const TrackList * l, const SelectedRegion &selectedRegion, const std::shared_ptr &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; // Abandon redo states if (saved >= current) { saved = -1; } RemoveStates( current + 1, stack.size() ); // Assume tags was duplicated before any changes. // Just save a NEW shared_ptr to it. stack.push_back( std::make_unique (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::SetStateTo(unsigned int n, const Consumer &consumer) { wxASSERT(n < stack.size()); current = n; lastAction = {}; 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 = {}; 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 = {}; 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); } void UndoManager::StateSaved() { saved = current; } // 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); // } //}