/* * All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or * its licensors. * * For complete copyright and license terms please see the LICENSE at the root of this * distribution (the "License"). All use of this software is governed by the License, * or, if provided, by the license below or the license accompanying this file. Do not * remove or modify any license notices. This file is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * */ // Original file Copyright Crytek GMBH or its affiliates, used under license. #include "StdAfx.h" #include "Undo.h" #include "IUndoManagerListener.h" #include "Objects/ObjectManager.h" #include #include #include #include "QtUtilWin.h" #define UNDOREDO_BUTTON_POPUP_TEXT_WIDTH 81 #define UNDOREDO_MULTIPLE_OBJECTS_TEXT " (Multiple Objects)" //! CSuperUndo objects groups together a block of UndoStepto to allow them to be Undo by single operation. class CSuperUndoStep : public CUndoStep { public: //! Add new undo object to undo step. void AddUndoStep(CUndoStep* step) { m_undoSteps.push_back(step); } virtual int GetSize() const { int size = 0; for (int i = 0; i < m_undoSteps.size(); i++) { size += m_undoSteps[i]->GetSize(); } return size; } virtual bool IsEmpty() const { return m_undoSteps.empty(); } virtual void Undo(bool bUndo) { for (int i = m_undoSteps.size() - 1; i >= 0; i--) { m_undoSteps[i]->Undo(bUndo); } } virtual void Redo() { for (int i = 0; i < m_undoSteps.size(); i++) { m_undoSteps[i]->Redo(); } } // to get memory statistics void GetMemoryUsage(ICrySizer* pSizer) { for (int i = 0; i < m_undoSteps.size(); i++) { m_undoSteps[i]->GetMemoryUsage(pSizer); } pSizer->Add(*this); } private: //! Undo steps included in this step. std::vector m_undoSteps; }; ////////////////////////////////////////////////////////////////////////// CUndoManager::CUndoManager() { m_bRecording = false; m_bSuperRecording = false; m_currentUndo = 0; m_superUndo = 0; m_suspendCount = 0; m_bUndoing = false; m_bRedoing = false; } ////////////////////////////////////////////////////////////////////////// CUndoManager::~CUndoManager() { m_bRecording = false; ClearRedoStack(); ClearUndoStack(); delete m_superUndo; delete m_currentUndo; } ////////////////////////////////////////////////////////////////////////// void CUndoManager::Begin() { //CryLog( " Begin SuspendCount=%d",m_suspendCount ); //if (m_bSuperRecording) //CLogFile::FormatLine( " Begin (Inside SuperSuper)" ); if (m_bUndoing || m_bRedoing) // If Undoing or redoing now, ignore this calls. { return; } // assert( m_bRecording == false ); if (m_bRecording) { //CLogFile::WriteLine( " Begin (already recording)" ); // Not cancel, just combine. return; } // Begin Creates a new undo object. m_currentUndo = new CUndoStep; m_bRecording = true; //CLogFile::WriteLine( " Begin OK" ); } ////////////////////////////////////////////////////////////////////////// void CUndoManager::Restore(bool bUndo) { if (m_bUndoing || m_bRedoing) // If Undoing or redoing now, ignore this calls. { return; } if (m_currentUndo) { BeginRestoreTransaction(); Suspend(); if (bUndo && m_currentUndo) { m_currentUndo->Undo(false); // Undo not by Undo command (no need to store Redo) } Resume(); if (m_currentUndo) { m_currentUndo->ClearObjects(); } EndRestoreTransaction(); } //CryLog( "Restore Undo" ); } // This function is used below to decide if an operation should force a save or not. This currently // prevents selecting an entity, either from the outliner or both the old and new viewports. static bool ShouldPersist(const QString& name) { return name != "Select Object(s)" && name != "Select Entity" && name != "Box Select Entities"; } ////////////////////////////////////////////////////////////////////////// void CUndoManager::Accept(const QString& name) { //CryLog( " Accept, Suspend Count=%d",m_suspendCount ); if (m_bUndoing || m_bRedoing) // If Undoing or redoing now, ignore this calls. { return; } if (!m_bRecording) { //CLogFile::WriteLine( " Accept (Not recording)" ); return; } if (!m_currentUndo->IsEmpty()) { const bool persist = ShouldPersist(name); if (persist) { GetIEditor()->SetModifiedFlag(); } // If accepting new undo object, must clear all redo stack. ClearRedoStack(); m_currentUndo->SetName(name); if (m_bSuperRecording) { m_superUndo->AddUndoStep(m_currentUndo); } else { // Normal recording. // Keep max undo steps. while (m_undoStack.size() && (m_undoStack.size() >= GetIEditor()->GetEditorSettings()->undoLevels || GetDatabaseSize() > 100 * 1024 * 1024)) { delete m_undoStack.front(); m_undoStack.pop_front(); } m_undoStack.push_back(m_currentUndo); } //CLogFile::FormatLine( "Undo Object Accepted (Undo:%d,Redo:%d, Size=%dKb)",m_undoStack.size(),m_redoStack.size(),GetDatabaseSize()/1024 ); // If undo accepted, document modified. if (persist) { GetIEditor()->SetModifiedFlag(); } if (name.compare("Select Object(s)", Qt::CaseInsensitive) == 0) { GetIEditor()->SetModifiedModule(eModifiedBrushes); } else if (name.compare("Move Selection", Qt::CaseInsensitive) == 0) { GetIEditor()->SetModifiedModule(eModifiedBrushes); } else if (name.compare("SubObject Select", Qt::CaseInsensitive) == 0) { GetIEditor()->SetModifiedModule(eModifiedBrushes); } else if (name.compare("Manipulator Drag", Qt::CaseInsensitive) == 0) { GetIEditor()->SetModifiedModule(eModifiedBrushes); } } else { // If no any object was recorded, Cancel undo operation. Cancel(); } m_bRecording = false; m_currentUndo = 0; SignalNumUndoRedoToListeners(); //CLogFile::WriteLine( " Accept OK" ); } ////////////////////////////////////////////////////////////////////////// void CUndoManager::Cancel() { //CryLog( " Cancel" ); if (m_bUndoing || m_bRedoing) // If Undoing or redoing now, ignore this calls. { return; } if (!m_bRecording) { return; } assert(m_currentUndo != 0); m_bRecording = false; if (!m_currentUndo->IsEmpty()) { // Restore all objects to the state they was at Begin call and throws out all undo objects. Restore(true); //GetIEditor()->GetLogFile()->FormatLine( "Undo Object Canceled (Undo:%d,Redo:%d)",m_undoStack.size(),m_redoStack.size() ); } delete m_currentUndo; m_currentUndo = 0; //CLogFile::WriteLine( " Cancel OK" ); } ////////////////////////////////////////////////////////////////////////// void CUndoManager::Redo(int numSteps) { GetIEditor()->Notify(eNotify_OnBeginUndoRedo); if (m_bUndoing || m_bRedoing) // If Undoing or redoing now, ignore this calls. { return; } if (m_bRecording || m_bSuperRecording) { GetIEditor()->GetLogFile()->FormatLine("Cannot Redo while Recording"); return; } m_bRedoing = true; BeginUndoTransaction(); m_bRedoing = false; if (!m_redoStack.empty()) { Suspend(); while (numSteps-- > 0 && !m_redoStack.empty() && !m_bClearRedoStackQueued) { m_bRedoing = true; CUndoStep* redo = m_redoStack.back(); redo->Redo(); m_redoStack.pop_back(); // Push undo object to redo stack. m_undoStack.push_back(redo); m_bRedoing = false; } Resume(); } if (m_suspendCount == 0) { GetIEditor()->UpdateViews(eUpdateObjects); } GetIEditor()->GetLogFile()->FormatLine("Redo (Undo:%d,Redo:%d)", m_undoStack.size(), m_redoStack.size()); GetIEditor()->GetObjectManager()->InvalidateVisibleList(); m_bRedoing = true; EndUndoTransaction(); SignalNumUndoRedoToListeners(); m_bRedoing = false; GetIEditor()->Notify(eNotify_OnEndUndoRedo); if (m_bClearRedoStackQueued) { ClearRedoStack(); } } ////////////////////////////////////////////////////////////////////////// void CUndoManager::Undo(int numSteps) { GetIEditor()->Notify(eNotify_OnBeginUndoRedo); if (m_bUndoing || m_bRedoing) // If Undoing or redoing now, ignore this calls. { return; } if (m_bRecording || m_bSuperRecording) { GetIEditor()->GetLogFile()->FormatLine("Cannot Undo while Recording"); return; } m_bUndoing = true; BeginUndoTransaction(); m_bUndoing = false; if (!m_undoStack.empty()) { Suspend(); while (numSteps-- > 0 && !m_undoStack.empty()) { m_bUndoing = true; CUndoStep* undo = m_undoStack.back(); undo->Undo(true); m_undoStack.pop_back(); // Push undo object to redo stack. m_redoStack.push_back(undo); m_bUndoing = false; } Resume(); } // Update viewports. if (m_suspendCount == 0) { GetIEditor()->UpdateViews(eUpdateObjects); } GetIEditor()->GetLogFile()->FormatLine("Undo (Undo:%d,Redo:%d)", m_undoStack.size(), m_redoStack.size()); GetIEditor()->GetObjectManager()->InvalidateVisibleList(); m_bUndoing = true; EndUndoTransaction(); SignalNumUndoRedoToListeners(); m_bUndoing = false; GetIEditor()->Notify(eNotify_OnEndUndoRedo); } ////////////////////////////////////////////////////////////////////////// void CUndoManager::RecordUndo(IUndoObject* obj) { //CryLog( " RecordUndo Name=%s",obj->GetDescription() ); if (m_bUndoing || m_bRedoing) // If Undoing or redoing now, ignore this calls. { //CLogFile::WriteLine( " RecordUndo (Undoing or Redoing)" ); obj->Release(); return; } if (m_bRecording && (m_suspendCount == 0)) { assert(m_currentUndo != 0); m_currentUndo->AddUndoObject(obj); //CLogFile::FormatLine( "Undo Object Added: %s",obj->GetDescription() ); } else { //CLogFile::WriteLine( " RecordUndo (Not Recording)" ); // Ignore this object. obj->Release(); } } ////////////////////////////////////////////////////////////////////////// void CUndoManager::ClearRedoStack() { if (m_bRedoing) { m_bClearRedoStackQueued = true; return; } m_bClearRedoStackQueued = false; for (std::list::iterator it = m_redoStack.begin(); it != m_redoStack.end(); it++) { delete *it; } m_redoStack.clear(); SignalNumUndoRedoToListeners(); } ////////////////////////////////////////////////////////////////////////// void CUndoManager::ClearUndoStack() { for (std::list::iterator it = m_undoStack.begin(); it != m_undoStack.end(); it++) { delete *it; } m_undoStack.clear(); SignalNumUndoRedoToListeners(); } ////////////////////////////////////////////////////////////////////////// void CUndoManager::ClearUndoStack(int num) { int i = num; while (i > 0 && !m_undoStack.empty()) { delete m_undoStack.front(); m_undoStack.pop_front(); i--; } SignalNumUndoRedoToListeners(); } ////////////////////////////////////////////////////////////////////////// void CUndoManager::ClearRedoStack(int num) { int i = num; while (i > 0 && !m_redoStack.empty()) { delete m_redoStack.back(); m_redoStack.pop_back(); i--; } SignalNumUndoRedoToListeners(); } ////////////////////////////////////////////////////////////////////////// bool CUndoManager::IsHaveRedo() const { return !m_redoStack.empty(); } ////////////////////////////////////////////////////////////////////////// bool CUndoManager::IsHaveUndo() const { return !m_undoStack.empty(); } ////////////////////////////////////////////////////////////////////////// void CUndoManager::Suspend() { m_suspendCount++; //CLogFile::FormatLine( " Suspend %d",m_suspendCount ); } ////////////////////////////////////////////////////////////////////////// void CUndoManager::Resume() { assert(m_suspendCount >= 0); if (m_suspendCount > 0) { m_suspendCount--; } //CLogFile::FormatLine( " Resume %d",m_suspendCount ); } ////////////////////////////////////////////////////////////////////////// void CUndoManager::SuperBegin() { //CLogFile::FormatLine( " SuperBegin (SuspendCount%d)",m_suspendCount ); if (m_bUndoing || m_bRedoing) // If Undoing or redoing now, ignore this calls. { return; } m_bSuperRecording = true; m_superUndo = new CSuperUndoStep; //CLogFile::WriteLine( " SuperBegin OK" ); } ////////////////////////////////////////////////////////////////////////// void CUndoManager::SuperAccept(const QString& name) { //CLogFile::WriteLine( " SupperAccept" ); if (m_bUndoing || m_bRedoing) // If Undoing or redoing now, ignore this calls. { return; } if (!m_bSuperRecording) { return; } assert(m_superUndo != 0); if (m_bRecording) { Accept(name); } if (!m_superUndo->IsEmpty()) { m_superUndo->SetName(name); // Keep max undo steps. while (m_undoStack.size() && (m_undoStack.size() >= GetIEditor()->GetEditorSettings()->undoLevels || GetDatabaseSize() > 100 * 1024 * 1024)) { delete m_undoStack.front(); m_undoStack.pop_front(); } m_undoStack.push_back(m_superUndo); } else { // If no any object was recorded, Cancel undo operation. SuperCancel(); } //CLogFile::FormatLine( "Undo Object Accepted (Undo:%d,Redo:%d)",m_undoStack.size(),m_redoStack.size() ); m_bSuperRecording = false; m_superUndo = 0; //CLogFile::WriteLine( " SupperAccept OK" ); SignalNumUndoRedoToListeners(); } ////////////////////////////////////////////////////////////////////////// void CUndoManager::SuperCancel() { //CLogFile::WriteLine( " SuperCancel" ); if (m_bUndoing || m_bRedoing) // If Undoing or redoing now, ignore this calls. { return; } if (!m_bSuperRecording) { return; } AzToolsFramework::EditorMetricsEventBusSelectionChangeHelper metricsHelper; assert(m_superUndo != 0); if (m_bRecording) { Cancel(); } Suspend(); //! Undo all changes already made. m_superUndo->Undo(false); // Undo not by Undo command (no need to store Redo) Resume(); m_bSuperRecording = false; delete m_superUndo; m_superUndo = 0; //CLogFile::WriteLine( " SuperCancel OK" ); } ////////////////////////////////////////////////////////////////////////// int CUndoManager::GetUndoStackLen() const { return m_undoStack.size(); } ////////////////////////////////////////////////////////////////////////// int CUndoManager::GetRedoStackLen() const { return m_redoStack.size(); } ////////////////////////////////////////////////////////////////////////// std::vector CUndoManager::GetUndoStackNames() const { std::vector undos(m_undoStack.size()); int i = 0; QString text; for (auto it = m_undoStack.begin(); it != m_undoStack.end(); it++) { QString objNames = (*it)->GetObjectNames(); text = (*it)->GetName() + objNames; if (text.length() > UNDOREDO_BUTTON_POPUP_TEXT_WIDTH) { undos[i++] = (*it)->GetName() + UNDOREDO_MULTIPLE_OBJECTS_TEXT; } else { undos[i++] = (*it)->GetName() + (objNames.isEmpty() ? "" : " (" + objNames + ")"); } } return undos; } ////////////////////////////////////////////////////////////////////////// std::vector CUndoManager::GetRedoStackNames() const { std::vector redos(m_redoStack.size()); int i = 0; QString text; for (auto it = m_redoStack.begin(); it != m_redoStack.end(); it++) { text = (*it)->GetName() + (*it)->GetObjectNames(); if (text.length() > UNDOREDO_BUTTON_POPUP_TEXT_WIDTH) { redos[i++] = (*it)->GetName() + UNDOREDO_MULTIPLE_OBJECTS_TEXT; } else { redos[i++] = (*it)->GetName() + " (" + (*it)->GetObjectNames() + ")"; } } return redos; } ////////////////////////////////////////////////////////////////////////// int CUndoManager::GetDatabaseSize() { int size = 0; { for (std::list::iterator it = m_undoStack.begin(); it != m_undoStack.end(); it++) { size += (*it)->GetSize(); } } { for (std::list::iterator it = m_redoStack.begin(); it != m_redoStack.end(); it++) { size += (*it)->GetSize(); } } return size; } ////////////////////////////////////////////////////////////////////////// void CUndoManager::Flush() { m_bRecording = false; ClearRedoStack(); ClearUndoStack(); delete m_superUndo; delete m_currentUndo; m_superUndo = 0; m_currentUndo = 0; SignalUndoFlushedToListeners(); } ////////////////////////////////////////////////////////////////////////// CUndoStep* CUndoManager::GetNextUndo() { if (!m_undoStack.empty()) { return m_undoStack.back(); } return nullptr; } ////////////////////////////////////////////////////////////////////////// CUndoStep* CUndoManager::GetNextRedo() { if (!m_redoStack.empty()) { return m_redoStack.back(); } return nullptr; } ////////////////////////////////////////////////////////////////////////// void CUndoManager::SetMaxUndoStep(int steps) { GetIEditor()->GetEditorSettings()->undoLevels = steps; }; ////////////////////////////////////////////////////////////////////////// int CUndoManager::GetMaxUndoStep() const { return GetIEditor()->GetEditorSettings()->undoLevels; } void CUndoManager::GetMemoryUsage(ICrySizer* pSizer) { if (m_currentUndo) { m_currentUndo->GetMemoryUsage(pSizer); } if (m_superUndo) { m_superUndo->GetMemoryUsage(pSizer); } for (std::list::const_iterator it = m_undoStack.begin(); it != m_undoStack.end(); it++) { (*it)->GetMemoryUsage(pSizer); } for (std::list::const_iterator it = m_redoStack.begin(); it != m_redoStack.end(); it++) { (*it)->GetMemoryUsage(pSizer); } pSizer->Add(*this); } void CUndoManager::AddListener(IUndoManagerListener* pListener) { stl::push_back_unique(m_listeners, pListener); } void CUndoManager::RemoveListener(IUndoManagerListener* pListener) { stl::find_and_erase(m_listeners, pListener); } void CUndoManager::BeginUndoTransaction() { for (auto iter = m_listeners.begin(); iter != m_listeners.end(); ++iter) { (*iter)->BeginUndoTransaction(); } } void CUndoManager::EndUndoTransaction() { for (auto iter = m_listeners.begin(); iter != m_listeners.end(); ++iter) { (*iter)->EndUndoTransaction(); } } void CUndoManager::BeginRestoreTransaction() { for (auto iter = m_listeners.begin(); iter != m_listeners.end(); ++iter) { (*iter)->BeginRestoreTransaction(); } } void CUndoManager::EndRestoreTransaction() { for (auto iter = m_listeners.begin(); iter != m_listeners.end(); ++iter) { (*iter)->EndRestoreTransaction(); } } void CUndoManager::SignalNumUndoRedoToListeners() { for (IUndoManagerListener* listener : m_listeners) { listener->SignalNumUndoRedo(m_undoStack.size(), m_redoStack.size()); } } void CUndoManager::SignalUndoFlushedToListeners() { for (IUndoManagerListener* listener : m_listeners) { listener->UndoStackFlushed(); } } bool CUndoManager::IsUndoRecording() const { return (m_bRecording || m_bSuperRecording) && m_suspendCount == 0; } bool CUndoManager::IsUndoSuspended() const { return m_suspendCount != 0; }