pjs/lib/libmisc/undo.c

445 строки
9.3 KiB
C

/* -*- Mode: C; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*-
*
* The contents of this file are subject to the Netscape Public License
* Version 1.0 (the "NPL"); you may not use this file except in
* compliance with the NPL. You may obtain a copy of the NPL at
* http://www.mozilla.org/NPL/
*
* Software distributed under the NPL is distributed on an "AS IS" basis,
* WITHOUT WARRANTY OF ANY KIND, either express or implied. See the NPL
* for the specific language governing rights and limitations under the
* NPL.
*
* The Initial Developer of this code under the NPL is Netscape
* Communications Corporation. Portions created by Netscape are
* Copyright (C) 1998 Netscape Communications Corporation. All Rights
* Reserved.
*/
/*
undo.c --- undo facilities
*/
#include "xp.h" /* For XP_Bool... */
#include "undo.h"
extern int MK_OUT_OF_MEMORY;
typedef enum {
BOUNDARY,
EVENT
} undo_event_type;
typedef struct undo_event
{
undo_event_type type;
struct undo_event* next;
struct undo_event* prev;
void* closure;
int (*undoit)(void*);
void (*freeit)(void*);
void* tag;
void (*freetag)(void*);
} undo_event;
struct UndoState {
undo_event* events;
undo_event* redoevents;
int depth;
XP_Bool undoing;
XP_Bool redoing;
XP_Bool loggedsomething;
int count;
XP_AllocStructInfo allocinfo;
int maxdepth;
XP_Bool discardall;
};
#ifdef DEBUG
static void
undo_check_integrity(UndoState* state)
{
int i;
for (i=0 ; i<2 ; i++) {
undo_event** start;
undo_event* tmp;
int count1, count2;
if (i == 0) {
start = &(state->redoevents);
} else {
start = &(state->events);
}
if (*start) {
count1 = 0;
for (tmp = (*start)->next ; tmp != *start ; tmp = tmp->next) {
count1++;
}
count2 = 0;
for (tmp = (*start)->prev ; tmp != *start ; tmp = tmp->prev) {
count2++;
}
XP_ASSERT(count1 == count2);
}
}
}
#else
#define undo_check_integrity(state) /* no-op */
#endif
UndoState*
UNDO_Create(int maxdepth)
{
UndoState* state = XP_NEW_ZAP(UndoState);
if (state) {
state->maxdepth = maxdepth;
XP_InitAllocStructInfo(&(state->allocinfo), sizeof(undo_event));
}
return state;
}
static void
undo_free_list(UndoState* state, undo_event** list)
{
undo_event* tmp;
undo_event* next;
undo_check_integrity(state);
if (*list) {
(*list)->prev->next = NULL;
for (tmp = *list ; tmp ; tmp = next) {
next = tmp->next;
if (tmp->freeit) (*tmp->freeit)(tmp->closure);
if (tmp->freetag) (*tmp->freetag)(tmp->tag);
XP_FreeStruct(&state->allocinfo, tmp);
}
*list = NULL;
}
undo_check_integrity(state);
}
void
UNDO_Destroy(UndoState* state)
{
undo_free_list(state, &state->events);
undo_free_list(state, &state->redoevents);
XP_FreeAllStructs(&state->allocinfo);
XP_FREE(state);
}
void
UNDO_DiscardAll(UndoState* state)
{
undo_free_list(state, &state->events);
undo_free_list(state, &state->redoevents);
if (state->depth > 0) state->discardall = TRUE;
state->count = 0;
}
static int
undo_log(UndoState* state, undo_event* event)
{
undo_event** start;
int status;
undo_check_integrity(state);
if (event->type != BOUNDARY && state->depth == 0) {
UNDO_StartBatch(state);
status = undo_log(state, event);
if (status < 0) {
state->depth = 0;
return status;
}
return UNDO_EndBatch(state, event->freetag, event->tag);
}
if (state->undoing) {
start = &(state->redoevents);
} else {
start = &(state->events);
}
if (!*start) {
event->next = event;
event->prev = event;
} else {
event->next = *start;
event->prev = event->next->prev;
event->next->prev = event;
event->prev->next = event;
}
*start = event;
undo_check_integrity(state);
return 0;
}
int
UNDO_LogEvent(UndoState* state, int (*undoit)(void*),
void (*freeit)(void*), void* closure,
void (*freetag)(void*), void* tag)
{
undo_event* tmp;
if (state->discardall) {
(*freeit)(closure);
if (freetag) (*freetag)(tag);
return 0;
}
tmp = (undo_event*) XP_AllocStructZero(&state->allocinfo);
if (!tmp) {
UNDO_DiscardAll(state);
(*freeit)(closure);
if (freetag) (*freetag)(tag);
return MK_OUT_OF_MEMORY;
}
state->loggedsomething = TRUE;
tmp->type = EVENT;
tmp->undoit = undoit;
tmp->freeit = freeit;
tmp->closure = closure;
tmp->tag = tag;
tmp->freetag = freetag;
return undo_log(state, tmp);
}
int
UNDO_StartBatch(UndoState* state)
{
#if 0 /* This is bad. The redo stack is not purged when a new event is logged. */
if (state->depth == 0) {
#else /* A better way: Only reset loggedsomething if we're in the midst of an undo or redo. */
if( state->undoing || state->redoing ) {
#endif
state->loggedsomething = FALSE;
}
state->depth++;
undo_check_integrity(state);
return 0;
}
int
UNDO_EndBatch(UndoState* state, void (*freetag)(void*), void* tag)
{
int status;
undo_check_integrity(state);
XP_ASSERT(state->depth > 0);
state->depth--;
if (state->depth == 0) {
undo_event** start;
if (state->discardall) {
UNDO_DiscardAll(state);
state->discardall = FALSE;
return 0;
}
if (state->undoing) {
start = &(state->redoevents);
} else {
start = &(state->events);
if (state->loggedsomething) {
undo_free_list(state, &state->redoevents);
}
}
if (*start && (*start)->type != BOUNDARY) {
undo_event* tmp = (undo_event*)XP_AllocStructZero(&state->allocinfo);
if (!tmp) {
UNDO_DiscardAll(state);
return MK_OUT_OF_MEMORY;
}
tmp->type = BOUNDARY;
tmp->tag = tag;
tmp->freetag = freetag;
status = undo_log(state, tmp);
if (status < 0) {
UNDO_DiscardAll(state);
return status;
}
if (!state->undoing) {
if (state->count >= state->maxdepth) {
/* exceeded undo count - pull one off the bottom of the stack. */
undo_event* prev;
for (tmp = state->events->prev ; ; tmp = prev) {
undo_event_type type = tmp->type;
/* better not be at the top of the stack, */
XP_ASSERT(tmp != state->events);
prev = tmp->prev;
tmp->prev->next = tmp->next;
tmp->next->prev = tmp->prev;
if (type == EVENT) {
if (tmp->freeit) (*tmp->freeit)(tmp->closure);
if (tmp->freetag) (*tmp->freetag)(tmp->tag);
}
XP_FreeStruct(&state->allocinfo, tmp);
/* stop at the next boundary, which makes a whole Batch */
if (type == BOUNDARY) break;
}
}
else
{
state->count++;
}
}
}
undo_check_integrity(state);
}
return 0;
}
static int
undo_doone(UndoState* state, undo_event** from)
{
int status = 0;
undo_event* tmp = *from;
undo_check_integrity(state);
XP_ASSERT(tmp != NULL);
switch (tmp->type) {
case BOUNDARY:
break;
case EVENT:
if (tmp->undoit) {
status = (*tmp->undoit)(tmp->closure);
}
break;
default:
XP_ASSERT(0);
}
*from = tmp->next;
if (*from == tmp) {
*from = NULL;
} else {
(*from)->prev = tmp->prev;
(*from)->prev->next = (*from);
}
undo_check_integrity(state);
XP_FreeStruct(&state->allocinfo, tmp);
return status;
}
static int
undo_doit(UndoState* state, undo_event** from)
{
int status;
void (*tmp_freetag)(void*);
void *tmp_tag;
XP_ASSERT(state->depth == 0);
if (!*from) return 0;
status = UNDO_StartBatch(state);
if (status < 0) return status;
XP_ASSERT((*from)->type == BOUNDARY);
/* Save the tag information so it persists in the opposing undo/redo stack */
tmp_freetag = (*from)->freetag;
tmp_tag = (*from)->tag;
do {
status = undo_doone(state, from);
if (status < 0) break;
} while (*from && (*from)->type != BOUNDARY);
if (status >= 0) status = UNDO_EndBatch(state, tmp_freetag, tmp_tag);
if (status < 0) {
UNDO_DiscardAll(state);
}
return status;
}
int
UNDO_DoUndo(UndoState* state)
{
int status;
XP_ASSERT(state->depth == 0 && !state->undoing);
state->undoing = TRUE;
status = undo_doit(state, &(state->events));
state->undoing = FALSE;
state->count--;
return status;
}
int
UNDO_DoRedo(UndoState* state)
{
int status;
undo_event* tmp = state->redoevents;
state->redoevents = NULL; /* Prevent any code from throwing away the
remaining redo events. */
XP_ASSERT(state->depth == 0 && !state->undoing);
state->redoing = TRUE;
status = undo_doit(state, &tmp);
state->redoing = FALSE;
XP_ASSERT(state->redoevents == NULL);
state->redoevents = tmp;
return status;
}
static XP_Bool
undo_has_event(undo_event* event)
{
undo_event* tmp = event;
if (tmp) {
do {
if (tmp->type == EVENT) return TRUE;
tmp = tmp->next;
} while (tmp != event);
}
return FALSE;
}
XP_Bool
UNDO_CanUndo(UndoState* state)
{
return undo_has_event(state->events);
}
XP_Bool
UNDO_CanRedo(UndoState* state)
{
return undo_has_event(state->redoevents);
}
static void *
undo_get_tag(undo_event* event)
{
undo_event* tmp = event;
if (tmp) {
do {
if (tmp->type == BOUNDARY)
return tmp->tag;
tmp = tmp->next;
} while (tmp != event);
}
return NULL;
}
void *
UNDO_PeekUndoTag(UndoState* state)
{
return state ? undo_get_tag(state->events) : NULL;
}
void *
UNDO_PeekRedoTag(UndoState* state)
{
return state ? undo_get_tag(state->redoevents) : NULL;
}