Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Batch undo #6410

Open
wants to merge 20 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 18 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions include/ClipView.h
Original file line number Diff line number Diff line change
Expand Up @@ -199,8 +199,7 @@ protected slots:
Resize,
ResizeLeft,
Split,
CopySelection,
ToggleSelected
CopyOrToggleSelect, // depends on drag vs click
} ;

static TextFloat * s_textFloat;
Expand Down
9 changes: 8 additions & 1 deletion include/ProjectJournal.h
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@ class ProjectJournal
bool canRedo() const;

void addJournalCheckPoint( JournallingObject *jo );
void beginBatchCheckPoint(bool append = false);
void endBatchCheckPoint();

bool isJournalling() const
{
Expand Down Expand Up @@ -112,13 +114,18 @@ class ProjectJournal
jo_id_t joID;
DataFile data;
} ;
typedef QStack<CheckPoint> CheckPointStack;
typedef std::vector<CheckPoint> BatchCheckPoint;
typedef std::vector<BatchCheckPoint> CheckPointStack;

void restoreCheckPoint(ProjectJournal::CheckPointStack& restore, ProjectJournal::CheckPointStack& backup);

JoIdMap m_joIDs;

CheckPointStack m_undoCheckPoints;
CheckPointStack m_redoCheckPoints;

