view dwtx/core/commands/operations/DefaultOperationHistory.d @ 40:da5ad8eedf5d

debug prints, dwt.dwthelper restructure, ...
author Frank Benoit <benoit@tionex.de>
date Thu, 10 Apr 2008 08:59:39 +0200
parents 644f1334b451
children 04b47443bb01
line wrap: on
line source

/*******************************************************************************
 * Copyright (c) 2005, 2007 IBM Corporation and others.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *     IBM Corporation - initial API and implementation
 * Port to the D programming language:
 *     Frank Benoit <benoit@tionex.de>
 *******************************************************************************/
module dwtx.core.commands.operations.DefaultOperationHistory;

import tango.util.collection.ArraySeq;
import tango.util.collection.model.Seq;
import tango.util.collection.HashMap;
import tango.util.collection.model.Map;

import dwtx.core.commands.ExecutionException;
import dwtx.core.commands.util.Tracing;
import dwtx.core.runtime.Assert;
import dwtx.core.runtime.IAdaptable;
import dwtx.core.runtime.IProgressMonitor;
import dwtx.core.runtime.ISafeRunnable;
import dwtx.core.runtime.IStatus;
import dwtx.core.runtime.ListenerList;
import dwtx.core.runtime.OperationCanceledException;
import dwtx.core.runtime.SafeRunner;
import dwtx.core.runtime.Status;

import dwtx.core.commands.operations.IOperationHistory;
import dwtx.core.commands.operations.IUndoContext;
import dwtx.core.commands.operations.IUndoableOperation;
import dwtx.core.commands.operations.ICompositeOperation;
import dwtx.core.commands.operations.IOperationApprover;
import dwtx.core.commands.operations.IOperationApprover2;
import dwtx.core.commands.operations.IOperationHistoryListener;
import dwtx.core.commands.operations.OperationHistoryEvent;
import dwtx.core.commands.operations.IAdvancedUndoableOperation;

import dwt.dwthelper.utils;

/**
 * <p>
 * A base implementation of IOperationHistory that implements a linear undo and
 * redo model . The most recently added operation is available for undo, and the
 * most recently undone operation is available for redo.
 * </p>
 * <p>
 * If the operation eligible for undo is not in a state where it can be undone,
 * then no undo is available. No other operations are considered. Likewise, if
 * the operation available for redo cannot be redone, then no redo is available.
 * </p>
 * <p>
 * Implementations for the direct undo and redo of a specified operation are
 * available. If a strict linear undo is to be enforced, than an
 * IOperationApprover should be installed that prevents undo and redo of any
 * operation that is not the most recently undone or redone operation in all of
 * its undo contexts.
 * </p>
 * <p>
 * The data structures used by the DefaultOperationHistory are synchronized, and
 * entry points that modify the undo and redo history concurrently are also
 * synchronized. This means that the DefaultOperationHistory is relatively
 * "thread-friendly" in its implementation. Outbound notifications or operation
 * approval requests will occur on the thread that initiated the request.
 * Clients may use DefaultOperationHistory API from any thread; however,
 * listeners or operation approvers that receive notifications from the
 * DefaultOperationHistory must be prepared to receive these notifications from
 * a background thread. Any UI access occurring inside these notifications must
 * be properly synchronized using the techniques specified by the client's
 * widget library.
 * </p>
 *
 * <p>
 * This implementation is not intended to be subclassed.
 * </p>
 *
 * @see dwtx.core.commands.operations.IOperationHistory
 * @see dwtx.core.commands.operations.IOperationApprover
 *
 * @since 3.1
 */
public final class DefaultOperationHistory : IOperationHistory {
    /**
     * This flag can be set to <code>true</code> if the history should print
     * information to <code>System.out</code> whenever notifications about
     * changes to the history occur. This flag should be used for debug purposes
     * only.
     */
    public static bool DEBUG_OPERATION_HISTORY_NOTIFICATION = false;

    /**
     * This flag can be set to <code>true</code> if the history should print
     * information to <code>System.out</code> whenever an unexpected condition
     * arises. This flag should be used for debug purposes only.
     */
    public static bool DEBUG_OPERATION_HISTORY_UNEXPECTED = false;

    /**
     * This flag can be set to <code>true</code> if the history should print
     * information to <code>System.out</code> whenever an undo context is
     * disposed. This flag should be used for debug purposes only.
     */
    public static bool DEBUG_OPERATION_HISTORY_DISPOSE = false;

    /**
     * This flag can be set to <code>true</code> if the history should print
     * information to <code>System.out</code> during the open/close sequence.
     * This flag should be used for debug purposes only.
     */
    public static bool DEBUG_OPERATION_HISTORY_OPENOPERATION = false;

    /**
     * This flag can be set to <code>true</code> if the history should print
     * information to <code>System.out</code> whenever an operation is not
     * approved. This flag should be used for debug purposes only.
     */
    public static bool DEBUG_OPERATION_HISTORY_APPROVAL = false;

    static const int DEFAULT_LIMIT = 20;

    /**
     * the list of {@link IOperationApprover}s
     */
    ListenerList approvers;

    /**
     * a map of undo limits per context
     */
    private Map!(IUndoContext,Integer) limits;

    /**
     * the list of {@link IOperationHistoryListener}s
     */
    ListenerList listeners;

    /**
     * the list of operations available for redo, LIFO
     */
    private ArraySeq!(IUndoableOperation) redoList;

    /**
     * the list of operations available for undo, LIFO
     */
    private ArraySeq!(IUndoableOperation) undoList;

    /**
     * a lock that is used to synchronize access between the undo and redo
     * history
     */
    final Object undoRedoHistoryLock;

    /**
     * An operation that is "absorbing" all other operations while it is open.
     * When this is not null, other operations added or executed are added to
     * this composite.
     *
     */
    private ICompositeOperation openComposite = null;

    /**
     * a lock that is used to synchronize access to the open composite.
     */
    const Object openCompositeLock;

    /**
     * Create an instance of DefaultOperationHistory.
     */
    public this() {
        openCompositeLock = new Object();
        undoRedoHistoryLock = new Object();
        approvers = new ListenerList(ListenerList.IDENTITY);
        limits = new HashMap!(IUndoContext,Integer);
        listeners = new ListenerList(ListenerList.IDENTITY);
        redoList = new ArraySeq!(IUndoableOperation);
        undoList = new ArraySeq!(IUndoableOperation);
    }

