windows-nt/Source/XPSP1/NT/shell/osshell/accesory/mspaint/undo.cpp
2020-09-26 16:20:57 +08:00

751 lines
18 KiB
C++

#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