//! Used to determine if checkpoints should be batched or not
int m_batchingCount = 0;
bool m_journalling;

} ;
Expand Down
7 changes: 4 additions & 3 deletions src/core/AutomatableModel.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -296,14 +296,15 @@ void AutomatableModel::setValue( const float value )
{
m_oldValue = m_value;
++m_setValueDepth;
const float old_val = m_value;
float newValue = fittedValue(value);

m_value = fittedValue( value );
if( old_val != m_value )
if (m_oldValue != newValue)
{
// add changes to history so user can undo it
addJournalCheckPoint();

m_value = newValue;

// notify linked models
for( AutoModelVector::Iterator it = m_linkedModels.begin(); it != m_linkedModels.end(); ++it )
{
Expand Down
4 changes: 4 additions & 0 deletions src/core/AutomationClip.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -842,6 +842,10 @@ void AutomationClip::loadSettings( const QDomElement & _this )
useCustomClipColor( true );
setColor( _this.attribute( "color" ) );
}
else
{
useCustomClipColor(false);
}

int len = _this.attribute( "len" ).toInt();
if( len <= 0 )
Expand Down
2 changes: 2 additions & 0 deletions src/core/Clip.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,8 @@ Clip::Clip( Track * track ) :
movePosition( 0 );
changeLength( 0 );
setJournalling( true );

connect(&m_mutedModel, &Model::dataChanged, this, &Model::dataChanged);
}


Expand Down
119 changes: 89 additions & 30 deletions src/core/ProjectJournal.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@

#include <cstdlib>

#include "debug.h"
#include "ProjectJournal.h"
#include "Engine.h"
#include "JournallingObject.h"
Expand Down Expand Up @@ -51,84 +52,142 @@ ProjectJournal::ProjectJournal() :

void ProjectJournal::undo()
{
while( !m_undoCheckPoints.isEmpty() )
{
CheckPoint c = m_undoCheckPoints.pop();
JournallingObject *jo = m_joIDs[c.joID];
restoreCheckPoint(m_undoCheckPoints, m_redoCheckPoints);
}

if( jo )
{
DataFile curState( DataFile::JournalData );
jo->saveState( curState, curState.content() );
m_redoCheckPoints.push( CheckPoint( c.joID, curState ) );

bool prev = isJournalling();
setJournalling( false );
jo->restoreState( c.data.content().firstChildElement() );
setJournalling( prev );
Engine::getSong()->setModified();
break;
}
}


void ProjectJournal::redo()
{
restoreCheckPoint(m_redoCheckPoints, m_undoCheckPoints);
}



void ProjectJournal::redo()

/*! \brief Take a backup of the current state before restoring the most recent checkpoint
*
* \param restoreStack pop a checkpoint from this stack and restore it
* \param backupStack append a checkpoint of the current state to this stack
*/
void ProjectJournal::restoreCheckPoint(ProjectJournal::CheckPointStack& restoreStack,
ProjectJournal::CheckPointStack& backupStack)
{
while( !m_redoCheckPoints.isEmpty() )
while (!restoreStack.empty())
{
CheckPoint c = m_redoCheckPoints.pop();
JournallingObject *jo = m_joIDs[c.joID];
BatchCheckPoint backup;

if( jo )
// For every checkpoint (journaled object) in the last batch...
for (CheckPoint& restorePoint: restoreStack.back())
{
DataFile curState( DataFile::JournalData );
JournallingObject* jo = journallingObject(restorePoint.joID);
// TODO when can this be null?
if (!jo) { continue; }

// Create a backup of the object's current state
DataFile curState(DataFile::JournalData);
jo->saveState( curState, curState.content() );
m_undoCheckPoints.push( CheckPoint( c.joID, curState ) );
backup.push_back(CheckPoint(restorePoint.joID, curState));
allejok96 marked this conversation as resolved.
Show resolved Hide resolved

// Restore object to its previous state
bool prev = isJournalling();
setJournalling( false );
jo->restoreState( c.data.content().firstChildElement() );
jo->restoreState(restorePoint.data.content().firstChildElement());
setJournalling( prev );
Engine::getSong()->setModified();
break;
}
restoreStack.pop_back();

// Keep trying until something is restored
if (backup.empty()) { continue; }
allejok96 marked this conversation as resolved.
Show resolved Hide resolved

backupStack.push_back(backup);
allejok96 marked this conversation as resolved.
Show resolved Hide resolved
return;
}
}




bool ProjectJournal::canUndo() const
{
return !m_undoCheckPoints.isEmpty();
return !m_undoCheckPoints.empty();
}




bool ProjectJournal::canRedo() const
{
return !m_redoCheckPoints.isEmpty();
return !m_redoCheckPoints.empty();
}




void ProjectJournal::addJournalCheckPoint( JournallingObject *jo )
{
if( isJournalling() )
{
m_redoCheckPoints.clear();

// If we're not batching checkpoints, begin on a new one
if (m_batchingCount == 0 || m_undoCheckPoints.empty())
{
m_undoCheckPoints.emplace_back();
}
BatchCheckPoint& batch = m_undoCheckPoints.back();

// If this object already has a checkpoint in the batch, skip it
PhysSong marked this conversation as resolved.
Show resolved Hide resolved
for (const CheckPoint& checkpoint: batch)
{
if (checkpoint.joID == jo->id()) { return; }
}

// Create a checkpoint and save it to the batch
DataFile dataFile( DataFile::JournalData );
jo->saveState( dataFile, dataFile.content() );
batch.push_back(CheckPoint(jo->id(), dataFile));
allejok96 marked this conversation as resolved.
Show resolved Hide resolved

m_undoCheckPoints.push( CheckPoint( jo->id(), dataFile ) );
// Remove excessive checkpoints from the stack
if( m_undoCheckPoints.size() > MAX_UNDO_STATES )
{
m_undoCheckPoints.remove( 0, m_undoCheckPoints.size() - MAX_UNDO_STATES );
m_undoCheckPoints.erase(m_undoCheckPoints.begin(), m_undoCheckPoints.end() - MAX_UNDO_STATES);
}
}
}




/*! \brief Start adding checkpoints together as a batch
*
* \param append - Don't create a new batch, use the last checkpoint
*/
void ProjectJournal::beginBatchCheckPoint(bool append)
{
if (!isJournalling()) { return; }
// Begin on a new batch if we are not already batching, or if there is nothing to append to
if (append ? m_undoCheckPoints.empty() : m_batchingCount == 0) { m_undoCheckPoints.emplace_back(); }
++m_batchingCount;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

First, I found the variable name confusing. batchingCount sounds like "This is the number of CheckPoints that I am batching". Maybe subjective, though 😁

Secondly, why is this variable changed if we add/remove checkpoints here, but not e.g. in ProjectJournal::addJournalCheckPoint? Intended?

Thirdly, I guess this variable would (if stuff like "secondly" would be changed) be equivalent to something like m_undoCheckPoints.size() or m_undoCheckPoints.size() + m_redoCheckPoints.size()?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes confusing name. Perhaps batchesInProgress would be more descriptive?

The count is not of the number of checkpoints in the current batch, instead it counts the number of times beginBatchCheckPoint have been called (like a recursion level, but it's not recursive). Calling the begin function multiple time has no real effect, but we must count the calls, because for every call to begin you expect a call to endBatchCheckPoint.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah! So the only reason for that variable is to make sure that begin... and end... match? Then it would all make sense.

An RAII version, like BashPointGuard would be cool (though, probably, less descriptive...).

Copy link
Contributor Author

@allejok96 allejok96 Jul 9, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not so good at RAII so I don't understand how that would be implemented in this case. I just get a feeling it would make things a lot more complex than they had to be, and you'd still have to use some form of counter or vector so you can detect when the last guard is deleted.

What would be nice if there was something like a context manager:

def doThing():
    with journal.batchCheckPoints():
        for obj in objects:
            obj.addJournalCheckPoint()

But the closest I can think of is

void doThing()
{
    BatchHandle handle = journal->getBatchHandle(); // error: variable never used
    for (Object ob: objects)
    {
        obj->addJournalCheckPoint();
    }
}

(It would not remove the need for the counter, but it would remove the need to call endBatchCheckPoint() which could mess things up if forgotten)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, this was what I had in mind, too. It should not cause unused variable warnings, since std::lock_guard is used the same way.

I'm OK with either begin/end or RAII, as long as it's clear what the counter is used for 😁

}




void ProjectJournal::endBatchCheckPoint()
{
if (!isJournalling()) { return; }
// If no checkpoints were added to the batch, remove it
if (m_undoCheckPoints.back().empty()) { m_undoCheckPoints.pop_back(); }
--m_batchingCount;
assert(m_batchingCount >= 0);
}




jo_id_t ProjectJournal::allocID( JournallingObject * _obj )
{
jo_id_t id;
Expand Down
6 changes: 4 additions & 2 deletions src/core/Song.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -779,29 +779,31 @@ void Song::stopExport()

void Song::insertBar()
{
Engine::projectJournal()->beginBatchCheckPoint();
m_tracksMutex.lockForRead();
for (Track* track: tracks())
{
// FIXME journal batch of tracks instead of each track individually
if (track->numOfClips() > 0) { track->addJournalCheckPoint(); }
track->insertBar(m_playPos[Mode_PlaySong]);
}
m_tracksMutex.unlock();
Engine::projectJournal()->endBatchCheckPoint();
}




void Song::removeBar()
{
Engine::projectJournal()->beginBatchCheckPoint();
m_tracksMutex.lockForRead();
for (Track* track: tracks())
{
// FIXME journal batch of tracks instead of each track individually
if (track->numOfClips() > 0) { track->addJournalCheckPoint(); }
track->removeBar(m_playPos[Mode_PlaySong]);
}
m_tracksMutex.unlock();
Engine::projectJournal()->endBatchCheckPoint();
}


Expand Down
5 changes: 5 additions & 0 deletions src/core/Track.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
#include "InstrumentTrack.h"
#include "PatternStore.h"
#include "PatternTrack.h"
#include "ProjectJournal.h"
#include "SampleTrack.h"
#include "Song.h"

Expand Down Expand Up @@ -584,6 +585,9 @@ bar_t Track::length() const
*/
void Track::toggleSolo()
{
// The solo led will have created its own checkpoint already, so we append to that batch
allejok96 marked this conversation as resolved.
Show resolved Hide resolved
Engine::projectJournal()->beginBatchCheckPoint(/*append*/ true);

const TrackContainer::TrackList & tl = m_trackContainer->tracks();

bool soloBefore = false;
Expand Down Expand Up @@ -638,6 +642,7 @@ void Track::toggleSolo()
}
}
}
Engine::projectJournal()->endBatchCheckPoint();
}

void Track::setColor(const QColor& c)
Expand Down
Loading