    /*
     * (non-Javadoc)
     *
     * @see dwtx.core.commands.operations.IOperationHistory#add(dwtx.core.commands.operations.IUndoableOperation)
     */
    public void add(IUndoableOperation operation) {
        Assert.isNotNull( cast(Object)operation);

        /*
         * If we are in the middle of executing an open batching operation, and
         * this is not that operation, then we need only add the context of the
         * new operation to the batch. The operation itself is disposed since we
         * will never undo or redo it. We consider it to be triggered by the
         * batching operation and assume that its undo will be triggered by the
         * batching operation undo.
         */
        synchronized (openCompositeLock) {
            if (openComposite !is null && openComposite !is operation) {
                openComposite.add(operation);
                return;
            }
        }

        if (checkUndoLimit(operation)) {
            synchronized (undoRedoHistoryLock) {
                undoList.append(operation);
            }
            notifyAdd(operation);

            // flush redo stack for related contexts
            IUndoContext[] contexts = operation.getContexts();
            for (int i = 0; i < contexts.length; i++) {
                flushRedo(contexts[i]);
            }
        } else {
            // Dispose the operation since we will not have a reference to it.
            operation.dispose();
        }
    }

    /**
     * <p>
     * Add the specified approver to the list of operation approvers consulted
     * by the operation history before an undo or redo is allowed to proceed.
     * This method has no effect if the instance being added is already in the
     * list.
     * </p>
     * <p>
     * Operation approvers must be prepared to receive these the operation
     * approval messages from a background thread. Any UI access occurring
     * inside the implementation must be properly synchronized using the
     * techniques specified by the client's widget library.
     * </p>
     *
     * @param approver
     *            the IOperationApprover to be added as an approver.
     *
     */

    public void addOperationApprover(IOperationApprover approver) {
        approvers.add(cast(Object)approver);
    }

    /**
     * <p>
     * Add the specified listener to the list of operation history listeners
     * that are notified about changes in the history or operations that are
     * executed, undone, or redone. This method has no effect if the instance
     * being added is already in the list.
     * </p>
     * <p>
     * Operation history listeners must be prepared to receive notifications
     * from a background thread. Any UI access occurring inside the
     * implementation must be properly synchronized using the techniques
     * specified by the client's widget library.
     * </p>
     *
     * @param listener
     *            the IOperationHistoryListener to be added as a listener.
     *
     * @see dwtx.core.commands.operations.IOperationHistoryListener
     * @see dwtx.core.commands.operations.OperationHistoryEvent
     */
    public void addOperationHistoryListener(IOperationHistoryListener listener) {
        listeners.add(cast(Object)listener);
    }

    /*
     * (non-Javadoc)
     *
     * @see dwtx.core.commands.operations.IOperationHistory#canRedo(dwtx.core.commands.operations.IUndoContext)
     */
    public bool canRedo(IUndoContext context) {
        // null context is allowed and passed through
        IUndoableOperation operation = getRedoOperation(context);
        return (operation !is null && operation.canRedo());
    }

    /*
     * (non-Javadoc)
     *
     * @see dwtx.core.commands.operations.IOperationHistory#canUndo(dwtx.core.commands.operations.IUndoContext)
     */
    public bool canUndo(IUndoContext context) {
        // null context is allowed and passed through
        IUndoableOperation operation = getUndoOperation(context);
        return (operation !is null && operation.canUndo());
    }

    /**
     * Check the redo limit before adding an operation. In theory the redo limit
     * should never be reached, because the redo items are transferred from the
     * undo history, which has the same limit. The redo history is cleared
     * whenever a new operation is added. We check for completeness since
     * implementations may change over time.
     *
     * Return a bool indicating whether the redo should proceed.
     */
    private bool checkRedoLimit(IUndoableOperation operation) {
        IUndoContext[] contexts = operation.getContexts();
        for (int i = 0; i < contexts.length; i++) {
            int limit = getLimit(contexts[i]);
            if (limit > 0) {
                forceRedoLimit(contexts[i], limit - 1);
            } else {
                // this context has a 0 limit
                operation.removeContext(contexts[i]);
            }
        }
        return operation.getContexts().length > 0;
    }

    /**
     * Check the undo limit before adding an operation. Return a bool
     * indicating whether the undo should proceed.
     */
    private bool checkUndoLimit(IUndoableOperation operation) {
        IUndoContext[] contexts = operation.getContexts();
        for (int i = 0; i < contexts.length; i++) {
            int limit = getLimit(contexts[i]);
            if (limit > 0) {
                forceUndoLimit(contexts[i], limit - 1);
            } else {
                // this context has a 0 limit
                operation.removeContext(contexts[i]);
            }
        }
        return operation.getContexts().length > 0;
    }

    /*
     * (non-Javadoc)
     *
     * @see dwtx.core.commands.operations.IOperationHistory#dispose(dwtx.core.commands.operations.IUndoContext,
     *      bool, bool, bool)
     */
    public void dispose(IUndoContext context, bool flushUndo_,
            bool flushRedo_, bool flushContext_) {
        // dispose of any limit that was set for the context if it is not to be
        // used again.
        if (flushContext_) {
            if (DEBUG_OPERATION_HISTORY_DISPOSE) {
                Tracing.printTrace("OPERATIONHISTORY", "Flushing context " //$NON-NLS-1$//$NON-NLS-2$
                        ~ (cast(Object)context).toString );
            }
            flushUndo(context);
            flushRedo(context);
            synchronized(limits) limits.removeKey(context);
            return;
        }
        if (flushUndo_) {
            flushUndo(context);
        }
        if (flushRedo_) {
            flushRedo(context);
        }

    }

