1 /**********************************************************************
2 
3   Audacity: A Digital Audio Editor
4 
5   UndoManager.cpp
6 
7   Dominic Mazzoni
8 
9 *******************************************************************//**
10 
11 \class UndoManager
12 \brief Works with HistoryDialog to provide the Undo functionality.
13 
14 *//****************************************************************//**
15 
16 \class UndoStackElem
17 \brief Holds one item with description and time range for the
18 UndoManager
19 
20 *//*******************************************************************/
21 
22 
23 
24 #include "UndoManager.h"
25 
26 #include <wx/hashset.h>
27 
28 #include "Clipboard.h"
29 #include "DBConnection.h"
30 #include "Diags.h"
31 #include "Project.h"
32 #include "SampleBlock.h"
33 #include "Sequence.h"
34 #include "WaveTrack.h"          // temp
35 //#include "NoteTrack.h"  // for Sonify* function declarations
36 #include "Diags.h"
37 #include "Tags.h"
38 #include "widgets/ProgressDialog.h"
39 
40 
41 #include <unordered_set>
42 
43 wxDEFINE_EVENT(EVT_UNDO_PUSHED, wxCommandEvent);
44 wxDEFINE_EVENT(EVT_UNDO_MODIFIED, wxCommandEvent);
45 wxDEFINE_EVENT(EVT_UNDO_RENAMED, wxCommandEvent);
46 wxDEFINE_EVENT(EVT_UNDO_OR_REDO, wxCommandEvent);
47 wxDEFINE_EVENT(EVT_UNDO_RESET, wxCommandEvent);
48 wxDEFINE_EVENT(EVT_UNDO_PURGE, wxCommandEvent);
49 
50 using SampleBlockID = long long;
51 
52 static const AudacityProject::AttachedObjects::RegisteredFactory key{
53    [](AudacityProject &project)
__anon78b219cb0102() 54       { return std::make_unique<UndoManager>( project ); }
55 };
56 
Get(AudacityProject & project)57 UndoManager &UndoManager::Get( AudacityProject &project )
58 {
59    return project.AttachedObjects::Get< UndoManager >( key );
60 }
61 
Get(const AudacityProject & project)62 const UndoManager &UndoManager::Get( const AudacityProject &project )
63 {
64    return Get( const_cast< AudacityProject & >( project ) );
65 }
66 
UndoManager(AudacityProject & project)67 UndoManager::UndoManager( AudacityProject &project )
68    : mProject{ project }
69 {
70    current = -1;
71    saved = -1;
72 }
73 
~UndoManager()74 UndoManager::~UndoManager()
75 {
76    wxASSERT( stack.empty() );
77 }
78 
79 namespace {
80    SpaceArray::value_type
CalculateUsage(const TrackList & tracks,SampleBlockIDSet & seen)81    CalculateUsage(const TrackList &tracks, SampleBlockIDSet &seen)
82    {
83       SpaceArray::value_type result = 0;
84       //TIMER_START( "CalculateSpaceUsage", space_calc );
85       InspectBlocks(
86          tracks,
87          BlockSpaceUsageAccumulator( result ),
88          &seen
89       );
90       return result;
91    }
92 }
93 
CalculateSpaceUsage()94 void UndoManager::CalculateSpaceUsage()
95 {
96    space.clear();
97    space.resize(stack.size(), 0);
98 
99    SampleBlockIDSet seen;
100 
101    // After copies and pastes, a block file may be used in more than
102    // one place in one undo history state, and it may be used in more than
103    // one undo history state.  It might even be used in two states, but not
104    // in another state that is between them -- as when you have state A,
105    // then make a cut to get state B, but then paste it back into state C.
106 
107    // So be sure to count each block file once only, in the last undo item that
108    // contains it.
109 
110    // Why the last and not the first? Because the user of the History dialog
111    // may DELETE undo states, oldest first.  To reclaim disk space you must
112    // DELETE all states containing the block file.  So the block file's
113    // contribution to space usage should be counted only in that latest state.
114 
115    for (size_t nn = stack.size(); nn--;)
116    {
117       // Scan all tracks at current level
118       auto &tracks = *stack[nn]->state.tracks;
119       space[nn] = CalculateUsage(tracks, seen);
120    }
121 
122    // Count the usage of the clipboard separately, using another set.  Do not
123    // multiple-count any block occurring multiple times within the clipboard.
124    seen.clear();
125    mClipboardSpaceUsage = CalculateUsage(
126       Clipboard::Get().GetTracks(), seen);
127 
128    //TIMER_STOP( space_calc );
129 }
130 
GetLongDescription(unsigned int n,TranslatableString * desc,TranslatableString * size)131 wxLongLong_t UndoManager::GetLongDescription(
132    unsigned int n, TranslatableString *desc, TranslatableString *size)
133 {
134    wxASSERT(n < stack.size());
135    wxASSERT(space.size() == stack.size());
136 
137    *desc = stack[n]->description;
138 
139    *size = Internat::FormatSize(space[n]);
140 
141    return space[n];
142 }
143 
GetShortDescription(unsigned int n,TranslatableString * desc)144 void UndoManager::GetShortDescription(unsigned int n, TranslatableString *desc)
145 {
146    wxASSERT(n < stack.size());
147 
148    *desc = stack[n]->shortDescription;
149 }
150 
SetLongDescription(unsigned int n,const TranslatableString & desc)151 void UndoManager::SetLongDescription(
152   unsigned int n, const TranslatableString &desc)
153 {
154    n -= 1;
155 
156    wxASSERT(n < stack.size());
157 
158    stack[n]->description = desc;
159 }
160 
RemoveStateAt(int n)161 void UndoManager::RemoveStateAt(int n)
162 {
163    // Remove the state from the array first, and destroy it at function exit.
164    // Because in case of callbacks from destruction of Sample blocks, there
165    // might be a yield to GUI and other events might inspect the undo stack
166    // (such as history window update).  Don't expose an inconsistent stack
167    // state.
168    auto iter = stack.begin() + n;
169    auto state = std::move(*iter);
170    stack.erase(iter);
171 }
172 
173 
174 //! Just to find a denominator for a progress indicator.
175 /*! This estimate procedure should in fact be exact */
EstimateRemovedBlocks(size_t begin,size_t end)176 size_t UndoManager::EstimateRemovedBlocks(size_t begin, size_t end)
177 {
178    if (begin == end)
179       return 0;
180 
181    // Collect ids that survive
182    SampleBlockIDSet wontDelete;
183    auto f = [&](const auto &p){
184       InspectBlocks(*p->state.tracks, {}, &wontDelete);
185    };
186    auto first = stack.begin(), last = stack.end();
187    std::for_each( first, first + begin, f );
188    std::for_each( first + end, last, f );
189    if (saved >= 0)
190       std::for_each( first + saved, first + saved + 1, f );
191    InspectBlocks(TrackList::Get(mProject), {}, &wontDelete);
192 
193    // Collect ids that won't survive (and are not negative pseudo ids)
194    SampleBlockIDSet seen, mayDelete;
195    std::for_each( first + begin, first + end, [&](const auto &p){
196       auto &tracks = *p->state.tracks;
197       InspectBlocks(tracks, [&]( const SampleBlock &block ){
198          auto id = block.GetBlockID();
199          if ( id > 0 && !wontDelete.count( id ) )
200             mayDelete.insert( id );
201       },
202       &seen);
203    } );
204    return mayDelete.size();
205 }
206 
RemoveStates(size_t begin,size_t end)207 void UndoManager::RemoveStates(size_t begin, size_t end)
208 {
209    // Install a callback function that updates a progress indicator
210    unsigned long long nToDelete = EstimateRemovedBlocks(begin, end),
211       nDeleted = 0;
212    ProgressDialog dialog{ XO("Progress"), XO("Discarding undo/redo history"),
213       pdlgHideStopButton | pdlgHideCancelButton
214    };
215    auto callback = [&](const SampleBlock &){
216       dialog.Update(++nDeleted, nToDelete);
217    };
218    auto &trackFactory = WaveTrackFactory::Get( mProject );
219    auto &pSampleBlockFactory = trackFactory.GetSampleBlockFactory();
220    auto prevCallback =
221       pSampleBlockFactory->SetBlockDeletionCallback(callback);
222    auto cleanup = finally([&]{ pSampleBlockFactory->SetBlockDeletionCallback( prevCallback ); });
223 
224    // Wrap the whole in a savepoint for better performance
225    Optional<TransactionScope> pTrans;
226    auto pConnection = ConnectionPtr::Get(mProject).mpConnection.get();
227    if (pConnection)
228       pTrans.emplace(*pConnection, "DiscardingUndoStates");
229 
230    for (size_t ii = begin; ii < end; ++ii) {
231       RemoveStateAt(begin);
232 
233       if (current > begin)
234         --current;
235       if (saved > static_cast<int>(begin))
236         --saved;
237    }
238 
239    // Success, commit the savepoint
240    if (pTrans)
241       pTrans->Commit();
242 
243    if (begin != end)
244       // wxWidgets will own the event object
245       mProject.QueueEvent( safenew wxCommandEvent{ EVT_UNDO_PURGE } );
246 
247    // Check sanity
248    wxASSERT_MSG(
249       nDeleted == 0 || // maybe bypassing all deletions
250       nDeleted == nToDelete, "Block count was misestimated");
251 }
252 
ClearStates()253 void UndoManager::ClearStates()
254 {
255    RemoveStates(0, stack.size());
256    current = -1;
257    saved = -1;
258 }
259 
GetNumStates()260 unsigned int UndoManager::GetNumStates()
261 {
262    return stack.size();
263 }
264 
GetCurrentState()265 unsigned int UndoManager::GetCurrentState()
266 {
267    return current;
268 }
269 
UndoAvailable()270 bool UndoManager::UndoAvailable()
271 {
272    return (current > 0);
273 }
274 
RedoAvailable()275 bool UndoManager::RedoAvailable()
276 {
277    return (current < (int)stack.size() - 1);
278 }
279 
ModifyState(const TrackList * l,const SelectedRegion & selectedRegion,const std::shared_ptr<Tags> & tags)280 void UndoManager::ModifyState(const TrackList * l,
281                               const SelectedRegion &selectedRegion,
282                               const std::shared_ptr<Tags> &tags)
283 {
284    if (current == wxNOT_FOUND) {
285       return;
286    }
287 
288 //   SonifyBeginModifyState();
289    // Delete current -- not necessary, but let's reclaim space early
290    stack[current]->state.tracks.reset();
291 
292    // Duplicate
293    auto tracksCopy = TrackList::Create( nullptr );
294    for (auto t : *l) {
295       if ( t->GetId() == TrackId{} )
296          // Don't copy a pending added track
297          continue;
298       tracksCopy->Add(t->Duplicate());
299    }
300 
301    // Replace
302    stack[current]->state.tracks = std::move(tracksCopy);
303    stack[current]->state.tags = tags;
304 
305    stack[current]->state.selectedRegion = selectedRegion;
306 //   SonifyEndModifyState();
307 
308    // wxWidgets will own the event object
309    mProject.QueueEvent( safenew wxCommandEvent{ EVT_UNDO_MODIFIED } );
310 }
311 
RenameState(int state,const TranslatableString & longDescription,const TranslatableString & shortDescription)312 void UndoManager::RenameState( int state,
313    const TranslatableString &longDescription,
314    const TranslatableString &shortDescription)
315 {
316    if (state >= 0 && state < stack.size() ) {
317       auto &theState = *stack[state];
318       theState.description = longDescription;
319       theState.shortDescription = shortDescription;
320 
321       // wxWidgets will own the event object
322       mProject.QueueEvent( safenew wxCommandEvent{ EVT_UNDO_RENAMED } );
323    }
324 }
325 
PushState(const TrackList * l,const SelectedRegion & selectedRegion,const std::shared_ptr<Tags> & tags,const TranslatableString & longDescription,const TranslatableString & shortDescription,UndoPush flags)326 void UndoManager::PushState(const TrackList * l,
327                             const SelectedRegion &selectedRegion,
328                             const std::shared_ptr<Tags> &tags,
329                             const TranslatableString &longDescription,
330                             const TranslatableString &shortDescription,
331                             UndoPush flags)
332 {
333    if ( (flags & UndoPush::CONSOLIDATE) != UndoPush::NONE &&
334        // compare full translations not msgids!
335        lastAction.Translation() == longDescription.Translation() &&
336        mayConsolidate ) {
337       ModifyState(l, selectedRegion, tags);
338       // MB: If the "saved" state was modified by ModifyState, reset
339       //  it so that UnsavedChanges returns true.
340       if (current == saved) {
341          saved = -1;
342       }
343       return;
344    }
345 
346    auto tracksCopy = TrackList::Create( nullptr );
347    for (auto t : *l) {
348       if ( t->GetId() == TrackId{} )
349          // Don't copy a pending added track
350          continue;
351       tracksCopy->Add(t->Duplicate());
352    }
353 
354    mayConsolidate = true;
355 
356    AbandonRedo();
357 
358    // Assume tags was duplicated before any changes.
359    // Just save a NEW shared_ptr to it.
360    stack.push_back(
361       std::make_unique<UndoStackElem>
362          (std::move(tracksCopy),
363             longDescription, shortDescription, selectedRegion, tags)
364    );
365 
366    current++;
367 
368    lastAction = longDescription;
369 
370    // wxWidgets will own the event object
371    mProject.QueueEvent( safenew wxCommandEvent{ EVT_UNDO_PUSHED } );
372 }
373 
AbandonRedo()374 void UndoManager::AbandonRedo()
375 {
376    if (saved > current) {
377       saved = -1;
378    }
379    RemoveStates( current + 1, stack.size() );
380 }
381 
SetStateTo(unsigned int n,const Consumer & consumer)382 void UndoManager::SetStateTo(unsigned int n, const Consumer &consumer)
383 {
384    wxASSERT(n < stack.size());
385 
386    current = n;
387 
388    lastAction = {};
389    mayConsolidate = false;
390 
391    consumer( *stack[current] );
392 
393    // wxWidgets will own the event object
394    mProject.QueueEvent( safenew wxCommandEvent{ EVT_UNDO_RESET } );
395 }
396 
Undo(const Consumer & consumer)397 void UndoManager::Undo(const Consumer &consumer)
398 {
399    wxASSERT(UndoAvailable());
400 
401    current--;
402 
403    lastAction = {};
404    mayConsolidate = false;
405 
406    consumer( *stack[current] );
407 
408    // wxWidgets will own the event object
409    mProject.QueueEvent( safenew wxCommandEvent{ EVT_UNDO_OR_REDO } );
410 }
411 
Redo(const Consumer & consumer)412 void UndoManager::Redo(const Consumer &consumer)
413 {
414    wxASSERT(RedoAvailable());
415 
416    current++;
417 
418    /*
419    if (!RedoAvailable()) {
420       *sel0 = stack[current]->sel0;
421       *sel1 = stack[current]->sel1;
422    }
423    else {
424       current++;
425       *sel0 = stack[current]->sel0;
426       *sel1 = stack[current]->sel1;
427       current--;
428    }
429    */
430 
431    lastAction = {};
432    mayConsolidate = false;
433 
434    consumer( *stack[current] );
435 
436    // wxWidgets will own the event object
437    mProject.QueueEvent( safenew wxCommandEvent{ EVT_UNDO_OR_REDO } );
438 }
439 
VisitStates(const Consumer & consumer,bool newestFirst)440 void UndoManager::VisitStates( const Consumer &consumer, bool newestFirst )
441 {
442    auto fn = [&]( decltype(stack[0]) &ptr ){ consumer( *ptr ); };
443    if (newestFirst)
444       std::for_each(stack.rbegin(), stack.rend(), fn);
445    else
446       std::for_each(stack.begin(), stack.end(), fn);
447 }
448 
VisitStates(const Consumer & consumer,size_t begin,size_t end)449 void UndoManager::VisitStates(
450    const Consumer &consumer, size_t begin, size_t end )
451 {
452    auto size = stack.size();
453    if (begin < end) {
454       end = std::min(end, size);
455       for (auto ii = begin; ii < end; ++ii)
456          consumer(*stack[ii]);
457    }
458    else {
459       if (size == 0)
460          return;
461       begin = std::min(begin, size - 1);
462       for (auto ii = begin; ii > end; --ii)
463          consumer(*stack[ii]);
464    }
465 }
466 
UnsavedChanges() const467 bool UndoManager::UnsavedChanges() const
468 {
469    return (saved != current);
470 }
471 
StateSaved()472 void UndoManager::StateSaved()
473 {
474    saved = current;
475 }
476 
GetSavedState() const477 int UndoManager::GetSavedState() const
478 {
479    return saved;
480 }
481 
482 // currently unused
483 //void UndoManager::Debug()
484 //{
485 //   for (unsigned int i = 0; i < stack.size(); i++) {
486 //      for (auto t : stack[i]->tracks->Any())
487 //         wxPrintf(wxT("*%d* %s %f\n"),
488 //                  i, (i == (unsigned int)current) ? wxT("-->") : wxT("   "),
489 //                t ? t->GetEndTime()-t->GetStartTime() : 0);
490 //   }
491 //}
492 
493