|
|
#include "stdafx.h"
#include "global.h"
#include "pbrush.h"
#include "pbrusdoc.h"
#include "bmobject.h"
#include "undo.h"
#include "props.h"
#ifdef _DEBUG
#undef THIS_FILE
static CHAR BASED_CODE THIS_FILE[] = __FILE__; #endif
IMPLEMENT_DYNAMIC(CUndoBmObj, CBitmapObj)
#include "memtrace.h"
CUndoBmObj NEAR theUndo;
static BOOL m_bFlushAtEnd;
/////////////////////////////////////////////////////////////////////////////
//
// A CBmObjSequence is a packed array of slob property changes or custom
// actions. Each record contains a property or action id, a pointer to
// a slob, a property type, and a value (depending on the type).
//
// These sequences are used to store undo/redo information in theUndo.
// Each undo/redo-able thing is contained in one CBmObjSequence.
//
CBmObjSequence::CBmObjSequence() : CByteArray(), m_strDescription() { SetSize(0, 100); // increase growth rate
m_nCursor = 0; }
CBmObjSequence::~CBmObjSequence() { Cleanup(); }
// Pull an array of bytes out of the sequence.
//
void CBmObjSequence::Retrieve( BYTE* rgb, int cb ) { for (int ib = 0; ib < cb; ib += 1) *rgb++ = GetAt(m_nCursor++); }
// Pull a string out the sequence.
void CBmObjSequence::RetrieveStr( CString& str ) { int nStrLen; RetrieveInt(nStrLen); if (nStrLen == 0) { str.Empty(); } else { BYTE* pb = (BYTE*)str.GetBufferSetLength(nStrLen); for (int nByte = 0; nByte < nStrLen; nByte += 1) *pb++ = GetAt(m_nCursor++); str.ReleaseBuffer(nStrLen); } }
// Traverse the sequence and remove any slobs that are contained within.
//
void CBmObjSequence::Cleanup() { m_nCursor = 0;
while (m_nCursor < GetSize()) { BYTE op; CBitmapObj* pSlob; int nPropID;
RetrieveByte(op);
RetrievePtr(pSlob); RetrieveInt(nPropID);
switch (op) { default: TRACE1("Illegal undo opcode (%d)\n", op); ASSERT(FALSE);
case CUndoBmObj::opAction: { int cbUndoRecord; RetrieveInt(cbUndoRecord); int ib = m_nCursor; pSlob->DeleteUndoAction(this, nPropID); m_nCursor = ib + cbUndoRecord; } break;
case CUndoBmObj::opIntProp: case CUndoBmObj::opBoolProp: { int val; RetrieveInt(val); } break;
case CUndoBmObj::opLongProp: { long val; RetrieveLong(val); } break;
case CUndoBmObj::opDoubleProp: { double num; RetrieveNum(num); } break;
case CUndoBmObj::opStrProp: { CString str; RetrieveStr(str); } break;
case CUndoBmObj::opSlobProp: { CBitmapObj* pSlobVal; RetrievePtr(pSlobVal); } break;
case CUndoBmObj::opRectProp: { CRect rcVal; RetrieveRect(rcVal); } break;
case CUndoBmObj::opPointProp: { CPoint ptVal; RetrievePoint(ptVal); } break; } } }
// Start looking right after the begin op for ops we really need to keep.
// If none are found, the entire record is discarded below. (For now, we
// only throw away records that are empty or consist only of selection
// change ops.)
//
BOOL CBmObjSequence::IsUseful(CBitmapObj*& pLastSlob, int& nLastPropID) { m_nCursor = 0; while (m_nCursor < GetSize() && GetAt(m_nCursor) == CUndoBmObj::opAction) { BYTE op; int nAction, cbActionRecord; CBitmapObj* pSlob;
RetrieveByte(op); ASSERT(op == CUndoBmObj::opAction); RetrievePtr(pSlob); RetrieveInt(nAction); RetrieveInt(cbActionRecord);
if (nAction != A_PreSel && nAction != A_PostSel) { // Back cursor up to the opcode...
m_nCursor -= sizeof (int) * 2 + sizeof (CBitmapObj*) + 1; break; }
m_nCursor += cbActionRecord; }
if (m_nCursor == GetSize()) return FALSE; // sequnce consists only of selection changes
// Now check if we should throw this away because it's just
// modifying the same string or rectangle property as the last
// undoable operation... This is an incredible hack to implement
// a "poor man's" Multiple-Consecutive-Changes-to-a-Property-as-
// One-Operation feature.
BYTE op; RetrieveByte(op);
if (op == CUndoBmObj::opStrProp || op == CUndoBmObj::opRectProp) { CBitmapObj* pSlob; int nPropID;
RetrievePtr(pSlob); RetrieveInt(nPropID);
nLastPropID = nPropID; pLastSlob = pSlob; }
m_nCursor = 0; return TRUE; }
// Perform the property changes and actions listed in the sequence.
//
void CBmObjSequence::Apply() { m_nCursor = 0; while (m_nCursor < GetSize()) { BYTE op; CBitmapObj* pSlob; int nPropID;
RetrieveByte(op); RetrievePtr(pSlob); RetrieveInt(nPropID);
switch (op) { default: TRACE1("Illegal undo opcode (%d)\n", op); ASSERT(FALSE);
case CUndoBmObj::opAction: pSlob->UndoAction(this, nPropID); break;
case CUndoBmObj::opIntProp: case CUndoBmObj::opBoolProp: { int val; RetrieveInt(val); pSlob->SetIntProp(nPropID, val); } break; } } }
/////////////////////////////////////////////////////////////////////////////
CUndoBmObj::CUndoBmObj() : m_seqs() { ASSERT(this == &theUndo); // only one of these is allowed!
m_nRecording = 0; m_cbUndo = 0; m_nMaxLevels = 2; m_pLastSlob = NULL; m_nLastPropID = 0; m_nPauseLevel = 0; m_nRedoSeqs = 0; }
CUndoBmObj::~CUndoBmObj() { Flush(); }
// Set the maximum number of sequences that can be held at once.
//
void CUndoBmObj::SetMaxLevels(int nLevels) { if (nLevels < 1) return;
m_nMaxLevels = nLevels; Truncate(); }
// Returns the maximum number of sequences that can be held at once.
//
int CUndoBmObj::GetMaxLevels() const { return m_nMaxLevels; }
// Call this to after a sequence is recorded to prevent the next
// sequence from being coalesced with it.
//
void CUndoBmObj::FlushLast() { m_pLastSlob = NULL; m_nLastPropID = 0; }
// Call this at the start of an undoable user action. Calls may be nested
// as long as each call to BeginUndo is balanced with a call to EndUndo.
// Only the "outermost" calls actually have any affect on the undo buffer.
//
// The szCmd parameter should contain the text that you want to appear
// after "Undo" in the Edit menu.
//
// The bResetCursor parameter is only used internally to modify behaviour
// when recording redo sequences and you should NOT pass anything for this
// parameter.
//
void CUndoBmObj::BeginUndo(const TCHAR* szCmd, BOOL bResetCursor) { #ifdef _DEBUG
if (theApp.m_bLogUndo) TRACE2("BeginUndo: %s (%d)\n", szCmd, m_nRecording); #endif
// Handle nesting
m_nRecording += 1; if (m_nRecording != 1) return;
if (bResetCursor) // this is the default case
{ // Disable Redo for non-Undo/Redo commands...
while (m_nRedoSeqs > 0) { delete m_seqs.GetHead(); m_seqs.RemoveHead(); m_nRedoSeqs -= 1; } }
m_pCurSeq = new CBmObjSequence; m_pCurSeq->m_strDescription = szCmd;
m_bFlushAtEnd = FALSE; }
// In most cases, this overloaded function will be called. It takes a
// resource ID instead of a char*, allowing easier internationalization
//
void CUndoBmObj::BeginUndo(const UINT idCmd, BOOL bResetCursor) { CString strCmd; VERIFY(strCmd.LoadString(idCmd));
BeginUndo(strCmd, bResetCursor); }
// Call this at the end of an undoable user action to cause the sequence
// since the BeginUndo to be stored in the undo buffer.
//
void CUndoBmObj::EndUndo() { #ifdef _DEBUG
if (theApp.m_bLogUndo) TRACE1("EndUndo: %d\n", m_nRecording - 1); #endif
ASSERT(m_nRecording > 0);
// Handle nesting
m_nRecording -= 1; if (m_nRecording != 0) return;
if (!m_pCurSeq->IsUseful(m_pLastSlob, m_nLastPropID)) { // Remove empty or otherwise useless undo records!
delete m_pCurSeq; m_pCurSeq = NULL; return; }
// We'll keep it, add it to the list...
if (m_nRedoSeqs > 0) { // Add AFTER any redo sequences we have but before any undo's
POSITION pos = m_seqs.FindIndex(m_nRedoSeqs - 1); ASSERT(pos != NULL); m_seqs.InsertAfter(pos, m_pCurSeq); } else { // Just add before any other undo sequences
m_seqs.AddHead(m_pCurSeq); } m_pCurSeq = NULL;
Truncate(); // Make sure the undo buffer doesn't get too big!
if (m_bFlushAtEnd) Flush(); }
// This functions ensures there aren't too many levels in the buffer.
//
void CUndoBmObj::Truncate() { POSITION pos = m_seqs.FindIndex(m_nRedoSeqs + m_nMaxLevels); while (pos != NULL) { #ifdef _DEBUG
if (theApp.m_bLogUndo) TRACE(TEXT("Undo record fell off the edge...\n")); #endif
POSITION posRemove = pos; delete m_seqs.GetNext(pos); m_seqs.RemoveAt(posRemove); } }
// Call this to perform an undo command.
//
void CUndoBmObj::DoUndo() { CWaitCursor waitCursor;
if (m_nRedoSeqs == m_seqs.GetCount()) return; // nothing to undo!
m_bPerformingUndoRedo = TRUE;
POSITION pos = m_seqs.FindIndex(m_nRedoSeqs); ASSERT(pos != NULL); CBmObjSequence* pSeq = (CBmObjSequence*)m_seqs.GetAt(pos);
BeginUndo(pSeq->m_strDescription, FALSE); // Setup Redo
// Remove this sequence after BeginUndo so the one inserted
// there goes to the right place...
m_seqs.RemoveAt(pos);
pSeq->Apply();
FlushLast(); EndUndo(); FlushLast();
m_bPerformingUndoRedo = FALSE;
delete pSeq;
// Do not bump the redo count if the undo flushed the buffer! (This
// happens when a resource is pasted/dropped, then opened, then a
// property in it changes, and the user undoes back to before the
// paste.)
if (m_seqs.GetCount() != 0) m_nRedoSeqs += 1;
}
// Call this to perform a redo command.
//
void CUndoBmObj::DoRedo() { if (m_nRedoSeqs == 0) return; // nothing in redo buffer
m_nRedoSeqs -= 1; DoUndo();
// Do not drop the redo count if the undo flushed the buffer! (This
// happens when a resource is pasted/dropped, then opened, then a
// property in it changes, and the user undoes back to before the
// paste.)
if (m_seqs.GetCount() != 0) m_nRedoSeqs -= 1; }
// Generate a string appropriate for the undo menu command.
//
void CUndoBmObj::GetUndoString(CString& strUndo) { static CString NEAR strUndoTemplate;
if (strUndoTemplate.IsEmpty()) VERIFY(strUndoTemplate.LoadString(IDS_UNDO));
CString strUndoCmd;
if (CanUndo()) { POSITION pos = m_seqs.FindIndex(m_nRedoSeqs); strUndoCmd = ((CBmObjSequence*)m_seqs.GetAt(pos))->m_strDescription; }
int cchUndo = strUndoTemplate.GetLength() - 2; // less 2 for "%s"
wsprintf(strUndo.GetBufferSetLength(cchUndo + strUndoCmd.GetLength()), strUndoTemplate, (const TCHAR*)strUndoCmd); }
// Generate a string appropriate for the redo menu command.
//
void CUndoBmObj::GetRedoString(CString& strRedo) { static CString NEAR strRedoTemplate;
if (strRedoTemplate.IsEmpty()) VERIFY(strRedoTemplate.LoadString(IDS_REDO));
CString strRedoCmd;
if (CanRedo()) { POSITION pos = m_seqs.FindIndex(m_nRedoSeqs - 1); strRedoCmd = ((CBmObjSequence*)m_seqs.GetAt(pos))->m_strDescription; }
int cchRedo = strRedoTemplate.GetLength() - 2; // less 2 for "%s"
wsprintf(strRedo.GetBufferSetLength(cchRedo + strRedoCmd.GetLength()), strRedoTemplate, (const TCHAR*)strRedoCmd); }
// Call this to completely empty the undo buffer.
//
void CUndoBmObj::Flush() { PreTerminateList(&m_seqs);
m_cbUndo = 0; m_nRedoSeqs = 0;
m_bFlushAtEnd = TRUE; }
void CUndoBmObj::OnInform(CBitmapObj* pChangedSlob, UINT idChange) { if (idChange == SN_DESTROY) { // When a slob we have a reference to is deleted (for real), we
// have no choice but to flush the whole buffer... This normally
// only happens when a resource editor window is closed... (If
// the slob's container is the undo buffer, then we are already
// in the process of flushing, so don't recurse!)
Flush(); }
CBitmapObj::OnInform(pChangedSlob, idChange); }
//
// The following functions are used by the CBitmapObj code to insert commands
// into the undo/redo sequence currently being recorded. All of the On...
// functions are used to record changes to the various types of properties
// and are called by the CBitmapObj::Set...Prop functions exclusively.
//
// Insert an array of bytes.
//
UINT CUndoBmObj::Insert(const void* pv, int cb) { ASSERT(m_pCurSeq != NULL);
BYTE* rgb = (BYTE*)pv;
m_pCurSeq->InsertAt(0, 0, cb);
for (int ib = 0; ib < cb; ib += 1) m_pCurSeq->SetAt(ib, *rgb++);
return cb; }
// Insert a string.
//
UINT CUndoBmObj::InsertStr(const TCHAR* sz) { ASSERT(m_pCurSeq != NULL);
BYTE* pb = (BYTE*)sz; int nStrLen = lstrlen(sz);
InsertInt(nStrLen); if (nStrLen > 0) { m_pCurSeq->InsertAt(sizeof (int), 0, nStrLen); for (int nByte = 0; nByte < nStrLen; nByte += 1) m_pCurSeq->SetAt(sizeof (int) + nByte, *pb++); } return nStrLen + sizeof (int); }
void CUndoBmObj::OnSetIntProp(CBitmapObj* pChangedSlob, UINT nPropID, UINT nOldVal) { ASSERT(m_nRecording != 0);
CIntUndoRecord undoRecord; undoRecord.m_op = opIntProp; undoRecord.m_pBitmapObj = pChangedSlob; undoRecord.m_nPropID = nPropID; undoRecord.m_nOldVal = nOldVal; Insert(&undoRecord, sizeof (undoRecord)); pChangedSlob->AddDependant(this); }
#ifdef _DEBUG
/////////////////////////////////////////////////////////////////////////////
//
// Undo related debugging aids
//
void CBmObjSequence::Dump() { m_nCursor = 0; while (m_nCursor < GetSize()) { BYTE op; CBitmapObj* pSlob; int nPropID;
RetrieveByte(op); RetrievePtr(pSlob); RetrieveInt(nPropID);
switch (op) { default: TRACE1("Illegal undo opcode (%d)\n", op); ASSERT(FALSE);
case CUndoBmObj::opAction: { int cbUndoRecord; RetrieveInt(cbUndoRecord); m_nCursor += cbUndoRecord;
TRACE3("opAction: pSlob = 0x%08lx, nActionID = %d, " TEXT("nBytes = %d\n"), pSlob, nPropID, cbUndoRecord); } break;
case CUndoBmObj::opIntProp: case CUndoBmObj::opBoolProp: { int val; RetrieveInt(val); TRACE3("opInt: pSlob = 0x%08lx, nPropID = %d, val = %d\n", pSlob, nPropID, val); } break;
case CUndoBmObj::opLongProp: { long val; RetrieveLong(val); TRACE3("opInt: pSlob = 0x%08lx, nPropID = %d, val = %ld\n", pSlob, nPropID, val); } break;
case CUndoBmObj::opDoubleProp: { double num; RetrieveNum(num); TRACE3("opInt: pSlob = 0x%08lx, nPropID = %d, val = %f\n", pSlob, nPropID, num); } break;
case CUndoBmObj::opStrProp: { CString str; RetrieveStr(str); if (str.GetLength() > 80) { str = str.Left(80); str += TEXT("..."); } TRACE3("opStr: pSlob = 0x%08lx, nPropID = %d, val = %s\n", pSlob, nPropID, (const TCHAR*)str); } break;
case CUndoBmObj::opSlobProp: { CBitmapObj* pSlobVal; RetrievePtr(pSlobVal); TRACE3("opInt: pSlob = 0x%08lx, nPropID = %d, " TEXT("val = 0x%08lx\n"), pSlob, nPropID, pSlobVal); } break;
case CUndoBmObj::opRectProp: { CRect rcVal; RetrieveRect(rcVal); TRACE3("opRect: pSlob = 0x%08lx, nPropID = %d, " TEXT("val = %d,%d,%d,%d\n"), pSlob, nPropID, rcVal); } break;
case CUndoBmObj::opPointProp: { CPoint ptVal; RetrievePoint(ptVal); TRACE3("opPoint: pSlob = 0x%08lx, nPropID = %d, " TEXT("val = %d,%d,%d,%d\n"), pSlob, nPropID, ptVal); } break; } } }
void CUndoBmObj::Dump() { int nRecord = 0; POSITION pos = m_seqs.GetHeadPosition(); while (pos != NULL) { CBmObjSequence* pSeq = (CBmObjSequence*)m_seqs.GetNext(pos); TRACE2("Record (%d) %s:\n", nRecord, nRecord < m_nRedoSeqs ? TEXT("redo") : TEXT("undo")); pSeq->Dump(); nRecord += 1; } }
extern "C" void DumpUndo() { theUndo.Dump(); }
#endif
|