    /**
     * Perform the redo. All validity checks have already occurred.
     *
     * @param monitor
     * @param operation
     */
    private IStatus doRedo(IProgressMonitor monitor, IAdaptable info,
            IUndoableOperation operation) {

        IStatus status = getRedoApproval(operation, info);
        if (status.isOK()) {
            notifyAboutToRedo(operation);
            try {
                status = operation.redo(monitor, info);
            } catch (OperationCanceledException e) {
                status = Status.CANCEL_STATUS;
            } catch (ExecutionException e) {
                notifyNotOK(operation);
                if (DEBUG_OPERATION_HISTORY_UNEXPECTED) {
                    Tracing.printTrace("OPERATIONHISTORY", //$NON-NLS-1$
                            "ExecutionException while redoing " ~ (cast(Object)operation).toString ); //$NON-NLS-1$
                }
                throw e;
            } catch (Exception e) {
                notifyNotOK(operation);
                if (DEBUG_OPERATION_HISTORY_UNEXPECTED) {
                    Tracing.printTrace("OPERATIONHISTORY", //$NON-NLS-1$
                            "Exception while redoing " ~ (cast(Object)operation).toString); //$NON-NLS-1$
                }
                throw new ExecutionException(
                        "While redoing the operation, an exception occurred", e); //$NON-NLS-1$
            }
        }

        // if successful, the operation is removed from the redo history and
        // placed back in the undo history.
        if (status.isOK()) {
            bool addedToUndo = true;
            synchronized (undoRedoHistoryLock) {
                synchronized(redoList) redoList.remove(operation);
                if (checkUndoLimit(operation)) {
                    undoList.append(operation);
                } else {
                    addedToUndo = false;
                }
            }
            // dispose the operation since we could not add it to the
            // stack and will no longer have a reference to it.
            if (!addedToUndo) {
                operation.dispose();
            }

            // notify listeners must happen after history is updated
            notifyRedone(operation);
        } else {
            notifyNotOK(operation, status);
        }

        return status;
    }

    /**
     * Perform the undo. All validity checks have already occurred.
     *
     * @param monitor
     * @param operation
     */
    private IStatus doUndo(IProgressMonitor monitor, IAdaptable info,
            IUndoableOperation operation) {
        IStatus status = getUndoApproval(operation, info);
        if (status.isOK()) {
            notifyAboutToUndo(operation);
            try {
                status = operation.undo(monitor, info);
            } catch (OperationCanceledException e) {
                status = Status.CANCEL_STATUS;
            } catch (ExecutionException e) {
                notifyNotOK(operation);
                if (DEBUG_OPERATION_HISTORY_UNEXPECTED) {
                    Tracing.printTrace("OPERATIONHISTORY", //$NON-NLS-1$
                            "ExecutionException while undoing " ~ (cast(Object)operation).toString); //$NON-NLS-1$
                }
                throw e;
            } catch (Exception e) {
                notifyNotOK(operation);
                if (DEBUG_OPERATION_HISTORY_UNEXPECTED) {
                    Tracing.printTrace("OPERATIONHISTORY", //$NON-NLS-1$
                            "Exception while undoing " ~ (cast(Object)operation).toString); //$NON-NLS-1$
                }
                throw new ExecutionException(
                        "While undoing the operation, an exception occurred", e); //$NON-NLS-1$
            }
        }
        // if successful, the operation is removed from the undo history and
        // placed in the redo history.
        if (status.isOK()) {
            bool addedToRedo = true;
            synchronized (undoRedoHistoryLock) {
                undoList.remove(operation);
                if (checkRedoLimit(operation)) {
                    synchronized(redoList) redoList.append(operation);
                } else {
                    addedToRedo = false;
                }
            }
            // dispose the operation since we could not add it to the
            // stack and will no longer have a reference to it.
            if (!addedToRedo) {
                operation.dispose();
            }
            // notification occurs after the undo and redo histories are
            // adjusted
            notifyUndone(operation);
        } else {
            notifyNotOK(operation, status);
        }
        return status;
    }

    /*
     * (non-Javadoc)
     *
     * @see dwtx.core.commands.operations.IOperationHistory#execute(dwtx.core.commands.operations.IUndoableOperation,
     *      dwtx.core.runtime.IProgressMonitor,
     *      dwtx.core.runtime.IAdaptable)
     */
    public IStatus execute(IUndoableOperation operation,
            IProgressMonitor monitor, IAdaptable info) {
        Assert.isNotNull(cast(Object)operation);

        // error if operation is invalid
        if (!operation.canExecute()) {
            return IOperationHistory.OPERATION_INVALID_STATUS;
        }

        // check with the operation approvers
        IStatus status = getExecuteApproval(operation, info);
        if (!status.isOK()) {
            // not approved. No notifications are sent, just return the status.
            return status;
        }

        /*
         * If we are in the middle of an open composite, then we will add this
         * operation to the open operation rather than add the operation to the
         * history. We will still execute it.
         */
        bool merging = false;
        synchronized (openCompositeLock) {
            if (openComposite !is null) {
                // the composite shouldn't be executed explicitly while it is
                // still
                // open
                if (openComposite is operation) {
                    return IOperationHistory.OPERATION_INVALID_STATUS;
                }
                openComposite.add(operation);
                merging = true;
            }
        }

        /*
         * Execute the operation
         */
        if (!merging) {
            notifyAboutToExecute(operation);
        }
        try {
            status = operation.execute(monitor, info);
        } catch (OperationCanceledException e) {
            status = Status.CANCEL_STATUS;
        } catch (ExecutionException e) {
            notifyNotOK(operation);
            throw e;
        } catch (Exception e) {
            notifyNotOK(operation);
            throw new ExecutionException(
                    "While executing the operation, an exception occurred", e); //$NON-NLS-1$
        }

        // if successful, the notify listeners are notified and the operation is
        // added to the history
        if (!merging) {
            if (status.isOK()) {
                notifyDone(operation);
                add(operation);
            } else {
                notifyNotOK(operation, status);
                // dispose the operation since we did not add it to the stack
                // and will no longer have a reference to it.
                operation.dispose();
            }
        }
        // all other severities are not interpreted. Simply return the status.
        return status;
    }

