aseprite/src/app/doc_undo.cpp
David Capello 38c0400927 Fix Doc::isModified() when we are in a similar UndoState to the saved one
If the current UndoState doesn't modify the "saved state" (e.g. there
is a sequence of undoes/redoes that doesn't modify the saved version
of the sprite compared to the current one), we can indicate that we
are in the saved state anyway (!Doc::isModified).
2022-11-02 09:58:18 -03:00

322 lines
8.2 KiB
C++

// Aseprite
// Copyright (C) 2022 Igara Studio S.A.
// Copyright (C) 2001-2018 David Capello
//
// This program is distributed under the terms of
// the End-User License Agreement for Aseprite.
#ifdef HAVE_CONFIG_H
#include "config.h"
#endif
#include "app/doc_undo.h"
#include "app/app.h"
#include "app/cmd.h"
#include "app/cmd_transaction.h"
#include "app/context.h"
#include "app/doc_undo_observer.h"
#include "app/pref/preferences.h"
#include "base/mem_utils.h"
#include "undo/undo_history.h"
#include "undo/undo_state.h"
#include <cassert>
#include <stdexcept>
#define UNDO_TRACE(...)
#define STATE_CMD(state) (static_cast<CmdTransaction*>(state->cmd()))
namespace app {
DocUndo::DocUndo()
: m_undoHistory(this)
{
}
void DocUndo::setContext(Context* ctx)
{
m_ctx = ctx;
}
void DocUndo::add(CmdTransaction* cmd)
{
ASSERT(cmd);
UNDO_TRACE("UNDO: Add state <%s> of %s to %s\n",
cmd->label().c_str(),
base::get_pretty_memory_size(cmd->memSize()).c_str(),
base::get_pretty_memory_size(m_totalUndoSize).c_str());
// A linear undo history is the default behavior
if (!App::instance() ||
!App::instance()->preferences().undo.allowNonlinearHistory()) {
clearRedo();
}
m_undoHistory.add(cmd);
m_totalUndoSize += cmd->memSize();
notify_observers(&DocUndoObserver::onAddUndoState, this);
notify_observers(&DocUndoObserver::onTotalUndoSizeChange, this);
if (App::instance()) {
const size_t undoLimitSize =
int(App::instance()->preferences().undo.sizeLimit())
* 1024 * 1024;
// If undo limit is 0, it means "no limit", so we ignore the
// complete logic to discard undo states.
if (undoLimitSize > 0 &&
m_totalUndoSize > undoLimitSize) {
UNDO_TRACE("UNDO: Reducing undo history from %s to %s\n",
base::get_pretty_memory_size(m_totalUndoSize).c_str(),
base::get_pretty_memory_size(undoLimitSize).c_str());
while (m_undoHistory.firstState() &&
m_totalUndoSize > undoLimitSize) {
if (!m_undoHistory.deleteFirstState())
break;
}
}
}
UNDO_TRACE("UNDO: New undo size %s\n",
base::get_pretty_memory_size(m_totalUndoSize).c_str());
}
bool DocUndo::canUndo() const
{
return m_undoHistory.canUndo();
}
bool DocUndo::canRedo() const
{
return m_undoHistory.canRedo();
}
void DocUndo::undo()
{
const size_t oldSize = m_totalUndoSize;
{
const undo::UndoState* state = nextUndo();
ASSERT(state);
const Cmd* cmd = STATE_CMD(state);
m_totalUndoSize -= cmd->memSize();
m_undoHistory.undo();
m_totalUndoSize += cmd->memSize();
}
// This notification could execute a script that modifies the sprite
// again (e.g. a script that is listening the "change" event, check
// the SpriteEvents class). If the sprite is modified, the "cmd" is
// not valid anymore.
notify_observers(&DocUndoObserver::onCurrentUndoStateChange, this);
if (m_totalUndoSize != oldSize)
notify_observers(&DocUndoObserver::onTotalUndoSizeChange, this);
}
void DocUndo::redo()
{
const size_t oldSize = m_totalUndoSize;
{
const undo::UndoState* state = nextRedo();
ASSERT(state);
const Cmd* cmd = STATE_CMD(state);
m_totalUndoSize -= cmd->memSize();
m_undoHistory.redo();
m_totalUndoSize += cmd->memSize();
}
notify_observers(&DocUndoObserver::onCurrentUndoStateChange, this);
if (m_totalUndoSize != oldSize)
notify_observers(&DocUndoObserver::onTotalUndoSizeChange, this);
}
void DocUndo::clearRedo()
{
// Do nothing
if (currentState() == lastState())
return;
m_undoHistory.clearRedo();
notify_observers(&DocUndoObserver::onClearRedo, this);
}
bool DocUndo::isInSavedStateOrSimilar() const
{
if (m_savedStateIsLost)
return false;
// Here we try to find if we can reach the saved state from the
// currentState() undoing or redoing and the sprite is exactly the
// same as the saved state, e.g. this can happen if the undo states
// don't modify the sprite (like actions that change the current
// selection/mask boundaries).
bool savedStateWithUndoes = true;
auto state = currentState();
while (state) {
if (m_savedState == state) {
return true;
}
else if (STATE_CMD(state)->doesChangeSavedState()) {
savedStateWithUndoes = false;
break;
}
state = state->prev();
}
// If we reached the end of the undo history (e.g. because all undo
// states do not modify the sprite), the only way to be in the saved
// state is if the initial point of history is the saved state too
// i.e. when m_savedState is nullptr (and m_savedStateIsLost is
// false).
if (savedStateWithUndoes && m_savedState == nullptr)
return true;
// Now we try with redoes.
state = (currentState() ? currentState()->next(): firstState());
while (state) {
if (STATE_CMD(state)->doesChangeSavedState()) {
return false;
}
else if (m_savedState == state) {
return true;
}
state = state->next();
}
return false;
}
void DocUndo::markSavedState()
{
m_savedState = currentState();
m_savedStateIsLost = false;
notify_observers(&DocUndoObserver::onNewSavedState, this);
}
void DocUndo::impossibleToBackToSavedState()
{
// Now there is no state related to the disk state.
m_savedState = nullptr;
m_savedStateIsLost = true;
notify_observers(&DocUndoObserver::onNewSavedState, this);
}
std::string DocUndo::nextUndoLabel() const
{
const undo::UndoState* state = nextUndo();
if (state)
return STATE_CMD(state)->label();
else
return "";
}
std::string DocUndo::nextRedoLabel() const
{
const undo::UndoState* state = nextRedo();
if (state)
return STATE_CMD(state)->label();
else
return "";
}
SpritePosition DocUndo::nextUndoSpritePosition() const
{
const undo::UndoState* state = nextUndo();
if (state)
return STATE_CMD(state)->spritePositionBeforeExecute();
else
return SpritePosition();
}
SpritePosition DocUndo::nextRedoSpritePosition() const
{
const undo::UndoState* state = nextRedo();
if (state)
return STATE_CMD(state)->spritePositionAfterExecute();
else
return SpritePosition();
}
std::istream* DocUndo::nextUndoDocRange() const
{
const undo::UndoState* state = nextUndo();
if (state)
return STATE_CMD(state)->documentRangeBeforeExecute();
else
return nullptr;
}
std::istream* DocUndo::nextRedoDocRange() const
{
const undo::UndoState* state = nextRedo();
if (state)
return STATE_CMD(state)->documentRangeAfterExecute();
else
return nullptr;
}
Cmd* DocUndo::lastExecutedCmd() const
{
const undo::UndoState* state = m_undoHistory.currentState();
if (state)
return STATE_CMD(state);
else
return NULL;
}
void DocUndo::moveToState(const undo::UndoState* state)
{
m_undoHistory.moveTo(state);
// After onCurrentUndoStateChange don't use the "state" argument, it
// might be deleted because some script might have modified the
// sprite on its "change" event.
notify_observers(&DocUndoObserver::onCurrentUndoStateChange, this);
// Recalculate the total undo size
size_t oldSize = m_totalUndoSize;
m_totalUndoSize = 0;
const undo::UndoState* s = m_undoHistory.firstState();
while (s) {
m_totalUndoSize += STATE_CMD(s)->memSize();
s = s->next();
}
if (m_totalUndoSize != oldSize)
notify_observers(&DocUndoObserver::onTotalUndoSizeChange, this);
}
const undo::UndoState* DocUndo::nextUndo() const
{
return m_undoHistory.currentState();
}
const undo::UndoState* DocUndo::nextRedo() const
{
const undo::UndoState* state = m_undoHistory.currentState();
if (state)
return state->next();
else
return m_undoHistory.firstState();
}
void DocUndo::onDeleteUndoState(undo::UndoState* state)
{
ASSERT(state);
Cmd* cmd = STATE_CMD(state);
UNDO_TRACE("UNDO: Deleting undo state <%s> of %s from %s\n",
cmd->label().c_str(),
base::get_pretty_memory_size(cmd->memSize()).c_str(),
base::get_pretty_memory_size(m_totalUndoSize).c_str());
m_totalUndoSize -= cmd->memSize();
notify_observers(&DocUndoObserver::onDeleteUndoState, this, state);
// Mark this document as impossible to match the version on disk
// because we're just going to delete the saved state.
if (m_savedState == state)
impossibleToBackToSavedState();
}
} // namespace app