コマンドチェインの最初と最後を検知できるように

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"