    /*
     * Filter the specified list to include only the specified undo context.
     */
    private IUndoableOperation[] filter(Seq!(IUndoableOperation) list, IUndoContext context) {
        /*
         * This method is used whenever there is a need to filter the undo or
         * redo history on a particular context. Currently there are no caches
         * kept to optimize repeated requests for the same filter. If benchmarks
         * show this to be a common pattern that causes performances problems,
         * we could implement a filtered cache here that is nullified whenever
         * the global history changes.
         */

        auto filtered = new ArraySeq!(IUndoableOperation);
        filtered.capacity(list.size());
//         Iterator iterator = list.iterator();
        synchronized (undoRedoHistoryLock) {
            foreach( operation; list ){
//             while (iterator.hasNext()) {
//                 IUndoableOperation operation = (IUndoableOperation) iterator
//                         .next();
                if (operation.hasContext(context)) {
                    filtered.append(operation);
                }
            }
        }
        return filtered.toArray();
    }

    /*
     * Flush the redo stack of all operations that have the given context.
     */
    private void flushRedo(IUndoContext context) {
        if (DEBUG_OPERATION_HISTORY_DISPOSE) {
            Tracing.printTrace("OPERATIONHISTORY", "Flushing redo history for " //$NON-NLS-1$ //$NON-NLS-2$
                    ~ (cast(Object)context).toString );
        }

        IUndoableOperation[] filtered = filter(redoList, context);
        for (int i = 0; i < filtered.length; i++) {
            IUndoableOperation operation = filtered[i];
            if (context is GLOBAL_UNDO_CONTEXT
                    || operation.getContexts().length is 1) {
                // remove the operation if it only has the context or we are
                // flushing all
                synchronized(redoList) redoList.remove(operation);
                internalRemove(operation);
            } else {
                // remove the reference to the context.
                // See https://bugs.eclipse.org/bugs/show_bug.cgi?id=161786
                // It is not enough to simply remove the context. There could
                // be one or more contexts that match the one we are trying to
                // dispose.
                IUndoContext[] contexts = operation.getContexts();
                for (int j = 0; j < contexts.length; j++) {
                    if (contexts[j].matches(context)) {
                        operation.removeContext(contexts[j]);
                    }
                }
                if (operation.getContexts().length is 0) {
                    synchronized(redoList) redoList.remove(operation);
                    internalRemove(operation);
                }
            }
        }
    }

    /*
     * Flush the undo stack of all operations that have the given context.
     */
    private void flushUndo(IUndoContext context) {
        if (DEBUG_OPERATION_HISTORY_DISPOSE) {
            Tracing.printTrace("OPERATIONHISTORY", "Flushing undo history for " //$NON-NLS-1$ //$NON-NLS-2$
                    ~ (cast(Object)context).toString );
        }

        // Get all operations that have the context (or one that matches)
        IUndoableOperation[] filtered = filter(undoList, context);
        for (int i = 0; i < filtered.length; i++) {
            IUndoableOperation operation = filtered[i];
            if (context is GLOBAL_UNDO_CONTEXT
                    || operation.getContexts().length is 1) {
                // remove the operation if it only has the context or we are
                // flushing all
                synchronized(undoList) undoList.remove(operation);
                internalRemove(operation);
            } else {
                // remove the reference to the context.
                // See https://bugs.eclipse.org/bugs/show_bug.cgi?id=161786
                // It is not enough to simply remove the context. There could
                // be one or more contexts that match the one we are trying to
                // dispose.
                IUndoContext[] contexts = operation.getContexts();
                for (int j = 0; j < contexts.length; j++) {
                    if (contexts[j].matches(context)) {
                        operation.removeContext(contexts[j]);
                    }
                }
                if (operation.getContexts().length is 0) {
                    synchronized(undoList) undoList.remove(operation);
                    internalRemove(operation);
                }
            }
        }
        /*
         * There may be an open composite. If it has this context, then the
         * context must be removed. If it has only this context or we are
         * flushing all operations, then null it out and notify that we are
         * ending it. We don't remove it since it was never added.
         */
        ICompositeOperation endedComposite = null;
        synchronized (openCompositeLock) {
            if (openComposite !is null) {
                if (openComposite.hasContext(context)) {
                    if (context is GLOBAL_UNDO_CONTEXT
                            || openComposite.getContexts().length is 1) {
                        endedComposite = openComposite;
                        openComposite = null;
                    } else {
                        openComposite.removeContext(context);
                    }
                }
            }
        }
        // notify outside of the synchronized block.
        if (endedComposite !is null) {
            notifyNotOK(endedComposite);
        }
    }

    /*
     * Force the redo history for the given context to contain max or less
     * items.
     */
    private void forceRedoLimit(IUndoContext context, int max) {
        IUndoableOperation[] filtered = filter(redoList, context);
        int size = filtered.length;
        if (size > 0) {
            int index = 0;
            while (size > max) {
                IUndoableOperation removed = filtered[index];
                if (context is GLOBAL_UNDO_CONTEXT
                        || removed.getContexts().length is 1) {
                    /*
                     * remove the operation if we are enforcing a global limit
                     * or if the operation only has the specified context
                     */
                    synchronized(redoList) redoList.remove(removed);
                    internalRemove(removed);
                } else {
                    /*
                     * if the operation has multiple contexts and we've reached
                     * the limit for only one of them, then just remove the
                     * context, not the operation.
                     */
                    removed.removeContext(context);
                }
                size--;
                index++;
            }
        }
    }

    /*
     * Force the undo history for the given context to contain max or less
     * items.
     */
    private void forceUndoLimit(IUndoContext context, int max) {
        IUndoableOperation[] filtered = filter(undoList, context);
        int size = filtered.length;
        if (size > 0) {
            int index = 0;
            while (size > max) {
                IUndoableOperation removed = filtered[index];
                if (context is GLOBAL_UNDO_CONTEXT
                        || removed.getContexts().length is 1) {
                    /*
                     * remove the operation if we are enforcing a global limit
                     * or if the operation only has the specified context
                     */
                    synchronized(undoList) undoList.remove(removed);
                    internalRemove(removed);
                } else {
                    /*
                     * if the operation has multiple contexts and we've reached
                     * the limit for only one of them, then just remove the
                     * context, not the operation.
                     */
                    removed.removeContext(context);
                }
                size--;
                index++;
            }
        }
    }

