コマンドチェインの無効化に対応
例えば移動したディレクトリを記録しているときに、履歴に残っているディレクトリが途中で削除されてしまった場合には、そのディレクトリへ移動できないようにしたり、そのディレクトリを表す項目を淡色表示したい、というようなこともあるでしょう
そこで、登録されているコマンドが途中で無効になってしまったような場合には、そのコマンドを無視することができるようにしてみましょう
Undo コマンドの基底クラスの実装
#define UNDOREDO_DETACHED 0 #define UNDOREDO_ATTACHED 1 #define UNDOREDO_UNDO 2 #define UNDOREDO_REDO 3 #define UNDOREDO_UNDO_BEGIN 4 #define UNDOREDO_UNDO_END 5 #define UNDOREDO_REDO_BEGIN 6 #define UNDOREDO_REDO_END 7 #define UNDOREDO_UNDO_ENABLED 8 #define UNDOREDO_REDO_ENABLED 9 class CRestoreObj : public IRestoreObj { protected: virtual void undo(){} virtual void redo(){} virtual void undo_begin(){} virtual void undo_end(){} virtual void redo_begin(){} virtual void redo_end(){} virtual bool is_undo_enabled(){ return true; } virtual bool is_redo_enabled(){ return true; } public: virtual uint32_t add_ref(){ return ( uint32_t )++m_cRef; } virtual uint32_t release(){ int32_t cRef = --m_cRef; if( cRef == 0 ){ delete this; return 0; } return ( uint32_t )cRef; } virtual bool invoke( uint32_t, uint32_t uMsg ){ bool result = true; if( uMsg == UNDOREDO_DETACHED ){ release(); } else if( uMsg == UNDOREDO_ATTACHED ){ add_ref(); } else if( uMsg == UNDOREDO_UNDO ){ undo(); } else if( uMsg == UNDOREDO_REDO ){ redo(); } else if( uMsg == UNDOREDO_UNDO_BEGIN ){ undo_begin(); } else if( uMsg == UNDOREDO_UNDO_END ){ undo_end(); } else if( uMsg == UNDOREDO_REDO_BEGIN ){ redo_begin(); } else if( uMsg == UNDOREDO_REDO_END ){ redo_end(); } else if( uMsg == UNDOREDO_UNDO_ENABLED ){ result = is_undo_enabled(); } else if( uMsg == UNDOREDO_REDO_ENABLED ){ result = is_redo_enabled(); } return result; } CRestoreObj(): m_cRef(1) { } virtual ~CRestoreObj(){ } private: int32_t m_cRef; CRestoreObj( const CRestoreObj& ); CRestoreObj& operator=( const CRestoreObj& ); };
Undo コマンド管理クラスの実装
#define INVALID_COMMAND_ID (( uint32_t )-1) #define INVALID_CHAIN_ID (( uint32_t )-1) #define MAXIMUM_CHAIN_LEVEL (( uint32_t )-1) class cundoredoitem { uint32_t m_chain_id; uint32_t m_chain_level; uint32_t m_id; IRestoreObj* m_pRestoreObj; bool dispatch( uint32_t uMsg ); public: void detach(); bool attach( uint32_t chain_id, uint32_t chain_level, uint32_t id, IRestoreObj* pRestoreObj ); bool get_object( IRestoreObj** ppRestoreObj ) const; bool undo_begin(); bool undo_end(); bool undo(); bool redo_begin(); bool redo_end(); bool redo(); bool is_undo_enabled(); bool is_redo_enabled(); uint32_t chain_id() const; uint32_t chain_level() const; uint32_t id() const; bool operator==( uint32_t id ) const; cundoredoitem& operator=( const cundoredoitem& rhs ); cundoredoitem(); cundoredoitem( uint32_t chain_id, uint32_t chain_level, uint32_t id, IRestoreObj* pRestoreObj ); cundoredoitem( const cundoredoitem& rhs ); ~cundoredoitem(); };
inline uint32_t cundoredoitem::chain_id() const { return m_chain_id; } inline uint32_t cundoredoitem::chain_level() const { return m_chain_level; } inline uint32_t cundoredoitem::id() const { return m_id; } inline bool cundoredoitem::operator==( uint32_t id ) const { return ( m_id == id ); } inline cundoredoitem& cundoredoitem::operator=( const cundoredoitem& rhs ){ if( this != &rhs ){ m_chain_id = rhs.m_chain_id; m_chain_level = rhs.m_chain_level; m_id = rhs.m_id; if( m_pRestoreObj ){ m_pRestoreObj->release(); } m_pRestoreObj = rhs.m_pRestoreObj; if( m_pRestoreObj ){ m_pRestoreObj->add_ref(); } } return *this; } inline cundoredoitem::cundoredoitem(): m_chain_id(INVALID_CHAIN_ID), m_chain_level(0), m_id(INVALID_COMMAND_ID), m_pRestoreObj(NULL) { if( m_pRestoreObj ){ m_pRestoreObj->add_ref(); } } inline cundoredoitem::cundoredoitem( uint32_t chain_id, uint32_t chain_level, uint32_t id, IRestoreObj* pRestoreObj ): m_chain_id(chain_id), m_chain_level(chain_level), m_id(id), m_pRestoreObj(pRestoreObj) { if( m_pRestoreObj ){ m_pRestoreObj->add_ref(); } } inline cundoredoitem::cundoredoitem( const cundoredoitem& rhs ): m_chain_id(rhs.m_chain_id), m_chain_level(rhs.m_chain_level), m_id(rhs.m_id), m_pRestoreObj(rhs.m_pRestoreObj) { if( m_pRestoreObj ){ m_pRestoreObj->add_ref(); } } inline cundoredoitem::~cundoredoitem(){ if( m_pRestoreObj ){ m_pRestoreObj->release(); m_pRestoreObj = NULL; } } inline bool cundoredoitem::dispatch( uint32_t uMsg ){ bool result = true; if( !m_pRestoreObj ){ result = false; } if( result ){ result = m_pRestoreObj->invoke( m_id, uMsg ); } return result; } inline void cundoredoitem::detach(){ if( m_pRestoreObj ){ dispatch( UNDOREDO_DETACHED ); m_pRestoreObj->release(); m_pRestoreObj = NULL; } m_chain_id = INVALID_CHAIN_ID; m_chain_level = 0; m_id = INVALID_COMMAND_ID; } inline bool cundoredoitem::attach( uint32_t chain_id, uint32_t chain_level, uint32_t id, IRestoreObj* pRestoreObj ){ bool result = true; if(( m_chain_id != chain_id ) ||( m_chain_level != chain_level ) ||( m_id != id ) ||( m_pRestoreObj != pRestoreObj ) ){ detach(); if( pRestoreObj ){ m_chain_id = chain_id; m_chain_level = chain_level; m_id = id; pRestoreObj->add_ref(); m_pRestoreObj = pRestoreObj; result = dispatch( UNDOREDO_ATTACHED ); if( !result ){ m_pRestoreObj->release(); m_pRestoreObj = NULL; m_chain_id = INVALID_CHAIN_ID; m_chain_level = 0; m_id = INVALID_COMMAND_ID; } } } return result; } inline bool cundoredoitem::get_object( IRestoreObj** ppRestoreObj ) const { if( ppRestoreObj ){ *ppRestoreObj = NULL; } bool result = true; if( !ppRestoreObj ){ result = false; } else if( !m_pRestoreObj ){ result = false; } if( result ){ m_pRestoreObj->add_ref(); *ppRestoreObj = m_pRestoreObj; } return result; } inline bool cundoredoitem::undo_begin(){ return dispatch( UNDOREDO_UNDO_BEGIN ); } inline bool cundoredoitem::undo_end(){ return dispatch( UNDOREDO_UNDO_END ); } inline bool cundoredoitem::undo(){ return dispatch( UNDOREDO_UNDO ); } inline bool cundoredoitem::redo_begin(){ return dispatch( UNDOREDO_REDO_BEGIN ); } inline bool cundoredoitem::redo_end(){ return dispatch( UNDOREDO_REDO_END ); } inline bool cundoredoitem::redo(){ return dispatch( UNDOREDO_REDO ); } inline bool cundoredoitem::is_undo_enabled(){ return dispatch( UNDOREDO_UNDO_ENABLED ); } inline bool cundoredoitem::is_redo_enabled(){ return dispatch( UNDOREDO_REDO_ENABLED ); }
Undo チェイン管理クラスの実装
class cundoredochain : public std::vector<uint32_t> { uint32_t m_id; std::string m_name; public: uint32_t id() const { return m_id; } const std::string& name() const { return m_name; } void attach( uint32_t id, const std::string& name ){ m_id = id; m_name = name; clear(); } cundoredochain& operator=( const cundoredochain& rhs ){ if( this != &rhs ){ m_id = rhs.m_id; m_name = rhs.m_name; std::vector<uint32_t>::operator=( rhs ); } return *this; } cundoredochain(): std::vector<uint32_t>(), m_id(INVALID_CHAIN_ID), m_name() { } cundoredochain( uint32_t id, const std::string& name ): std::vector<uint32_t>(), m_id(id), m_name(name) { } cundoredochain( const cundoredochain& rhs ): std::vector<uint32_t>(rhs), m_id(rhs.m_id), m_name(rhs.m_name) { } };
Undo リスト管理クラスの実装
class cundoredo { std::deque<cundoredoitem> m_redo; std::deque<cundoredoitem> m_undo; std::map<uint32_t,cundoredochain> m_chains; uint32_t m_chain_id; uint32_t m_chain_level; uint32_t m_id; public: bool add_object( IRestoreObj* pRestoreObj, uint32_t* pid ); bool delete_object( uint32_t id ); bool get_object( uint32_t id, IRestoreObj** ppRestoreObj ) const; bool chain_begin( const std::string& chain_name ); bool chain_end(); bool chain_cancel(); bool undo(); bool redo(); bool is_undo_available() const; bool is_redo_available() const; size_t get_undo_chain_count() const; size_t get_redo_chain_count() const; std::vector<uint32_t> get_undo_chains() const; std::vector<uint32_t> get_redo_chains() const; std::string get_chain_name( uint32_t chain_id ) const; bool is_undo_enabled( uint32_t chain_id ) const; bool is_redo_enabled( uint32_t chain_id ) const; cundoredo(); ~cundoredo(); private: cundoredo( const cundoredo& ); cundoredo& operator=( const cundoredo& ); };
cundoredo::cundoredo(): m_redo(), m_undo(), m_chains(), m_chain_id(INVALID_CHAIN_ID), m_chain_level(0), m_id(INVALID_COMMAND_ID) { } cundoredo::~cundoredo(){ cundoredo::delete_object( INVALID_COMMAND_ID ); } bool cundoredo::add_object( IRestoreObj* pRestoreObj, uint32_t* pid ){ if( pid ){ pid = 0; } bool result = true; if( !pRestoreObj ){ result = false; } else if( m_chain_id == INVALID_CHAIN_ID ){ result = false; } if( result ){ cundoredoitem item; result = item.attach( m_chain_id, m_chain_level, ++m_id, pRestoreObj ); if( result ){ m_chains[m_chain_id].push_back( m_id ); m_undo.push_back( item ); for( std::deque<cundoredoitem>::iterator p = m_redo.begin(); p != m_redo.end(); ++p ){ m_chains.erase( p->chain_id()); p->detach(); } m_redo.clear(); result = m_undo.rbegin()->redo(); if( pid ){ *pid = m_id; } } } return result; } bool cundoredo::get_object( uint32_t id, IRestoreObj** ppRestoreObj ) const { if( ppRestoreObj ){ *ppRestoreObj = NULL; } bool result = true; if( !ppRestoreObj ){ result = false; } if( result ){ std::deque<cundoredoitem>::const_iterator p = std::find( m_undo.begin(), m_undo.end(), id ); if( p == m_undo.end()){ p = std::find( m_redo.begin(), m_redo.end(), id ); if( p == m_redo.end()){ result = false; } } if( result ){ result = p->get_object( ppRestoreObj ); } } return result; } bool cundoredo::delete_object( uint32_t id ){ bool result = true; if( id == INVALID_COMMAND_ID ){ for( std::deque<cundoredoitem>::iterator p = m_redo.begin(); p != m_redo.end(); ++p ){ p->detach(); } m_redo.clear(); for( std::deque<cundoredoitem>::iterator p = m_undo.begin(); p != m_undo.end(); ++p ){ p->detach(); } m_undo.clear(); m_chains.clear(); m_chain_id = INVALID_CHAIN_ID; m_chain_level = 0; m_id = INVALID_COMMAND_ID; } else{ result = false; std::deque<cundoredoitem>::iterator p = std::find( m_undo.begin(), m_undo.end(), id ); if( p != m_undo.end()){ uint32_t chain_id = p->chain_id(); p->detach(); m_undo.erase( p ); m_chains[chain_id].push_back( id ); result = true; } else{ p = std::find( m_redo.begin(), m_redo.end(), id ); if( p != m_redo.end()){ uint32_t chain_id = p->chain_id(); p->detach(); m_redo.erase( p ); m_chains[chain_id].push_back( id ); result = true; } } } return result; } bool cundoredo::is_undo_available() const { return !m_undo.empty(); } bool cundoredo::is_redo_available() const { return !m_redo.empty(); } bool cundoredo::undo(){ if( !m_undo.empty()){ std::deque<cundoredoitem> undo; do{ undo.push_back( m_undo.back()); m_undo.pop_back(); }while( !m_undo.empty() &&( m_undo.rbegin()->chain_id() == undo.rbegin()->chain_id())); for( std::deque<cundoredoitem>::reverse_iterator p = undo.rbegin(); p != undo.rend(); ++p ){ m_redo.push_back( *p ); } for( std::deque<cundoredoitem>::iterator p = undo.begin(); p != undo.end(); ++p ){ if( p == undo.begin()){ p->undo_begin(); } p->undo(); std::deque<cundoredoitem>::iterator q = p; if( ++q == undo.end()){ p->undo_end(); } } } return true; } bool cundoredo::redo(){ if( !m_redo.empty()){ std::deque<cundoredoitem> redo; do{ redo.push_back( m_redo.back()); m_redo.pop_back(); }while( !m_redo.empty() &&( m_redo.rbegin()->chain_id() == redo.rbegin()->chain_id())); for( std::deque<cundoredoitem>::reverse_iterator p = redo.rbegin(); p != redo.rend(); ++p ){ m_undo.push_back( *p ); } for( std::deque<cundoredoitem>::reverse_iterator p = redo.rbegin(); p != redo.rend(); ++p ){ if( p == redo.rbegin()){ p->redo_begin(); } p->redo(); std::deque<cundoredoitem>::reverse_iterator q = p; if( ++q == redo.rend()){ p->redo_end(); } } } return true; } bool cundoredo::chain_begin( const std::string& chain_name ){ bool result = true; if( !m_chain_level ){ if( m_chain_id + 1 == INVALID_CHAIN_ID ){ result = false; } if( result ){ ++m_chain_level; ++m_chain_id; m_chains[m_chain_id].attach( m_chain_id, chain_name ); } } else{ if( m_chain_level + 1 >= MAXIMUM_CHAIN_LEVEL ){ result = false; } if( result ){ ++m_chain_level; } } return result; } bool cundoredo::chain_end(){ bool result = true; if( !m_chain_level ){ result = false; } if( result ){ --m_chain_level; } return result; } bool cundoredo::chain_cancel(){ bool result = true; if( !m_chain_level ){ result = false; } if( result ){ while( !m_undo.empty()){ std::deque<cundoredoitem>::reverse_iterator p = m_undo.rbegin(); if(( p->chain_id() != m_chain_id )||( p->chain_level() != m_chain_level )){ break; } p->undo(); p->detach(); m_undo.pop_back(); } m_chains.erase( m_chain_id ); --m_chain_level; } return result; } size_t cundoredo::get_undo_chain_count() const { size_t chains = 0; uint32_t chain_id_prev = INVALID_CHAIN_ID; for( std::deque<cundoredoitem>::const_iterator p = m_undo.begin(); p != m_undo.end(); ++p ){ if( !chains ||( chain_id_prev != p->chain_id())){ chain_id_prev = p->chain_id(); ++chains; } } return chains; } std::vector<uint32_t> cundoredo::get_undo_chains() const { std::vector<std::pair<uint32_t,std::string> > chains; chains.reserve( m_undo.size()); for( std::deque<cundoredoitem>::const_reverse_iterator p = m_undo.rbegin(); p != m_undo.rend(); ++p ){ if( chains.empty() ||( chains.rbegin()->first != p->chain_id())){ chains.push_back( std::make_pair( p->chain_id(), m_chains.find( p->chain_id())->second.name())); } } std::vector<uint32_t> result; result.reserve( chains.size()); for( std::vector<std::pair<uint32_t,std::string> >::iterator p = chains.begin(); p != chains.end(); ++p ){ result.push_back( p->first ); } return result; } size_t cundoredo::get_redo_chain_count() const { size_t chains = 0; uint32_t chain_id_prev = INVALID_CHAIN_ID; for( std::deque<cundoredoitem>::const_iterator p = m_redo.begin(); p != m_redo.end(); ++p ){ if( !chains ||( chain_id_prev != p->chain_id())){ chain_id_prev = p->chain_id(); ++chains; } } return chains; } std::vector<uint32_t> cundoredo::get_redo_chains() const { std::vector<std::pair<uint32_t,std::string> > chains; chains.reserve( m_redo.size()); for( std::deque<cundoredoitem>::const_reverse_iterator p = m_redo.rbegin(); p != m_redo.rend(); ++p ){ if( chains.empty() ||( chains.rbegin()->first != p->chain_id())){ chains.push_back( std::make_pair( p->chain_id(), m_chains.find( p->chain_id())->second.name())); } } std::vector<uint32_t> result; result.reserve( chains.size()); for( std::vector<std::pair<uint32_t,std::string> >::iterator p = chains.begin(); p != chains.end(); ++p ){ result.push_back( p->first ); } return result; } std::string cundoredo::get_chain_name( uint32_t chain_id ) const { std::string name; std::map<uint32_t,cundoredochain>::const_iterator p = m_chains.find( chain_id ); if( p != m_chains.end()){ name = p->second.name(); } return name; } bool cundoredo::is_undo_enabled( uint32_t chain_id ) const { bool result = true; std::map<uint32_t,cundoredochain>::const_iterator p = m_chains.find( chain_id ); if( p != m_chains.end()){ for( cundoredochain::const_iterator i = p->second.begin(); i != p->second.end(); ++i ){ IRestoreObj* pRestoreObj = NULL; get_object( *i, &pRestoreObj ); if( pRestoreObj ){ result = pRestoreObj->invoke( *i, UNDOREDO_UNDO_ENABLED ); pRestoreObj->release(); if( !result ){ break; } } } } return result; } bool cundoredo::is_redo_enabled( uint32_t chain_id ) const { bool result = true; std::map<uint32_t,cundoredochain>::const_iterator p = m_chains.find( chain_id ); if( p != m_chains.end()){ for( cundoredochain::const_iterator i = p->second.begin(); i != p->second.end(); ++i ){ IRestoreObj* pRestoreObj = NULL; get_object( *i, &pRestoreObj ); if( pRestoreObj ){ result = pRestoreObj->invoke( *i, UNDOREDO_REDO_ENABLED ); pRestoreObj->release(); if( !result ){ break; } } } } return result; }
テストコード
inline std::string GetCurrentDirectoryA(){ std::string s; uint32_t cch = GetCurrentDirectoryA( 0, NULL ); if( cch ){ s.resize( cch+1 ); GetCurrentDirectoryA( cch, &s[0] ); s.erase( std::find( s.begin(), s.end(), 0 ), s.end()); } return s; } class CRestoreObj_chdir : public CRestoreObj { std::string m_old; std::string m_new; void chdir( const char* prefix, const std::string& s ){ if( GetFileAttributes( s.c_str()) != INVALID_FILE_ATTRIBUTES ){ std::string prev = GetCurrentDirectory(); SetCurrentDirectory( s.c_str()); debug_printf( "%s: \"%s\" → \"%s\"\n", prefix, prev.c_str(), s.c_str()); } else{ debug_printf( "\t(disabled)\n" ); } } public: void undo(){ chdir( "undo", m_old ); } void redo(){ chdir( "redo", m_new ); } void undo_begin(){ debug_printf( "undo:{\n" ); } void undo_end(){ debug_printf( "undo:}\n" ); } void redo_begin(){ debug_printf( "redo:{\n" ); } void redo_end(){ debug_printf( "redo:}\n" ); } bool is_undo_enabled(){ return ( GetFileAttributes( m_new.c_str()) != INVALID_FILE_ATTRIBUTES ); } bool is_redo_enabled(){ return ( GetFileAttributes( m_new.c_str()) != INVALID_FILE_ATTRIBUTES ); } explicit CRestoreObj_chdir( const std::string& new_ ): CRestoreObj(), m_old(GetCurrentDirectory()), m_new(new_) { } virtual ~CRestoreObj_chdir(){ } private: CRestoreObj_chdir(); CRestoreObj_chdir( const CRestoreObj_chdir& ); CRestoreObj_chdir& operator=( const CRestoreObj_chdir& ); }; inline std::string GetFullPathNameA( LPCSTR pszFileName, SIZE_T* pnFilePart = NULL ){ size_t nFilePart = 0; std::string s; uint32_t cch = GetFullPathNameA( pszFileName, 0, NULL, NULL ); if( cch ){ s.resize( cch ); LPSTR pFilePart = &s[0]; GetFullPathNameA( pszFileName, cch, &s[0], &pFilePart ); s.erase( std::find( s.begin(), s.end(), 0 ), s.end()); if( !s.empty()){ nFilePart = ( size_t )( pFilePart - &s[0] ); } } if( pnFilePart ){ *pnFilePart = nFilePart; } return s; } inline void chdir( cundoredo& undoredo, const std::string& caption, const std::string& dir, uint32_t* pid = NULL ){ IRestoreObj* p = new CRestoreObj_chdir( dir ); undoredo.chain_begin( caption + dir ); undoredo.add_object( p, pid ); undoredo.chain_end(); p->release(); } void undo( cundoredo& undoredo ){ std::vector<uint32_t> chains = undoredo.get_undo_chains(); if( !chains.empty()){ bool fDone = false; std::vector<uint32_t>::iterator p = chains.begin(); while( ++p != chains.end()){ undoredo.undo(); if( undoredo.is_undo_enabled( *p )){ fDone = true; break; } } if( !fDone ){ undoredo.undo(); } } } void redo( cundoredo& undoredo ){ std::vector<uint32_t> chains = undoredo.get_redo_chains(); for( std::vector<uint32_t>::iterator p = chains.begin(); p != chains.end(); ++p ){ undoredo.redo(); if( undoredo.is_undo_enabled( *p )){ break; } } } int main(){ std::vector<std::string> sss; sss.push_back( GetFullPathName("aaaa")); sss.push_back( GetFullPathName("bbbb")); sss.push_back( GetFullPathName("cccc")); sss.push_back( GetCurrentDirectory()); CreateDirectory( sss[0].c_str(), NULL ); CreateDirectory( sss[1].c_str(), NULL ); CreateDirectory( sss[2].c_str(), NULL ); cundoredo undoredo; chdir( undoredo, "[0]", sss[0] ); chdir( undoredo, "[1]", sss[1] ); chdir( undoredo, "[2]", sss[2] ); chdir( undoredo, "[3]", sss[3] ); output_chains( undoredo ); RemoveDirectory( sss[1].c_str()); debug_printf( "\nRemoveDirectory( \"%s\" );\n\n", sss[1].c_str()); undo( undoredo ); output_chains( undoredo ); undo( undoredo ); output_chains( undoredo ); undo( undoredo ); output_chains( undoredo ); redo( undoredo ); output_chains( undoredo ); redo( undoredo ); output_chains( undoredo ); return 0; }
出力
redo: "D:\undo_06_disable" → "D:\undo_06_disable\aaaa" redo: "D:\undo_06_disable\aaaa" → "D:\undo_06_disable\bbbb" redo: "D:\undo_06_disable\bbbb" → "D:\undo_06_disable\cccc" redo: "D:\undo_06_disable\cccc" → "D:\undo_06_disable" { [0]D:\undo_06_disable\aaaa [1]D:\undo_06_disable\bbbb [2]D:\undo_06_disable\cccc [3]D:\undo_06_disable --- } RemoveDirectory( "D:\undo_06_disable\bbbb" ); undo:{ undo: "D:\undo_06_disable" → "D:\undo_06_disable\cccc" undo:} { [0]D:\undo_06_disable\aaaa [1]D:\undo_06_disable\bbbb (disabled) [2]D:\undo_06_disable\cccc --- [3]D:\undo_06_disable } undo:{ (disabled) undo:} undo:{ undo: "D:\undo_06_disable\cccc" → "D:\undo_06_disable\aaaa" undo:} { [0]D:\undo_06_disable\aaaa --- [1]D:\undo_06_disable\bbbb [2]D:\undo_06_disable\cccc [3]D:\undo_06_disable } undo:{ undo: "D:\undo_06_disable\aaaa" → "D:\undo_06_disable" undo:} { --- [0]D:\undo_06_disable\aaaa [1]D:\undo_06_disable\bbbb [2]D:\undo_06_disable\cccc [3]D:\undo_06_disable } redo:{ redo: "D:\undo_06_disable" → "D:\undo_06_disable\aaaa" redo:} { [0]D:\undo_06_disable\aaaa --- [1]D:\undo_06_disable\bbbb [2]D:\undo_06_disable\cccc [3]D:\undo_06_disable } redo:{ (disabled) redo:} redo:{ redo: "D:\undo_06_disable\aaaa" → "D:\undo_06_disable\cccc" redo:} { [0]D:\undo_06_disable\aaaa [1]D:\undo_06_disable\bbbb (disabled) [2]D:\undo_06_disable\cccc --- [3]D:\undo_06_disable }