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