    /*
     * (non-Javadoc)
     *
     * @see dwtx.core.commands.operations.IOperationHistory#getLimit()
     */
    public int getLimit(IUndoContext context) {
        synchronized(limits) {
            if (!limits.containsKey(context)) {
                return DEFAULT_LIMIT;
            }
            return (cast(Integer) (limits.get(context))).intValue();
        }
    }

    /*
     * Consult the IOperationApprovers to see if the proposed redo should be
     * allowed.
     */
    private IStatus getRedoApproval(IUndoableOperation operation,
            IAdaptable info) {

        Object[] approverArray = approvers.getListeners();

        for (int i = 0; i < approverArray.length; i++) {
            IOperationApprover approver = cast(IOperationApprover) approverArray[i];
            IStatus approval = approver.proceedRedoing(operation, this, info);
            if (!approval.isOK()) {
                if (DEBUG_OPERATION_HISTORY_APPROVAL) {
                    Tracing.printTrace("OPERATIONHISTORY", //$NON-NLS-1$
                            "Redo not approved by " ~ (cast(Object)approver).toString //$NON-NLS-1$
                                    ~ "for operation " ~ (cast(Object)operation).toString //$NON-NLS-1$
                                    ~ " approved by " ~ (cast(Object)approval).toString); //$NON-NLS-1$
                }
                return approval;
            }
        }
        return Status.OK_STATUS;
    }

    /*
     * (non-Javadoc)
     *
     * @see dwtx.core.commands.operations.IOperationHistory#getRedoHistory(dwtx.core.commands.operations.IUndoContext)
     */
    public IUndoableOperation[] getRedoHistory(IUndoContext context) {
        Assert.isNotNull(cast(Object)context);
        return filter(redoList, context);
    }

    /*
     * (non-Javadoc)
     *
     * @see dwtx.core.commands.operations.IOperationHistory#getOperation(dwtx.core.commands.operations.IUndoContext)
     */
    public IUndoableOperation getRedoOperation(IUndoContext context) {
        Assert.isNotNull(cast(Object)context);
        synchronized (undoRedoHistoryLock) {
            for (int i = redoList.size() - 1; i >= 0; i--) {
                IUndoableOperation operation;
                synchronized(redoList) operation = cast(IUndoableOperation) redoList.get(i);
                if (operation.hasContext(context)) {
                    return operation;
                }
            }
        }
        return null;
    }

    /*
     * Consult the IOperationApprovers to see if the proposed undo should be
     * allowed.
     */
    private IStatus getUndoApproval(IUndoableOperation operation,
            IAdaptable info) {

        final Object[] approverArray = approvers.getListeners();

        for (int i = 0; i < approverArray.length; i++) {
            IOperationApprover approver = cast(IOperationApprover) approverArray[i];
            IStatus approval = approver.proceedUndoing(operation, this, info);
            if (!approval.isOK()) {
                if (DEBUG_OPERATION_HISTORY_APPROVAL) {
                    Tracing.printTrace("OPERATIONHISTORY", //$NON-NLS-1$
                            "Undo not approved by " ~ (cast(Object)approver).toString //$NON-NLS-1$
                                    ~ "for operation " ~ (cast(Object)operation ).toString//$NON-NLS-1$
                                    ~ " with status " ~ (cast(Object)approval).toString); //$NON-NLS-1$
                }
                return approval;
            }
        }
        return Status.OK_STATUS;
    }

    /*
     * (non-Javadoc)
     *
     * @see dwtx.core.commands.operations.IOperationHistory#getUndoHistory(dwtx.core.commands.operations.IUndoContext)
     */
    public IUndoableOperation[] getUndoHistory(IUndoContext context) {
        Assert.isNotNull(cast(Object)context);
        return filter(undoList, context);
    }

    /*
     * (non-Javadoc)
     *
     * @see dwtx.core.commands.operations.IOperationHistory#getUndoOperation(dwtx.core.commands.operations.IUndoContext)
     */
    public IUndoableOperation getUndoOperation(IUndoContext context) {
        Assert.isNotNull(cast(Object)context);
        synchronized (undoRedoHistoryLock) {
            for (int i = undoList.size() - 1; i >= 0; i--) {
                IUndoableOperation operation;
                synchronized(undoList) operation = cast(IUndoableOperation) undoList.get(i);
                if (operation.hasContext(context)) {
                    return operation;
                }
            }
        }
        return null;
    }

    /*
     * Consult the IOperationApprovers to see if the proposed execution should
     * be allowed.
     *
     * @since 3.2
     */
    private IStatus getExecuteApproval(IUndoableOperation operation,
            IAdaptable info) {

        final Object[] approverArray = approvers.getListeners();

        for (int i = 0; i < approverArray.length; i++) {
            if ( auto apro = cast(IOperationApprover2)approverArray[i]  ) {
                IOperationApprover2 approver = apro;
                IStatus approval = approver.proceedExecuting(operation, this,
                        info);
                if (!approval.isOK()) {
                    if (DEBUG_OPERATION_HISTORY_APPROVAL) {
                        Tracing.printTrace("OPERATIONHISTORY", //$NON-NLS-1$
                                "Execute not approved by " ~ (cast(Object)approver).toString //$NON-NLS-1$
                                        ~ "for operation " ~ (cast(Object)operation).toString //$NON-NLS-1$
                                        ~ " with status " ~ (cast(Object)approval).toString); //$NON-NLS-1$
                    }
                    return approval;
                }
            }
        }
        return Status.OK_STATUS;
    }

    /*
     * Remove the operation by disposing it and notifying listeners.
     */
    private void internalRemove(IUndoableOperation operation) {
        operation.dispose();
        notifyRemoved(operation);
    }

