зеркало из https://github.com/mozilla/pjs.git
445 строки
9.3 KiB
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;
|
||
|
}
|