mirror of https://github.com/aseprite/aseprite.git
				
				
				
			
		
			
				
	
	
		
			337 lines
		
	
	
		
			8.6 KiB
		
	
	
	
		
			C++
		
	
	
	
			
		
		
	
	
			337 lines
		
	
	
		
			8.6 KiB
		
	
	
	
		
			C++
		
	
	
	
| // Aseprite
 | |
| // Copyright (C) 2022-2023  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/console.h"
 | |
| #include "app/context.h"
 | |
| #include "app/doc_undo_observer.h"
 | |
| #include "app/pref/preferences.h"
 | |
| #include "base/mem_utils.h"
 | |
| #include "base/scoped_value.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);
 | |
| 
 | |
|   if (m_undoing) {
 | |
|     delete cmd;
 | |
|     throw CannotModifyWhenUndoingException();
 | |
|   }
 | |
| 
 | |
|   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()
 | |
| {
 | |
|   ASSERT(!m_undoing);
 | |
|   base::ScopedValue undoing(m_undoing, true);
 | |
|   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()
 | |
| {
 | |
|   ASSERT(!m_undoing);
 | |
|   base::ScopedValue undoing(m_undoing, true);
 | |
|   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)
 | |
| {
 | |
|   ASSERT(!m_undoing);
 | |
|   base::ScopedValue undoing(m_undoing, true);
 | |
| 
 | |
|   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
 |