    /*
     * Notify listeners of an operation event.
     */
    private void notifyListeners(OperationHistoryEvent event) {
        if ( auto e = cast(IAdvancedUndoableOperation)event.getOperation()  ) {
            IAdvancedUndoableOperation advancedOp = e;
            SafeRunner.run(new class(advancedOp) ISafeRunnable {
                IAdvancedUndoableOperation advancedOp_;
                this(IAdvancedUndoableOperation a){ advancedOp_=a;}
                public void handleException(Exception exception) {
                    if (DEBUG_OPERATION_HISTORY_UNEXPECTED) {
                        Tracing.printTrace(
                                        "OPERATIONHISTORY", //$NON-NLS-1$
                                        "Exception during notification callback " ~ exception.toString); //$NON-NLS-1$
                    }
                }

                public void run() {
                    advancedOp_.aboutToNotify(event);
                }
            });
        }
        Object[] listenerArray = listeners.getListeners();
        for (int i = 0; i < listenerArray.length; i++) {
            IOperationHistoryListener listener = cast(IOperationHistoryListener) listenerArray[i];
            SafeRunner.run(new class(listener) ISafeRunnable {
                IOperationHistoryListener listener_;
                this(IOperationHistoryListener a){ listener_=a; }
                public void handleException(Exception exception) {
                    if (DEBUG_OPERATION_HISTORY_UNEXPECTED) {
                        Tracing.printTrace(
                                        "OPERATIONHISTORY", //$NON-NLS-1$
                                        "Exception during notification callback " ~ exception.toString); //$NON-NLS-1$
                    }
                }

                public void run() {
                    listener_.historyNotification(event);
                }
            });
        }
    }

    private void notifyAboutToExecute(IUndoableOperation operation) {
        if (DEBUG_OPERATION_HISTORY_NOTIFICATION) {
            Tracing.printTrace("OPERATIONHISTORY", "ABOUT_TO_EXECUTE " //$NON-NLS-1$ //$NON-NLS-2$
                    ~ (cast(Object)operation).toString);
        }

        notifyListeners(new OperationHistoryEvent(
                OperationHistoryEvent.ABOUT_TO_EXECUTE, this, operation));
    }

    /*
     * Notify listeners that an operation is about to redo.
     */
    private void notifyAboutToRedo(IUndoableOperation operation) {
        if (DEBUG_OPERATION_HISTORY_NOTIFICATION) {
            Tracing.printTrace("OPERATIONHISTORY", "ABOUT_TO_REDO " //$NON-NLS-1$ //$NON-NLS-2$
                    ~ (cast(Object)operation).toString);
        }

        notifyListeners(new OperationHistoryEvent(
                OperationHistoryEvent.ABOUT_TO_REDO, this, operation));
    }

    /*
     * Notify listeners that an operation is about to undo.
     */
    private void notifyAboutToUndo(IUndoableOperation operation) {
        if (DEBUG_OPERATION_HISTORY_NOTIFICATION) {
            Tracing.printTrace("OPERATIONHISTORY", "ABOUT_TO_UNDO " //$NON-NLS-1$ //$NON-NLS-2$
                    ~ (cast(Object)operation).toString);
        }

        notifyListeners(new OperationHistoryEvent(
                OperationHistoryEvent.ABOUT_TO_UNDO, this, operation));
    }

    /*
     * Notify listeners that an operation has been added.
     */
    private void notifyAdd(IUndoableOperation operation) {
        if (DEBUG_OPERATION_HISTORY_NOTIFICATION) {
            Tracing.printTrace("OPERATIONHISTORY", "OPERATION_ADDED " //$NON-NLS-1$ //$NON-NLS-2$
                    ~ (cast(Object)operation).toString);
        }

        notifyListeners(new OperationHistoryEvent(
                OperationHistoryEvent.OPERATION_ADDED, this, operation));
    }

    /*
     * Notify listeners that an operation is done executing.
     */
    private void notifyDone(IUndoableOperation operation) {
        if (DEBUG_OPERATION_HISTORY_NOTIFICATION) {
            Tracing.printTrace("OPERATIONHISTORY", "DONE " ~ (cast(Object)operation).toString); //$NON-NLS-1$ //$NON-NLS-2$
        }

        notifyListeners(new OperationHistoryEvent(OperationHistoryEvent.DONE,
                this, operation));
    }

    /*
     * Notify listeners that an operation did not succeed after an attempt to
     * execute, undo, or redo was made.
     */
    private void notifyNotOK(IUndoableOperation operation) {
        notifyNotOK(operation, null);
    }

    /*
     * Notify listeners that an operation did not succeed after an attempt to
     * execute, undo, or redo was made. Include the status associated with the
     * attempt.
     *
     * @since 3.2
     */
    private void notifyNotOK(IUndoableOperation operation, IStatus status) {
        if (DEBUG_OPERATION_HISTORY_NOTIFICATION) {
            Tracing.printTrace("OPERATIONHISTORY", "OPERATION_NOT_OK " //$NON-NLS-1$ //$NON-NLS-2$
                    ~ (cast(Object)operation).toString);
        }

        notifyListeners(new OperationHistoryEvent(
                OperationHistoryEvent.OPERATION_NOT_OK, this, operation, status));
    }

    /*
     * Notify listeners that an operation was redone.
     */
    private void notifyRedone(IUndoableOperation operation) {
        if (DEBUG_OPERATION_HISTORY_NOTIFICATION) {
            Tracing.printTrace("OPERATIONHISTORY", "REDONE " ~ (cast(Object)operation).toString); //$NON-NLS-1$ //$NON-NLS-2$
        }

        notifyListeners(new OperationHistoryEvent(OperationHistoryEvent.REDONE,
                this, operation));
    }

    /*
     * Notify listeners that an operation has been removed from the history.
     */
    private void notifyRemoved(IUndoableOperation operation) {
        if (DEBUG_OPERATION_HISTORY_NOTIFICATION) {
            Tracing.printTrace("OPERATIONHISTORY", "OPERATION_REMOVED " //$NON-NLS-1$ //$NON-NLS-2$
                    ~ (cast(Object)operation).toString);
        }

        notifyListeners(new OperationHistoryEvent(
                OperationHistoryEvent.OPERATION_REMOVED, this, operation));
    }

    /*
     * Notify listeners that an operation has been undone.
     */
    private void notifyUndone(IUndoableOperation operation) {
        if (DEBUG_OPERATION_HISTORY_NOTIFICATION) {
            Tracing.printTrace("OPERATIONHISTORY", "UNDONE " ~ (cast(Object)operation).toString); //$NON-NLS-1$ //$NON-NLS-2$
        }

        notifyListeners(new OperationHistoryEvent(OperationHistoryEvent.UNDONE,
                this, operation));
    }

