コマンドチェインに対応

一度の操作で複数のコマンドを一括して Undo/Redo したいというときがありますが、前回のコードでは対応していませんでした
コマンドのグループ化、コマンドチェインに対応しましょう

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_id;
	IRestoreObj*	m_pRestoreObj;
	
	bool	dispatch( uint32_t	uMsg );
public:
	void	detach();
	bool	attach( uint32_t	chain_id, uint32_t	id, IRestoreObj*	pRestoreObj );
	bool	get_object( IRestoreObj**	ppRestoreObj );
	bool	undo();
	bool	redo();
	
	uint32_t	chain_id() const;
	
	bool	operator==( uint32_t	id ) const;
	cundoredoitem&	operator=( const cundoredoitem&	rhs );
	
	cundoredoitem();
	cundoredoitem( uint32_t	chain_id, uint32_t	id, IRestoreObj*	pRestoreObj );
	cundoredoitem( const cundoredoitem&	rhs );
	~cundoredoitem();
};
inline uint32_t	cundoredoitem::chain_id() const {
	return	m_chain_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_id = rhs.m_id;
		m_pRestoreObj = rhs.m_pRestoreObj;
	}
	return	*this;
}
inline cundoredoitem::cundoredoitem():
m_chain_id(INVALID_CHAIN_ID),
m_id(INVALID_COMMAND_ID),
m_pRestoreObj(NULL)
{
}
inline cundoredoitem::cundoredoitem( uint32_t	chain_id, uint32_t	id, IRestoreObj*	pRestoreObj ):
m_chain_id(chain_id),
m_id(id),
m_pRestoreObj(pRestoreObj)
{
}
inline cundoredoitem::cundoredoitem( const cundoredoitem&	rhs ):
m_chain_id(rhs.m_chain_id),
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_id = INVALID_COMMAND_ID;
}
inline bool	cundoredoitem::attach( uint32_t	chain_id, uint32_t	id, IRestoreObj*	pRestoreObj ){
	bool	result = true;
	if(( m_chain_id != chain_id )
	 ||( m_id != id )
	 ||( m_pRestoreObj != pRestoreObj )
	){
		detach();
		if( pRestoreObj ){
			m_chain_id = chain_id;
			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_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(){
	return	dispatch( UNDOREDO_UNDO );
}
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	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_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 ){
			p->undo();
		}
	}
	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 ){
			p->redo();
		}
	}
	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;
}

テストコード

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_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: "ABC123D" → "ABC123"
undo: "ABC123" → "ABC12"
undo: "ABC12" → "ABC1"
undo: "ABC1" → "ABC"
undo: "ABC" → "AB"
undo: "AB" → "A"
undo: "A" → ""
redo: "" → "A"
redo: "A" → "AB"
redo: "AB" → "ABC"
redo: "ABC" → "ABC1"
redo: "ABC1" → "ABC12"
redo: "ABC12" → "ABC123"
redo: "ABC123" → "ABC123D"
undo: "ABC123D" → "ABC123"
undo: "ABC123" → "ABC12"
undo: "ABC12" → "ABC1"
undo: "ABC1" → "ABC"
undo: "ABC" → "AB"
undo: "AB" → "A"
undo: "A" → ""
redo: "" → "E"