/* Copyright (C) 2001-2006, William Joseph. All Rights Reserved. This file is part of GtkRadiant. GtkRadiant is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. GtkRadiant is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with GtkRadiant; if not, write to the Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA */ #include "undo.h" #include "debugging/debugging.h" #include "warnings.h" #include "iundo.h" #include "preferencesystem.h" #include "string/string.h" #include "generic/callback.h" #include "preferences.h" #include "stringio.h" #include #include #include #include "timer.h" class DebugScopeTimer { Timer m_timer; const char* m_operation; public: DebugScopeTimer(const char* operation) : m_operation(operation) { m_timer.start(); } ~DebugScopeTimer() { unsigned int elapsed = m_timer.elapsed_msec(); if(elapsed > 0) { globalOutputStream() << m_operation << ": " << elapsed << " msec\n"; } } }; class RadiantUndoSystem : public UndoSystem { INTEGER_CONSTANT(MAX_UNDO_LEVELS, 1024); class Snapshot { class StateApplicator { public: Undoable* m_undoable; private: UndoMemento* m_data; public: StateApplicator(Undoable* undoable, UndoMemento* data) : m_undoable(undoable), m_data(data) { } void restore() { m_undoable->importState(m_data); } void release() { m_data->release(); } }; typedef std::list states_t; states_t m_states; public: bool empty() const { return m_states.empty(); } std::size_t size() const { return m_states.size(); } void save(Undoable* undoable) { m_states.push_front(StateApplicator(undoable, undoable->exportState())); } void restore() { for(states_t::iterator i = m_states.begin(); i != m_states.end(); ++i) { (*i).restore(); } } void release() { for(states_t::iterator i = m_states.begin(); i != m_states.end(); ++i) { (*i).release(); } } }; struct Operation { Snapshot m_snapshot; CopiedString m_command; Operation(const char* command) : m_command(command) { } ~Operation() { m_snapshot.release(); } }; class UndoStack { //! Note: using std::list instead of vector/deque, to avoid copying of undos typedef std::list Operations; Operations m_stack; Operation* m_pending; public: UndoStack() : m_pending(0) { } ~UndoStack() { clear(); } bool empty() const { return m_stack.empty(); } std::size_t size() const { return m_stack.size(); } Operation* back() { return m_stack.back(); } const Operation* back() const { return m_stack.back(); } Operation* front() { return m_stack.front(); } const Operation* front() const { return m_stack.front(); } void pop_front() { delete m_stack.front(); m_stack.pop_front(); } void pop_back() { delete m_stack.back(); m_stack.pop_back(); } void clear() { if(!m_stack.empty()) { for(Operations::iterator i = m_stack.begin(); i != m_stack.end(); ++i) { delete *i; } m_stack.clear(); } } void start(const char* command) { if(m_pending != 0) { delete m_pending; } m_pending = new Operation(command); } bool finish(const char* command) { if(m_pending != 0) { delete m_pending; m_pending = 0; return false; } else { ASSERT_MESSAGE(!m_stack.empty(), "undo stack empty"); m_stack.back()->m_command = command; return true; } } void save(Undoable* undoable) { if(m_pending != 0) { m_stack.push_back(m_pending); m_pending = 0; } back()->m_snapshot.save(undoable); } }; UndoStack m_undo_stack; UndoStack m_redo_stack; class UndoStackFiller : public UndoObserver { UndoStack* m_stack; public: UndoStackFiller() : m_stack(0) { } void save(Undoable* undoable) { ASSERT_NOTNULL(undoable); if(m_stack != 0) { m_stack->save(undoable); m_stack = 0; } } void setStack(UndoStack* stack) { m_stack = stack; } }; typedef std::map undoables_t; undoables_t m_undoables; void mark_undoables(UndoStack* stack) { for(undoables_t::iterator i = m_undoables.begin(); i != m_undoables.end(); ++i) { (*i).second.setStack(stack); } } std::size_t m_undo_levels; typedef std::set Trackers; Trackers m_trackers; public: RadiantUndoSystem() : m_undo_levels(64) { } ~RadiantUndoSystem() { clear(); } UndoObserver* observer(Undoable* undoable) { ASSERT_NOTNULL(undoable); return &m_undoables[undoable]; } void release(Undoable* undoable) { ASSERT_NOTNULL(undoable); m_undoables.erase(undoable); } void setLevels(std::size_t levels) { if(levels > MAX_UNDO_LEVELS()) { levels = MAX_UNDO_LEVELS(); } while(m_undo_stack.size() > levels) { m_undo_stack.pop_front(); } m_undo_levels = levels; } std::size_t getLevels() const { return m_undo_levels; } std::size_t size() const { return m_undo_stack.size(); } void startUndo() { m_undo_stack.start("unnamedCommand"); mark_undoables(&m_undo_stack); } bool finishUndo(const char* command) { bool changed = m_undo_stack.finish(command); mark_undoables(0); return changed; } void startRedo() { m_redo_stack.start("unnamedCommand"); mark_undoables(&m_redo_stack); } bool finishRedo(const char* command) { bool changed = m_redo_stack.finish(command); mark_undoables(0); return changed; } void start() { m_redo_stack.clear(); if(m_undo_stack.size() == m_undo_levels) { m_undo_stack.pop_front(); } startUndo(); trackersBegin(); } void finish(const char* command) { if(finishUndo(command)) { globalOutputStream() << command << '\n'; } } void undo() { if(m_undo_stack.empty()) { globalOutputStream() << "Undo: no undo available\n"; } else { Operation* operation = m_undo_stack.back(); globalOutputStream() << "Undo: " << operation->m_command.c_str() << "\n"; startRedo(); trackersUndo(); operation->m_snapshot.restore(); finishRedo(operation->m_command.c_str()); m_undo_stack.pop_back(); } } void redo() { if(m_redo_stack.empty()) { globalOutputStream() << "Redo: no redo available\n"; } else { Operation* operation = m_redo_stack.back(); globalOutputStream() << "Redo: " << operation->m_command.c_str() << "\n"; startUndo(); trackersRedo(); operation->m_snapshot.restore(); finishUndo(operation->m_command.c_str()); m_redo_stack.pop_back(); } } void clear() { mark_undoables(0); m_undo_stack.clear(); m_redo_stack.clear(); trackersClear(); } void trackerAttach(UndoTracker& tracker) { ASSERT_MESSAGE(m_trackers.find(&tracker) == m_trackers.end(), "undo tracker already attached"); m_trackers.insert(&tracker); } void trackerDetach(UndoTracker& tracker) { ASSERT_MESSAGE(m_trackers.find(&tracker) != m_trackers.end(), "undo tracker cannot be detached"); m_trackers.erase(&tracker); } void trackersClear() const { for(Trackers::const_iterator i = m_trackers.begin(); i != m_trackers.end(); ++i) { (*i)->clear(); } } void trackersBegin() const { for(Trackers::const_iterator i = m_trackers.begin(); i != m_trackers.end(); ++i) { (*i)->begin(); } } void trackersUndo() const { for(Trackers::const_iterator i = m_trackers.begin(); i != m_trackers.end(); ++i) { (*i)->undo(); } } void trackersRedo() const { for(Trackers::const_iterator i = m_trackers.begin(); i != m_trackers.end(); ++i) { (*i)->redo(); } } }; void UndoLevels_importString(RadiantUndoSystem& undo, const char* value) { int levels; Int_importString(levels, value); undo.setLevels(levels); } typedef ReferenceCaller1 UndoLevelsImportStringCaller; void UndoLevels_exportString(const RadiantUndoSystem& undo, const StringImportCallback& importer) { Int_exportString(static_cast(undo.getLevels()), importer); } typedef ConstReferenceCaller1 UndoLevelsExportStringCaller; #include "generic/callback.h" void UndoLevelsImport(RadiantUndoSystem& self, int value) { self.setLevels(value); } typedef ReferenceCaller1 UndoLevelsImportCaller; void UndoLevelsExport(const RadiantUndoSystem& self, const IntImportCallback& importCallback) { importCallback(static_cast(self.getLevels())); } typedef ConstReferenceCaller1 UndoLevelsExportCaller; void Undo_constructPreferences(RadiantUndoSystem& undo, PreferencesPage& page) { page.appendSpinner("Undo Queue Size", 64, 0, 1024, IntImportCallback(UndoLevelsImportCaller(undo)), IntExportCallback(UndoLevelsExportCaller(undo))); } void Undo_constructPage(RadiantUndoSystem& undo, PreferenceGroup& group) { PreferencesPage page(group.createPage("Undo", "Undo Queue Settings")); Undo_constructPreferences(undo, page); } void Undo_registerPreferencesPage(RadiantUndoSystem& undo) { PreferencesDialog_addSettingsPage(ReferenceCaller1(undo)); } class UndoSystemDependencies : public GlobalPreferenceSystemModuleRef { }; class UndoSystemAPI { RadiantUndoSystem m_undosystem; public: typedef UndoSystem Type; STRING_CONSTANT(Name, "*"); UndoSystemAPI() { GlobalPreferenceSystem().registerPreference("UndoLevels", makeIntStringImportCallback(UndoLevelsImportCaller(m_undosystem)), makeIntStringExportCallback(UndoLevelsExportCaller(m_undosystem))); Undo_registerPreferencesPage(m_undosystem); } UndoSystem* getTable() { return &m_undosystem; } }; #include "modulesystem/singletonmodule.h" #include "modulesystem/moduleregistry.h" typedef SingletonModule UndoSystemModule; typedef Static StaticUndoSystemModule; StaticRegisterModule staticRegisterUndoSystem(StaticUndoSystemModule::instance()); class undoable_test : public Undoable { struct state_type : public UndoMemento { state_type() : test_data(0) { } state_type(const state_type& other) : UndoMemento(other), test_data(other.test_data) { } void release() { delete this; } int test_data; }; state_type m_state; UndoObserver* m_observer; public: undoable_test() : m_observer(GlobalUndoSystem().observer(this)) { } ~undoable_test() { GlobalUndoSystem().release(this); } UndoMemento* exportState() const { return new state_type(m_state); } void importState(const UndoMemento* state) { ASSERT_NOTNULL(state); m_observer->save(this); m_state = *(static_cast(state)); } void mutate(unsigned int data) { m_observer->save(this); m_state.test_data = data; } }; #if 0 class TestUndo { public: TestUndo() { undoable_test test; GlobalUndoSystem().begin("bleh"); test.mutate(3); GlobalUndoSystem().begin("blah"); test.mutate(4); GlobalUndoSystem().undo(); GlobalUndoSystem().undo(); GlobalUndoSystem().redo(); GlobalUndoSystem().redo(); } }; TestUndo g_TestUndo; #endif