    /*
     * Notify listeners that an operation has been undone.
     */
    private void notifyChanged(IUndoableOperation operation) {
        if (DEBUG_OPERATION_HISTORY_NOTIFICATION) {
            Tracing.printTrace("OPERATIONHISTORY", "OPERATION_CHANGED " //$NON-NLS-1$//$NON-NLS-2$
                    ~ (cast(Object)operation).toString);
        }

        notifyListeners(new OperationHistoryEvent(
                OperationHistoryEvent.OPERATION_CHANGED, this, operation));
    }

    /*
     * (non-Javadoc)
     *
     * @see dwtx.core.commands.operations.IOperationHistory#redo(dwtx.core.commands.operations.IUndoContext,
     *      dwtx.core.runtime.IProgressMonitor,
     *      dwtx.core.runtime.IAdaptable)
     */
    public IStatus redo(IUndoContext context, IProgressMonitor monitor,
            IAdaptable info) {
        Assert.isNotNull(cast(Object)context);
        IUndoableOperation operation = getRedoOperation(context);

        // info if there is no operation
        if (operation is null) {
            return IOperationHistory.NOTHING_TO_REDO_STATUS;
        }

        // error if operation is invalid
        if (!operation.canRedo()) {
            if (DEBUG_OPERATION_HISTORY_UNEXPECTED) {
                Tracing.printTrace("OPERATIONHISTORY", //$NON-NLS-1$
                        "Redo operation not valid - " ~ (cast(Object)operation).toString); //$NON-NLS-1$
            }

            return IOperationHistory.OPERATION_INVALID_STATUS;
        }

        return doRedo(monitor, info, operation);
    }

    /*
     * (non-Javadoc)
     *
     * @see dwtx.core.commands.operations.IOperationHistory#redoOperation(dwtx.core.commands.operations.IUndoableOperation,
     *      dwtx.core.runtime.IProgressMonitor,
     *      dwtx.core.runtime.IAdaptable)
     */

    public IStatus redoOperation(IUndoableOperation operation,
            IProgressMonitor monitor, IAdaptable info) {
        Assert.isNotNull(cast(Object)operation);
        IStatus status;
        if (operation.canRedo()) {
            status = doRedo(monitor, info, operation);
        } else {
            if (DEBUG_OPERATION_HISTORY_UNEXPECTED) {
                Tracing.printTrace("OPERATIONHISTORY", //$NON-NLS-1$
                        "Redo operation not valid - " ~ (cast(Object)operation).toString); //$NON-NLS-1$
            }
            status = IOperationHistory.OPERATION_INVALID_STATUS;
        }
        return status;
    }

    /*
     * (non-Javadoc)
     *
     * @see dwtx.core.commands.operations.IOperationHistory#removeOperationApprover(dwtx.core.commands.operations.IOperationApprover)
     */
    public void removeOperationApprover(IOperationApprover approver) {
        approvers.remove(cast(Object)approver);
    }

    /*
     * (non-Javadoc)
     *
     * @see dwtx.core.commands.operations.IOperationHistory#removeOperationHistoryListener(dwtx.core.commands.operations.IOperationHistoryListener)
     */
    public void removeOperationHistoryListener(
            IOperationHistoryListener listener) {
        listeners.remove(cast(Object)listener);
    }

    /*
     * (non-Javadoc)
     *
     * @see dwtx.core.commands.operations.IOperationHistory#replaceOperation(dwtx.core.commands.operations.IUndoableOperation,
     *      dwtx.core.commands.operations.IUndoableOperation [])
     */
    public void replaceOperation(IUndoableOperation operation,
            IUndoableOperation[] replacements) {
        // check the undo history first.
        bool inUndo = false;
        synchronized (undoRedoHistoryLock) {
            int index = 0;
            synchronized(undoList) {
                foreach( o; undoList ){
                    if( o == operation ){
                        break;
                    }
                    index++;
                }
                if( index is undoList.size() ){
                    index = -1;
                }
            }
            if (index > -1) {
                inUndo = true;
                synchronized(undoList) undoList.remove(operation);
                // notify listeners after the lock on undoList is released
                auto allContexts = new ArraySeq!(IUndoContext);
                allContexts.capacity(replacements.length);
                for (int i = 0; i < replacements.length; i++) {
                    IUndoContext[] opContexts = replacements[i].getContexts();
                    for (int j = 0; j < opContexts.length; j++) {
                        allContexts.append(opContexts[j]);
                    }
                    synchronized(undoList) undoList.addAt(index, replacements[i]);
                    // notify listeners after the lock on the history is
                    // released
                }
                // recheck all the limits. We do this at the end so the index
                // doesn't change during replacement
                for (int i = 0; i < allContexts.size(); i++) {
                    IUndoContext context = allContexts.get(i);
                    forceUndoLimit(context, getLimit(context));
                }
            }
        }
        if (inUndo) {
            // notify listeners of operations added and removed
            internalRemove(operation);
            for (int i = 0; i < replacements.length; i++) {
                notifyAdd(replacements[i]);
            }
            return;
        }

        // operation was not in the undo history. Check the redo history.

        synchronized (undoRedoHistoryLock) {
            int index = 0;
            synchronized(redoList) {
                foreach( o; redoList ){
                    if( o == operation ){
                        break;
                    }
                    index++;
                }
                if( index is redoList.size() ){
                    index = -1;
                }
            }
            if (index is -1) {
                return;
            }
            auto allContexts = new ArraySeq!(IUndoContext);
            allContexts.capacity(replacements.length);
            synchronized(redoList) redoList.remove(operation);
            // notify listeners after we release the lock on redoList
            for (int i = 0; i < replacements.length; i++) {
                IUndoContext[] opContexts = replacements[i].getContexts();
                for (int j = 0; j < opContexts.length; j++) {
                    allContexts.append(opContexts[j]);
                }
                synchronized(redoList) redoList.addAt(index, replacements[i]);
                // notify listeners after we release the lock on redoList
            }
            // recheck all the limits. We do this at the end so the index
            // doesn't change during replacement
            for (int i = 0; i < allContexts.size(); i++) {
                IUndoContext context = cast(IUndoContext) allContexts.get(i);
                forceRedoLimit(context, getLimit(context));
            }
        }
        // send listener notifications after we release the lock on the history
        internalRemove(operation);
        for (int i = 0; i < replacements.length; i++) {
            notifyAdd(replacements[i]);
        }
    }

