コマンドチェインの最初と最後を検知できるように
Undo/Redo の際、コマンドチェインの最初と最後のタイミングがわかると便利なときがあります
コマンドチェインを実行するときに、最初と最後のタイミングも通知するようにしてみましょう
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 class CRestoreObj : public IRestoreObj { int32_t m_cRef; protected: virtual void undo(){} virtual void redo(){} virtual void undo_begin(){} virtual void undo_end(){} virtual void redo_begin(){} virtual void redo_end(){} 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 void invoke( uint32_t, uint32_t uMsg ){ 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(); } } CRestoreObj(): m_cRef(1) { } virtual ~CRestoreObj(){ } private: 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 ); bool undo_begin(); bool undo_end(); bool undo(); bool redo_begin(); bool redo_end(); bool redo(); uint32_t chain_id() const; uint32_t chain_level() 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 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; m_pRestoreObj = rhs.m_pRestoreObj; } return *this; } inline cundoredoitem::cundoredoitem(): m_chain_id(INVALID_CHAIN_ID), m_chain_level(0), m_id(INVALID_COMMAND_ID), m_pRestoreObj(NULL) { } 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) { } 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 ){ 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 ){ 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 ); }
Undo リスト管理クラスの実装
class cundoredo { std::deque<cundoredoitem> m_redo; std::deque<cundoredoitem> m_undo; 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 ); bool chain_begin(); bool chain_end(); bool chain_cancel(); bool undo(); bool redo(); bool is_undo_available(); bool is_redo_available(); cundoredo(); ~cundoredo(); private: cundoredo( const cundoredo& ); cundoredo& operator=( const cundoredo& ); };
cundoredo::cundoredo(): m_redo(), m_undo(), 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_undo.push_back( item ); for( std::deque<cundoredoitem>::iterator p = m_redo.begin(); p != m_redo.end(); ++p ){ p->detach(); } m_redo.clear(); result = m_undo.rbegin()->redo(); } } return result; } bool cundoredo::get_object( uint32_t id, IRestoreObj** ppRestoreObj ){ if( ppRestoreObj ){ *ppRestoreObj = NULL; } bool result = true; if( !ppRestoreObj ){ result = false; } if( result ){ std::deque<cundoredoitem>::iterator p = std::find( m_undo.begin(), m_undo.end(), id ); if( p == m_undo.end()){ result = false; } else{ 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_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()){ p->detach(); m_undo.erase( p ); result = true; } else{ p = std::find( m_redo.begin(), m_redo.end(), id ); if( p != m_redo.end()){ p->detach(); m_redo.erase( p ); result = true; } } } return result; } bool cundoredo::is_undo_available(){ return !m_undo.empty(); } bool cundoredo::is_redo_available(){ 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(){ 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; } } 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_chain_level; } return result; }
テストコード
class CRestoreObj_string_push_back : public CRestoreObj { std::string& m_string; char m_c; uint8_t :8; uint8_t :8; uint8_t :8; void push_back( char c ){ m_string.push_back( c ); } void pop_back(){ if( !m_string.empty()){ m_string.erase( m_string.size()-1 ); } } public: void undo(){ std::string string_prev = m_string; pop_back(); debug_printf( "undo: \"%s\" → \"%s\"\n", string_prev.c_str(), m_string.c_str()); } void redo(){ std::string string_prev = m_string; push_back( m_c ); debug_printf( "redo: \"%s\" → \"%s\"\n", string_prev.c_str(), m_string.c_str()); } 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" ); } CRestoreObj_string_push_back( std::string& s, char c ): CRestoreObj(), m_string(s), m_c(c) { } virtual ~CRestoreObj_string_push_back(){ } private: CRestoreObj_string_push_back(); CRestoreObj_string_push_back( const CRestoreObj_string_push_back& ); CRestoreObj_string_push_back& operator=( const CRestoreObj_string_push_back& ); }; int main(){ std::string s; cundoredo undoredo; undoredo.chain_begin(); string_push_back( undoredo, s, 'A' ); string_push_back( undoredo, s, 'B' ); string_push_back( undoredo, s, 'C' ); undoredo.chain_begin(); string_push_back( undoredo, s, '1' ); string_push_back( undoredo, s, '2' ); string_push_back( undoredo, s, '3' ); //undoredo.chain_cancel(); //undoredo.chain_cancel(); undoredo.chain_end(); string_push_back( undoredo, s, 'D' ); undoredo.chain_end(); undoredo.undo(); undoredo.redo(); undoredo.undo(); undoredo.chain_begin(); string_push_back( undoredo, s, 'E' ); undoredo.chain_end(); return 0; }
出力
redo: "" → "A" redo: "A" → "AB" redo: "AB" → "ABC" redo: "ABC" → "ABC1" redo: "ABC1" → "ABC12" redo: "ABC12" → "ABC123" redo: "ABC123" → "ABC123D" undo:{ undo: "ABC123D" → "ABC123" undo: "ABC123" → "ABC12" undo: "ABC12" → "ABC1" undo: "ABC1" → "ABC" undo: "ABC" → "AB" undo: "AB" → "A" undo: "A" → "" undo:} redo:{ redo: "" → "A" redo: "A" → "AB" redo: "AB" → "ABC" redo: "ABC" → "ABC1" redo: "ABC1" → "ABC12" redo: "ABC12" → "ABC123" redo: "ABC123" → "ABC123D" undo:} undo:{ undo: "ABC123D" → "ABC123" undo: "ABC123" → "ABC12" undo: "ABC12" → "ABC1" undo: "ABC1" → "ABC" undo: "ABC" → "AB" undo: "AB" → "A" undo: "A" → "" undo:} redo: "" → "E"