An Undo/Redo Manager

The design for this system is based on Visual C++/MFC code by Jim Beveridge in Dr. Dobb's Journal, February 1996. The following components are the elements of a powerful and flexible undo/redo mechanism. The system is based on the abstract notion of tasks.


Each task has 4 methods, applications are expected to classify all undoable tasks and to subclass the C_TASK class to provide task-specific behaviors:


The undo manager maintains the list and current undo/redo position, and under the direction of the application's command manager, will carry out undo/redo of tasks:



See UNDOMAN in action - used in Solipeg 2.1

Since solipeg is relatively simple, the Solipeg C_JUMP class, relegates all of its work to the engine, sending the engine an O_ENG_JUMP message. In more complicated applications with more tasks, it is more likely that the engine will also provide more atomic or elemental services, which the task would call several of. For example, the code for O_ENG_JUMP is relatively simple, yet accesses much private engine data:

METHOD VOID soleng_eng_jump(    PR_SOLENG * self,
                                INT         x,
                                INT         y,
                                INT         iDir ) {
    BOARD[x][y] = S_EMPTY ;
    BOARD[x + vectors[iDir].x][y + vectors[iDir].y] = S_EMPTY ;
    BOARD[x + 2 * vectors[iDir].x][y + 2 * vectors[iDir].y] = S_FULL ;

    p_send3(w_ws->wserv.com, O_SCM_SPOS, XY(x + 2 * vectors[iDir].x, y + 2 * vectors[iDir].y)) ;
    p_send2(w_ws->wserv.cli, O_WN_CURSOR) ;
    p_send2(w_ws->wserv.com, O_SCM_UPDATE) ;
}


Class Definition

! Abstract type to represent tasks
CLASS task root
{
    DEFER       task_init
    DEFER       task_do
    DEFER       task_undo
    DEFER       task_text
}

! The only kind of task in Solipeg is a jump
CLASS jump task
{
    REPLACE     task_init
    REPLACE     task_do
    REPLACE     task_undo
    REPLACE     task_text

    PROPERTY
    {
        UBYTE   x ;
        UBYTE   y ;
        UBYTE   dir ;
    }
}

! The task undo manager
CLASS undoman root
{
    ADD     uman_init
    ADD     uman_submit
    ADD     uman_undo
    ADD     uman_redo
    ADD     uman_canredo
    ADD     uman_canundo
    ADD     uman_redotxt
    ADD     uman_undotxt
    ADD     uman_clear

    PROPERTY 1
    {
        PR_VAFLAT *     pList ;
        VOID *          pEngine ;
        INT             UndoIndex ;
    }
}


Undo/Redo Manager Methods

// UNDOMAN.C - Undo manager
// This file includes methods for:
// C_UNDOMAN: the undo manager

#define this    (self->undoman)

//  Macro:      TASK_PTR(index)
//  Returns:    &TASK[index] (A PR_TASK * in VAFLAT)
#define TASK_PTR(index) (*(PR_TASK **)p_send3(this.pList, O_VA_PREC, index))

// Initaliaze the undoman object, using category and class specified for
// the list manager
METHOD VOID undoman_uman_init(  PR_UNDOMAN *    self,
                                VOID *          pEngine,
                                INT             iCat,
                                INT             iClass ) {
    this.pEngine = pEngine ;
    this.pList = f_newsend( iCat,
                            iClass,
                            O_VA_INIT,
                            sizeof(PR_TASK *),
                            10 ) ;
    this.UndoIndex = 0 ;
}

// Submit a task - the task is sent a DO message
METHOD VOID undoman_uman_submit(    PR_UNDOMAN *    self,
                                    PR_TASK *       pTask ) {
    INT         nCount ;

    nCount = p_send2(this.pList, O_VA_COUNT) ;

    while ( this.UndoIndex < nCount ) {
        nCount-- ;
        p_send2(TASK_PTR(nCount), O_DESTROY) ;
        p_send3(this.pList, O_VA_DELETE, nCount) ;
    }

    p_send3(this.pList, O_VA_APPEND, &pTask) ;
    this.UndoIndex++ ;
    p_send3(pTask, O_TASK_DO, this.pEngine) ;
}

// Undo from the current position
METHOD VOID undoman_uman_undo(PR_UNDOMAN *self) {
    if ( p_send2(self, O_UMAN_CANUNDO) ) {
        this.UndoIndex-- ;
        p_send3(TASK_PTR(this.UndoIndex), O_TASK_UNDO, this.pEngine) ;
    }
}

// Redo from the current position
METHOD VOID undoman_uman_redo(PR_UNDOMAN *self) {
    if ( p_send2(self, O_UMAN_CANREDO) ) {
        p_send3(TASK_PTR(this.UndoIndex), O_TASK_DO, this.pEngine) ;
        this.UndoIndex++ ;
    }
}

// Is Redo allowed?
METHOD INT undoman_uman_canredo(PR_UNDOMAN *self) {
    return( this.UndoIndex < p_send2(this.pList, O_VA_COUNT) ) ;
}

// Is Undo allowed?
METHOD INT undoman_uman_canundo(PR_UNDOMAN *self) {
    return( this.UndoIndex > 0 ) ;
}

// Get text describing the Redo operation
METHOD VOID undoman_uman_redotxt(PR_UNDOMAN *self, TEXT *str) {
    if ( p_send2(self, O_UMAN_CANREDO) ) {
        p_send3(TASK_PTR(this.UndoIndex), O_TASK_TEXT, str) ;
    }
    else {
        *str = 0 ;
    }
}

// Get text describing the Undo operation
METHOD VOID undoman_uman_undotxt(PR_UNDOMAN *self, TEXT *str) {
    if ( p_send2(self, O_UMAN_CANUNDO) ) {
        p_send3(TASK_PTR(this.UndoIndex - 1), O_TASK_TEXT, str) ;
    }
    else {
        *str = 0 ;
    }
}

// Clear the undo history
// Fixed for Solipeg 2.1
METHOD VOID undoman_uman_clear(PR_UNDOMAN *self) {
    this.UndoIndex = p_send2(this.pList, O_VA_COUNT) ;

    while ( this.UndoIndex > 0 ) {
        this.UndoIndex-- ;
        p_send2(TASK_PTR(this.UndoIndex), O_DESTROY) ;
    }
    p_send2(this.pList, O_VA_RESET) ;
}


Sample Task Object Methods

// SOLTASK.C - Solipeg tasks
// This file includes methods for:
// Solipeg tasks

#undef this
#define this    (self->jump)

// Initialize an object of class jump
METHOD VOID jump_task_init( PR_JUMP *   self,
                            INT         x,
                            INT         y,
                            INT         dir ) {
    this.x = x ;
    this.y = y ;
    this.dir = dir ;
}

// Do a jump
METHOD VOID jump_task_do(   PR_JUMP *   self,
                            PR_SOLENG * engine ) {
    p_send5(engine, O_ENG_JUMP, this.x, this.y, this.dir) ;
}

// Undo a jump
METHOD VOID jump_task_undo( PR_JUMP *   self,
                            PR_SOLENG * engine ) {
    p_send5(engine, O_ENG_UNJUMP, this.x, this.y, this.dir) ;
}

// Return a suitable description of a jump
METHOD VOID jump_task_text( PR_JUMP *   self,
                            TEXT *      str ) {
    hLoadResBuf(RS_TASK_JUMP, str) ;
}



Back to My home page
© 1996 J. C. Roux