    /*
     * (non-Javadoc)
     *
     * @see dwtx.core.commands.operations.IOperationHistory#setLimit(dwtx.core.commands.operations.IUndoContext,
     *      int)
     */
    public void setLimit(IUndoContext context, int limit) {
        Assert.isTrue(limit >= 0);
        /*
         * The limit checking methods interpret a null context as a global limit
         * to be enforced. We do not wish to support a global limit in this
         * implementation, so we throw an exception for a null context. The rest
         * of the implementation can handle a null context, so subclasses can
         * override this if a global limit is desired.
         */
        Assert.isNotNull(cast(Object)context);
        synchronized(limits) limits.add(context, new Integer(limit));
        forceUndoLimit(context, limit);
        forceRedoLimit(context, limit);

    }

    /*
     * (non-Javadoc)
     *
     * @see dwtx.core.commands.operations.IOperationHistory#undo(dwtx.core.commands.operations.IUndoContext,
     *      dwtx.core.runtime.IProgressMonitor,
     *      dwtx.core.runtime.IAdaptable)
     */
    public IStatus undo(IUndoContext context, IProgressMonitor monitor,
            IAdaptable info) {
        Assert.isNotNull(cast(Object)context);
        IUndoableOperation operation = getUndoOperation(context);

        // info if there is no operation
        if (operation is null) {
            return IOperationHistory.NOTHING_TO_UNDO_STATUS;
        }

        // error if operation is invalid
        if (!operation.canUndo()) {
            if (DEBUG_OPERATION_HISTORY_UNEXPECTED) {
                Tracing.printTrace("OPERATIONHISTORY", //$NON-NLS-1$
                        "Undo operation not valid - " ~ (cast(Object)operation).toString); //$NON-NLS-1$
            }
            return IOperationHistory.OPERATION_INVALID_STATUS;
        }

        return doUndo(monitor, info, operation);
    }

    /*
     * (non-Javadoc)
     *
     * @see dwtx.core.commands.operations.IOperationHistory#undoOperation(dwtx.core.commands.operations.IUndoableOperation,
     *      dwtx.core.runtime.IProgressMonitor,
     *      dwtx.core.runtime.IAdaptable)
     */
    public IStatus undoOperation(IUndoableOperation operation,
            IProgressMonitor monitor, IAdaptable info) {
        Assert.isNotNull(cast(Object)operation);
        IStatus status;
        if (operation.canUndo()) {
            status = doUndo(monitor, info, operation);
        } else {
            if (DEBUG_OPERATION_HISTORY_UNEXPECTED) {
                Tracing.printTrace("OPERATIONHISTORY", //$NON-NLS-1$
                        "Undo operation not valid - " ~ (cast(Object)operation).toString); //$NON-NLS-1$
            }
            status = IOperationHistory.OPERATION_INVALID_STATUS;
        }
        return status;
    }

    /*
     * (non-Javadoc)
     *
     * @see dwtx.core.commands.operations.IOperationHistory#openOperation(dwtx.core.commands.operations.ICompositeOperation)
     */
    public void openOperation(ICompositeOperation operation, int mode) {
        synchronized (openCompositeLock) {
            if (openComposite !is null && openComposite !is operation) {
                // unexpected nesting of operations.
                if (DEBUG_OPERATION_HISTORY_UNEXPECTED) {
                    Tracing.printTrace("OPERATIONHISTORY", //$NON-NLS-1$
                            "Open operation called while another operation is open.  old: " //$NON-NLS-1$
                                    ~ (cast(Object)openComposite).toString ~ "; new:  " ~ (cast(Object)operation).toString); //$NON-NLS-1$
                }

                throw new IllegalStateException(
                        "Cannot open an operation while one is already open"); //$NON-NLS-1$
            }
            openComposite = operation;
        }
        if (DEBUG_OPERATION_HISTORY_OPENOPERATION) {
            Tracing.printTrace("OPERATIONHISTORY", "Opening operation " //$NON-NLS-1$ //$NON-NLS-2$
                    ~ (cast(Object)openComposite).toString);
        }

        if (mode is EXECUTE) {
            notifyAboutToExecute(openComposite);
        }
    }

    /*
     * (non-Javadoc)
     *
     * @see dwtx.core.commands.operations.IOperationHistory#closeOperation(bool,
     *      bool)
     */
    public void closeOperation(bool operationOK, bool addToHistory,
            int mode) {
        ICompositeOperation endedComposite = null;

        synchronized (openCompositeLock) {
            if (DEBUG_OPERATION_HISTORY_UNEXPECTED) {
                if (openComposite is null) {
                    Tracing.printTrace("OPERATIONHISTORY", //$NON-NLS-1$
                            "Attempted to close operation when none was open"); //$NON-NLS-1$
                    return;
                }
            }
            // notifications will occur outside the synchonized block
            if (openComposite !is null) {
                if (DEBUG_OPERATION_HISTORY_OPENOPERATION) {
                    Tracing.printTrace("OPERATIONHISTORY", "Closing operation " //$NON-NLS-1$ //$NON-NLS-2$
                            ~ (cast(Object)openComposite).toString);
                }
                endedComposite = openComposite;
                openComposite = null;
            }
        }
        // any mode other than EXECUTE was triggered by a request to undo or
        // redo something already in the history, so undo and redo
        // notification will occur at the end of that sequence.
        if (endedComposite !is null) {
            if (operationOK) {
                if (mode is EXECUTE) {
                    notifyDone(endedComposite);
                }
                if (addToHistory) {
                    add(endedComposite);
                }
            } else {
                if (mode is EXECUTE) {
                    notifyNotOK(endedComposite);
                }
            }
        }
    }

    /*
     * (non-Javadoc)
     *
     * @see dwtx.core.commands.operations.IOperationHistory#operationChanged(dwtx.core.commands.operations.IUndoableOperation)
     */
    public void operationChanged(IUndoableOperation operation) {
        if (undoList.contains(operation) || redoList.contains(operation)) {
            notifyChanged(operation);
        }
    }
}