view dwtx/jface/text/link/LinkedModeUI.d @ 153:f70d9508c95c

Fix java Collection imports
author Frank Benoit <benoit@tionex.de>
date Mon, 25 Aug 2008 00:27:31 +0200
parents 5cf141e43417
children 1a5b8f8129df
line wrap: on
line source

/*******************************************************************************
 * Copyright (c) 2000, 2008 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.jface.text.link.LinkedModeUI;

import dwtx.jface.text.link.LinkedModeModel; // packageimport
import dwtx.jface.text.link.LinkedPosition; // packageimport
import dwtx.jface.text.link.ILinkedModeListener; // packageimport
import dwtx.jface.text.link.TabStopIterator; // packageimport
import dwtx.jface.text.link.InclusivePositionUpdater; // packageimport
import dwtx.jface.text.link.LinkedPositionGroup; // packageimport
import dwtx.jface.text.link.LinkedModeManager; // packageimport
import dwtx.jface.text.link.LinkedPositionAnnotations; // packageimport
import dwtx.jface.text.link.ProposalPosition; // packageimport


import dwt.dwthelper.utils;

import dwtx.dwtxhelper.Collection;






import dwt.DWT;
import dwt.custom.StyledText;
import dwt.custom.VerifyKeyListener;
import dwt.events.ShellEvent;
import dwt.events.ShellListener;
import dwt.events.VerifyEvent;
import dwt.graphics.Point;
import dwt.widgets.Display;
import dwt.widgets.Shell;
import dwtx.core.runtime.Assert;
import dwtx.jface.internal.text.link.contentassist.ContentAssistant2;
import dwtx.jface.internal.text.link.contentassist.IProposalListener;
import dwtx.jface.text.BadLocationException;
import dwtx.jface.text.BadPartitioningException;
import dwtx.jface.text.BadPositionCategoryException;
import dwtx.jface.text.DefaultPositionUpdater;
import dwtx.jface.text.DocumentCommand;
import dwtx.jface.text.DocumentEvent;
import dwtx.jface.text.IAutoEditStrategy;
import dwtx.jface.text.IDocument;
import dwtx.jface.text.IDocumentExtension3;
import dwtx.jface.text.IDocumentListener;
import dwtx.jface.text.IEditingSupport;
import dwtx.jface.text.IEditingSupportRegistry;
import dwtx.jface.text.IPositionUpdater;
import dwtx.jface.text.IRegion;
import dwtx.jface.text.IRewriteTarget;
import dwtx.jface.text.ITextInputListener;
import dwtx.jface.text.ITextOperationTarget;
import dwtx.jface.text.ITextSelection;
import dwtx.jface.text.ITextViewer;
import dwtx.jface.text.ITextViewerExtension;
import dwtx.jface.text.ITextViewerExtension2;
import dwtx.jface.text.ITextViewerExtension5;
import dwtx.jface.text.Position;
import dwtx.jface.text.Region;
import dwtx.jface.text.contentassist.ICompletionProposal;
import dwtx.jface.text.contentassist.ICompletionProposalExtension6;
import dwtx.jface.text.source.IAnnotationModel;
import dwtx.jface.text.source.IAnnotationModelExtension;
import dwtx.jface.text.source.ISourceViewer;
import dwtx.jface.viewers.IPostSelectionProvider;
import dwtx.jface.viewers.ISelection;
import dwtx.jface.viewers.ISelectionChangedListener;
import dwtx.jface.viewers.SelectionChangedEvent;

/**
 * The UI for linked mode. Detects events that influence behavior of the linked mode
 * UI and acts upon them.
 * <p>
 * <code>LinkedModeUI</code> relies on all added
 * <code>LinkedModeUITarget</code>s to provide implementations of
 * <code>ITextViewer</code> that implement <code>ITextViewerExtension</code>,
 * and the documents being edited to implement <code>IDocumentExtension3</code>.
 * </p>
 * <p>
 * Clients may instantiate and extend this class.
 * </p>
 *
 * @since 3.0
 */
public class LinkedModeUI {

    /* cycle constants */
    /**
     * Constant indicating that this UI should never cycle from the last
     * position to the first and vice versa.
     */
    public static const Object CYCLE_NEVER= new Object();
    /**
     * Constant indicating that this UI should always cycle from the last
     * position to the first and vice versa.
     */
    public static const Object CYCLE_ALWAYS= new Object();
    /**
     * Constant indicating that this UI should cycle from the last position to
     * the first and vice versa if its model is not nested.
     */
    public static const Object CYCLE_WHEN_NO_PARENT= new Object();

    /**
     * Listener that gets notified when the linked mode UI switches its focus position.
     * <p>
     * Clients may implement this interface.
     * </p>
     */
    public interface ILinkedModeUIFocusListener {
        /**
         * Called when the UI for the linked mode leaves a linked position.
         *
         * @param position the position being left
         * @param target the target where <code>position</code> resides in
         */
        void linkingFocusLost(LinkedPosition position, LinkedModeUITarget target);
        /**
         * Called when the UI for the linked mode gives focus to a linked position.
         *
         * @param position the position being entered
         * @param target the target where <code>position</code> resides in
         */
        void linkingFocusGained(LinkedPosition position, LinkedModeUITarget target);
    }

    /**
     * Null object implementation of focus listener.
     */
    private static final class EmtpyFocusListener : ILinkedModeUIFocusListener {

        public void linkingFocusGained(LinkedPosition position, LinkedModeUITarget target) {
            // ignore
        }

        public void linkingFocusLost(LinkedPosition position, LinkedModeUITarget target) {
            // ignore
        }
    }

    /**
     * A link target consists of a viewer and gets notified if the linked mode UI on
     * it is being shown.
     * <p>
     * Clients may extend this class.
     * </p>
     * @since 3.0
     */
    public static abstract class LinkedModeUITarget : ILinkedModeUIFocusListener {
        /**
         * Returns the viewer represented by this target, never <code>null</code>.
         *
         * @return the viewer associated with this target.
         */
        public abstract ITextViewer getViewer();

        /**
         * The viewer's text widget is initialized when the UI first connects
         * to the viewer and never changed thereafter. This is to keep the
         * reference of the widget that we have registered our listeners with,
         * as the viewer, when it gets disposed, does not remember it, resulting
         * in a situation where we cannot uninstall the listeners and a memory leak.
         */
        StyledText fWidget;

        /** The cached shell - same reason as fWidget. */
        Shell fShell;

        /** The registered listener, or <code>null</code>. */
        KeyListener fKeyListener;

        /** The cached custom annotation model. */
        LinkedPositionAnnotations fAnnotationModel;
    }

    private static final class EmptyTarget : LinkedModeUITarget {

        private ITextViewer fTextViewer;

        /**
         * @param viewer the viewer
         */
        public this(ITextViewer viewer) {
            Assert.isNotNull(viewer);
            fTextViewer= viewer;
        }

        /*
         * @see dwtx.jdt.internal.ui.text.link2.LinkedModeUI.ILinkedUITarget#getViewer()
         */
        public ITextViewer getViewer() {
            return fTextViewer;
        }

        /**
         * {@inheritDoc}
         */
        public void linkingFocusLost(LinkedPosition position, LinkedModeUITarget target) {
        }

        /**
         * {@inheritDoc}
         */
        public void linkingFocusGained(LinkedPosition position, LinkedModeUITarget target) {
        }

    }

    /**
     * Listens for state changes in the model.
     */
    private final class ExitListener : ILinkedModeListener {
        public void left(LinkedModeModel model, int flags) {
            leave(ILinkedModeListener.EXIT_ALL | flags);
        }

        public void suspend(LinkedModeModel model) {
            disconnect();
            redraw();
        }

        public void resume(LinkedModeModel model, int flags) {
            if ((flags & ILinkedModeListener.EXIT_ALL) !is 0) {
                leave(flags);
            } else {
                connect();
                if ((flags & ILinkedModeListener.SELECT) !is 0)
                    select();
                ensureAnnotationModelInstalled();
                redraw();
            }
        }
    }

    /**
     * Exit flags returned if a custom exit policy wants to exit linked mode.
     * <p>
     * Clients may instantiate this class.
     * </p>
     */
    public static class ExitFlags {
        /** The flags to return in the <code>leave</code> method. */
        public int flags;
        /** The doit flag of the checked <code>VerifyKeyEvent</code>. */
        public bool doit;
        /**
         * Creates a new instance.
         *
         * @param flags the exit flags
         * @param doit the doit flag for the verify event
         */
        public this(int flags, bool doit) {
            this.flags= flags;
            this.doit= doit;
        }
    }

    /**
     * An exit policy can be registered by a caller to get custom exit
     * behavior.
     * <p>
     * Clients may implement this interface.
     * </p>
     */
    public interface IExitPolicy {
        /**
         * Checks whether the linked mode should be left after receiving the
         * given <code>VerifyEvent</code> and selection. Note that the event
         * carries widget coordinates as opposed to <code>offset</code> and
         * <code>length</code> which are document coordinates.
         *
         * @param model the linked mode model
         * @param event the verify event
         * @param offset the offset of the current selection
         * @param length the length of the current selection
         * @return valid exit flags or <code>null</code> if no special action
         *         should be taken
         */
        ExitFlags doExit(LinkedModeModel model, VerifyEvent event, int offset, int length);
    }

    /**
     * A NullObject implementation of <code>IExitPolicy</code>.
     */
    private static class NullExitPolicy : IExitPolicy {
        /*
         * @see dwtx.jdt.internal.ui.text.link2.LinkedModeUI.IExitPolicy#doExit(dwt.events.VerifyEvent, int, int)
         */
        public ExitFlags doExit(LinkedModeModel model, VerifyEvent event, int offset, int length) {
            return null;
        }
    }

    /**
     * Listens for shell events and acts upon them.
     */
    private class Closer : ShellListener, ITextInputListener {

        public void shellActivated(ShellEvent e) {
        }

        public void shellClosed(ShellEvent e) {
            leave(ILinkedModeListener.EXIT_ALL);
        }

        public void shellDeactivated(ShellEvent e) {
//          TODO re-enable after debugging
//          if (true) return;

            // from LinkedPositionUI:

            // don't deactivate on focus lost, since the proposal popups may take focus
            // plus: it doesn't hurt if you can check with another window without losing linked mode
            // since there is no intrusive popup sticking out.

            // need to check first what happens on reentering based on an open action
            // Seems to be no problem

            // Better:
            // Check with content assistant and only leave if its not the proposal shell that took the
            // focus away.

            StyledText text;
            final ITextViewer viewer;
            Display display;

            if (fCurrentTarget is null || (text= fCurrentTarget.fWidget) is null
                    || text.isDisposed() || (display= text.getDisplay()) is null
                    || display.isDisposed()
                    || (viewer= fCurrentTarget.getViewer()) is null)
            {
                leave(ILinkedModeListener.EXIT_ALL);
            }
            else
            {
                // Post in UI thread since the assistant popup will only get the focus after we lose it.
                display.asyncExec(new class()  Runnable {
                    public void run() {
                        if (fIsActive && cast(IEditingSupportRegistry)viewer ) {
                            IEditingSupport[] helpers= (cast(IEditingSupportRegistry) viewer).getRegisteredSupports();
                            for (int i= 0; i < helpers.length; i++) {
                                if (helpers[i].ownsFocusShell())
                                    return;
                            }
                        }

                        // else
                        leave(ILinkedModeListener.EXIT_ALL);

                    }
                });
            }
        }

        public void shellDeiconified(ShellEvent e) {
        }

        public void shellIconified(ShellEvent e) {
            leave(ILinkedModeListener.EXIT_ALL);
        }

        /*
         * @see dwtx.jface.text.ITextInputListener#inputDocumentAboutToBeChanged(dwtx.jface.text.IDocument, dwtx.jface.text.IDocument)
         */
        public void inputDocumentAboutToBeChanged(IDocument oldInput, IDocument newInput) {
            leave(ILinkedModeListener.EXIT_ALL);
        }

        /*
         * @see dwtx.jface.text.ITextInputListener#inputDocumentChanged(dwtx.jface.text.IDocument, dwtx.jface.text.IDocument)
         */
        public void inputDocumentChanged(IDocument oldInput, IDocument newInput) {
        }

    }

    /**
     * @since 3.1
     */
    private class DocumentListener : IDocumentListener {
        /*
         * @see dwtx.jface.text.IDocumentListener#documentAboutToBeChanged(dwtx.jface.text.DocumentEvent)
         */
        public void documentAboutToBeChanged(DocumentEvent event) {

            // default behavior: any document change outside a linked position
            // causes us to exit
            int end= event.getOffset() + event.getLength();
            for (int offset= event.getOffset(); offset <= end; offset++) {
                if (!fModel.anyPositionContains(offset)) {
                    ITextViewer viewer= fCurrentTarget.getViewer();
                    if (fFramePosition !is null && cast(IEditingSupportRegistry)viewer ) {
                        IEditingSupport[] helpers= (cast(IEditingSupportRegistry) viewer).getRegisteredSupports();
                        for (int i= 0; i < helpers.length; i++) {
                            if (helpers[i].isOriginator(null, new Region(fFramePosition.getOffset(), fFramePosition.getLength())))
                                return;
                        }
                    }

                    leave(ILinkedModeListener.EXTERNAL_MODIFICATION);
                    return;
                }
            }
        }

        /*
         * @see dwtx.jface.text.IDocumentListener#documentChanged(dwtx.jface.text.DocumentEvent)
         */
        public void documentChanged(DocumentEvent event) {
        }
    }

    /**
     * Listens for key events, checks the exit policy for custom exit
     * strategies but defaults to handling Tab, Enter, and Escape.
     */
    private class KeyListener : VerifyKeyListener {

        private bool fIsEnabled= true;

        public void verifyKey(VerifyEvent event) {

            if (!event.doit || !fIsEnabled)
                return;

            Point selection= fCurrentTarget.getViewer().getSelectedRange();
            int offset= selection.x;
            int length= selection.y;

            // if the custom exit policy returns anything, use that
            ExitFlags exitFlags= fExitPolicy.doExit(fModel, event, offset, length);
            if (exitFlags !is null) {
                leave(exitFlags.flags);
                event.doit= exitFlags.doit;
                return;
            }

            // standard behavior:
            // (Shift+)Tab: jumps from position to position, depending on cycle mode
            // Enter:       accepts all entries and leaves all (possibly stacked) environments, the last sets the caret
            // Esc:         accepts all entries and leaves all (possibly stacked) environments, the caret stays
            // ? what do we do to leave one level of a cycling model that is stacked?
            // -> This is only the case if the level was set up with forced cycling cast(CYCLE_ALWAYS), in which case
            // the caller is sure that one does not need by-level exiting.
            switch (event.character) {
                // [SHIFT-]TAB = hop between edit boxes
                case 0x09:
                    if (!(fExitPosition !is null && fExitPosition.includes(offset)) && !fModel.anyPositionContains(offset)) {
                        // outside any edit box -> leave (all? TODO should only leave the affected, level and forward to the next upper)
                        leave(ILinkedModeListener.EXIT_ALL);
                        break;
                    }

                    if (event.stateMask is DWT.SHIFT)
                        previous();
                    else
                        next();

                    event.doit= false;
                    break;

                // ENTER
                case 0x0A:
                // Ctrl+Enter on WinXP
                case 0x0D:
//                  if ((fExitPosition !is null && fExitPosition.includes(offset)) || !fModel.anyPositionContains(offset)) {
                    if (!fModel.anyPositionContains(offset)) {
//                  if ((fExitPosition is null || !fExitPosition.includes(offset)) && !fModel.anyPositionContains(offset)) {
                        // outside any edit box or on exit position -> leave (all? TODO should only leave the affected, level and forward to the next upper)
                        leave(ILinkedModeListener.EXIT_ALL);
                        break;
                    }

                    // normal case: exit entire stack and put caret to final position
                    leave(ILinkedModeListener.EXIT_ALL | ILinkedModeListener.UPDATE_CARET);
                    event.doit= false;
                    break;

                // ESC
                case 0x1B:
                    // exit entire stack and leave caret
                    leave(ILinkedModeListener.EXIT_ALL);
                    event.doit= false;
                    break;

                default:
                    if (event.character !is 0) {
                        if (!controlUndoBehavior(offset, length)) {
                            leave(ILinkedModeListener.EXIT_ALL);
                            break;
                        }
                    }
            }
        }

        private bool controlUndoBehavior(int offset, int length) {
            LinkedPosition position= fModel.findPosition(new LinkedPosition(fCurrentTarget.getViewer().getDocument(), offset, length, LinkedPositionGroup.NO_STOP));
            if (position !is null) {

                // if the last position is not the same and there is an open change: close it.
                if (!position.equals(fPreviousPosition))
                    endCompoundChange();

                beginCompoundChange();
            }

            fPreviousPosition= position;
            return fPreviousPosition !is null;
        }

        /**
         * @param enabled the new enabled state
         */
        public void setEnabled(bool enabled) {
            fIsEnabled= enabled;
        }

    }

    /**
     * Installed as post selection listener on the watched viewer. Updates the
     * linked position after cursor movement, even to positions not in the
     * iteration list.
     */
    private class MySelectionListener : ISelectionChangedListener {

        /*
         * @see dwtx.jface.viewers.ISelectionChangedListener#selectionChanged(dwtx.jface.viewers.SelectionChangedEvent)
         */
        public void selectionChanged(SelectionChangedEvent event) {
            ISelection selection= event.getSelection();
            if ( cast(ITextSelection)selection ) {
                ITextSelection textsel= cast(ITextSelection) selection;
                if ( cast(ITextViewer)event.getSelectionProvider() ) {
                    IDocument doc= (cast(ITextViewer) event.getSelectionProvider()).getDocument();
                    if (doc !is null) {
                        int offset= textsel.getOffset();
                        int length= textsel.getLength();
                        if (offset >= 0 && length >= 0) {
                            LinkedPosition find= new LinkedPosition(doc, offset, length, LinkedPositionGroup.NO_STOP);
                            LinkedPosition pos= fModel.findPosition(find);
                            if (pos is null && fExitPosition !is null && fExitPosition.includes(find))
                                pos= fExitPosition;

                            if (pos !is null)
                                switchPosition(pos, false, false);
                        }
                    }
                }
            }
        }

    }

    private class ProposalListener : IProposalListener {

        /*
         * @see dwtx.jface.internal.text.link.contentassist.IProposalListener#proposalChosen(dwtx.jface.text.contentassist.ICompletionProposal)
         */
        public void proposalChosen(ICompletionProposal proposal) {
            next();
        }
    }

    /** The current viewer. */
    private LinkedModeUITarget fCurrentTarget;
    /**
     * The manager of the linked positions we provide a UI for.
     * @since 3.1
     */
    private LinkedModeModel fModel;
    /** The set of viewers we manage. */
    private LinkedModeUITarget[] fTargets;
    /** The iterator over the tab stop positions. */
    private TabStopIterator fIterator;

    /* Our team of event listeners */
    /** The shell listener. */
    private Closer fCloser= new Closer();
    /** The linked mode listener. */
    private ILinkedModeListener fLinkedListener= new ExitListener();
    /** The selection listener. */
    private MySelectionListener fSelectionListener= new MySelectionListener();
    /** The content assist listener. */
    private ProposalListener fProposalListener= new ProposalListener();
    /**
     * The document listener.
     * @since 3.1
     */
    private IDocumentListener fDocumentListener= new DocumentListener();

    /** The last caret position, used by fCaretListener. */
    private const Position fCaretPosition= new Position(0, 0);
    /** The exit policy to control custom exit behavior */
    private IExitPolicy fExitPolicy= new NullExitPolicy();
    /** The current frame position shown in the UI, or <code>null</code>. */
    private LinkedPosition fFramePosition;
    /** The last visited position, used for undo / redo. */
    private LinkedPosition fPreviousPosition;
    /** The content assistant used to show proposals. */
    private ContentAssistant2 fAssistant;
    /** The exit position. */
    private LinkedPosition fExitPosition;
    /** State indicator to prevent multiple invocation of leave. */
    private bool fIsActive= false;
    /** The position updater for the exit position. */
    private IPositionUpdater fPositionUpdater= new DefaultPositionUpdater(getCategory());
    /** Whether to show context info. */
    private bool fDoContextInfo= false;
    /** Whether we have begun a compound change, but not yet closed. */
    private bool fHasOpenCompoundChange= false;
    /** The position listener. */
    private ILinkedModeUIFocusListener fPositionListener= new EmtpyFocusListener();
    private IAutoEditStrategy fAutoEditVetoer= new class()  IAutoEditStrategy {

        /*
         * @see dwtx.jface.text.IAutoEditStrategy#customizeDocumentCommand(dwtx.jface.text.IDocument, dwtx.jface.text.DocumentCommand)
         */
        public void customizeDocumentCommand(IDocument document, DocumentCommand command) {
            // invalidate the change to ensure that the change is performed on the document only.
            if (fModel.anyPositionContains(command.offset)) {
                command.doit= false;
                command.caretOffset= command.offset + command.length;
            }

        }
    };


    /** Whether this UI is in simple highlighting mode or not. */
    private bool fSimple;

    /**
     * Tells whether colored labels support is enabled.
     * @since 3.4
     */
    private bool fIsColoredLabelsSupportEnabled= false;

    /**
     * Creates a new UI on the given model and the set of viewers. The model
     * must provide a tab stop sequence with a non-empty list of tab stops.
     *
     * @param model the linked mode model
     * @param targets the non-empty list of targets upon which the linked mode
     *        UI should act
     */
    public this(LinkedModeModel model, LinkedModeUITarget[] targets) {
        constructor(model, targets);
    }

    /**
     * Convenience constructor for just one viewer.
     *
     * @param model the linked mode model
     * @param viewer the viewer upon which the linked mode UI should act
     */
    public this(LinkedModeModel model, ITextViewer viewer) {
        constructor(model, [new EmptyTarget(viewer)]);
    }

    /**
     * Convenience constructor for multiple viewers.
     *
     * @param model the linked mode model
     * @param viewers the non-empty list of viewers upon which the linked mode
     *        UI should act
     */
    public this(LinkedModeModel model, ITextViewer[] viewers) {
        LinkedModeUITarget[] array= new LinkedModeUITarget[viewers.length];
        for (int i= 0; i < array.length; i++) {
            array[i]= new EmptyTarget(viewers[i]);
        }
        constructor(model, array);
    }

    /**
     * Convenience constructor for one target.
     *
     * @param model the linked mode model
     * @param target the target upon which the linked mode UI should act
     */
    public this(LinkedModeModel model, LinkedModeUITarget target) {
        constructor(model, [target]);
    }

    /**
     * This does the actual constructor work.
     *
     * @param model the linked mode model
     * @param targets the non-empty array of targets upon which the linked mode UI
     *        should act
     */
    private void constructor(LinkedModeModel model, LinkedModeUITarget[] targets) {
        Assert.isNotNull(model);
        Assert.isNotNull(targets);
        Assert.isTrue(targets.length > 0);
        Assert.isTrue(model.getTabStopSequence().size() > 0);

        fModel= model;
        fTargets= targets;
        fCurrentTarget= targets[0];
        fIterator= new TabStopIterator(fModel.getTabStopSequence());
        fIterator.setCycling(!fModel.isNested());
        fModel.addLinkingListener(fLinkedListener);

        fAssistant= new ContentAssistant2();
        fAssistant.addProposalListener(fProposalListener);
        // TODO find a way to set up content assistant.
//      fAssistant.setDocumentPartitioning(IJavaPartitions.JAVA_PARTITIONING);
        fAssistant.enableColoredLabels(fIsColoredLabelsSupportEnabled);
        fCaretPosition.delete_();
    }

    /**
     * Starts this UI on the first position.
     */
    public void enter() {
        fIsActive= true;
        connect();
        next();
    }

    /**
     * Sets an <code>IExitPolicy</code> to customize the exit behavior of
     * this linked mode UI.
     *
     * @param policy the exit policy to use.
     */
    public void setExitPolicy(IExitPolicy policy) {
        fExitPolicy= policy;
    }

    /**
     * Sets the exit position to move the caret to when linked mode mode is
     * exited.
     *
     * @param target the target where the exit position is located
     * @param offset the offset of the exit position
     * @param length the length of the exit position (in case there should be a
     *        selection)
     * @param sequence set to the tab stop position of the exit position, or
     *        <code>LinkedPositionGroup.NO_STOP</code> if there should be no
     *        tab stop.
     * @throws BadLocationException if the position is not valid in the viewer's
     *         document
     */
    public void setExitPosition(LinkedModeUITarget target, int offset, int length, int sequence)  {
        // remove any existing exit position
        if (fExitPosition !is null) {
            fExitPosition.getDocument().removePosition(fExitPosition);
            fIterator.removePosition(fExitPosition);
            fExitPosition= null;
        }

        IDocument doc= target.getViewer().getDocument();
        if (doc is null)
            return;

        fExitPosition= new LinkedPosition(doc, offset, length, sequence);
        doc.addPosition(fExitPosition); // gets removed in leave()
        if (sequence !is LinkedPositionGroup.NO_STOP)
            fIterator.addPosition(fExitPosition);

    }

    /**
     * Sets the exit position to move the caret to when linked mode is exited.
     *
     * @param viewer the viewer where the exit position is located
     * @param offset the offset of the exit position
     * @param length the length of the exit position (in case there should be a
     *        selection)
     * @param sequence set to the tab stop position of the exit position, or
     *        <code>LinkedPositionGroup.NO_STOP</code> if there should be no tab stop.
     * @throws BadLocationException if the position is not valid in the
     *         viewer's document
     */
    public void setExitPosition(ITextViewer viewer, int offset, int length, int sequence)  {
        setExitPosition(new EmptyTarget(viewer), offset, length, sequence);
    }

    /**
     * Sets the cycling mode to either of <code>CYCLING_ALWAYS</code>,
     * <code>CYCLING_NEVER</code>, or <code>CYCLING_WHEN_NO_PARENT</code>,
     * which is the default.
     *
     * @param mode the new cycling mode.
     */
    public void setCyclingMode(Object mode) {
        if (mode !is CYCLE_ALWAYS && mode !is CYCLE_NEVER && mode !is CYCLE_WHEN_NO_PARENT)
            throw new IllegalArgumentException();

        if (mode is CYCLE_ALWAYS || mode is CYCLE_WHEN_NO_PARENT && !fModel.isNested())
            fIterator.setCycling(true);
        else
            fIterator.setCycling(false);
    }

    void next() {
        if (fIterator.hasNext(fFramePosition)) {
            switchPosition(fIterator.next(fFramePosition), true, true);
            return;
        }
        leave(ILinkedModeListener.UPDATE_CARET);
    }

    void previous() {
        if (fIterator.hasPrevious(fFramePosition)) {
            switchPosition(fIterator.previous(fFramePosition), true, true);
        } else
            // dont't update caret, but rather select the current frame
            leave(ILinkedModeListener.SELECT);
    }

    private void triggerContextInfo() {
        ITextOperationTarget target= fCurrentTarget.getViewer().getTextOperationTarget();
        if (target !is null) {
            if (target.canDoOperation(ISourceViewer.CONTENTASSIST_CONTEXT_INFORMATION))
                target.doOperation(ISourceViewer.CONTENTASSIST_CONTEXT_INFORMATION);
        }
    }

    /** Trigger content assist on choice positions */
    private void triggerContentAssist() {
        if ( cast(ProposalPosition)fFramePosition ) {
            ProposalPosition pp= cast(ProposalPosition) fFramePosition;
            ICompletionProposal[] choices= pp.getChoices();
            if (choices !is null && choices.length > 0) {
                fAssistant.setCompletions(choices);
                fAssistant.showPossibleCompletions();
                return;
            }
        }

        fAssistant.setCompletions(new ICompletionProposal[0]);
        fAssistant.hidePossibleCompletions();
    }

    private void switchPosition(LinkedPosition pos, bool select, bool showProposals) {
        Assert.isNotNull(pos);
        if (pos.equals(fFramePosition))
            return;

        if (fFramePosition !is null && fCurrentTarget !is null)
            fPositionListener.linkingFocusLost(fFramePosition, fCurrentTarget);

        // undo
        endCompoundChange();

        redraw(); // redraw current position being left - usually not needed
        IDocument oldDoc= fFramePosition is null ? null : fFramePosition.getDocument();
        IDocument newDoc= pos.getDocument();

        switchViewer(oldDoc, newDoc, pos);
        fFramePosition= pos;

        if (select)
            select();
        if (fFramePosition is fExitPosition && !fIterator.isCycling())
            leave(ILinkedModeListener.NONE);
        else {
            redraw(); // redraw new position
            ensureAnnotationModelInstalled();
        }
        if (showProposals)
            triggerContentAssist();
        if (fFramePosition !is fExitPosition && fDoContextInfo)
            triggerContextInfo();

        if (fFramePosition !is null && fCurrentTarget !is null)
            fPositionListener.linkingFocusGained(fFramePosition, fCurrentTarget);

    }

    private void ensureAnnotationModelInstalled() {
        LinkedPositionAnnotations lpa= fCurrentTarget.fAnnotationModel;
        if (lpa !is null) {
            ITextViewer viewer= fCurrentTarget.getViewer();
            if ( cast(ISourceViewer)viewer ) {
                ISourceViewer sv= cast(ISourceViewer) viewer;
                IAnnotationModel model= sv.getAnnotationModel();
                if ( cast(IAnnotationModelExtension)model ) {
                    IAnnotationModelExtension ext= cast(IAnnotationModelExtension) model;
                    IAnnotationModel ourModel= ext.getAnnotationModel(getUniqueKey());
                    if (ourModel is null) {
                        ext.addAnnotationModel(getUniqueKey(), lpa);
                    }
                }
            }
        }
    }

    private void uninstallAnnotationModel(LinkedModeUITarget target) {
        ITextViewer viewer= target.getViewer();
        if ( cast(ISourceViewer)viewer ) {
            ISourceViewer sv= cast(ISourceViewer) viewer;
            IAnnotationModel model= sv.getAnnotationModel();
            if ( cast(IAnnotationModelExtension)model ) {
                IAnnotationModelExtension ext= cast(IAnnotationModelExtension) model;
                ext.removeAnnotationModel(getUniqueKey());
            }
        }
    }

    private void switchViewer(IDocument oldDoc, IDocument newDoc, LinkedPosition pos) {
        if (oldDoc !is newDoc) {

            // redraw current document with new position before switching viewer
            if (fCurrentTarget.fAnnotationModel !is null)
                fCurrentTarget.fAnnotationModel.switchToPosition(fModel, pos);

            LinkedModeUITarget target= null;
            for (int i= 0; i < fTargets.length; i++) {
                if (fTargets[i].getViewer().getDocument() is newDoc) {
                    target= fTargets[i];
                    break;
                }
            }
            if (target !is fCurrentTarget) {
                disconnect();
                fCurrentTarget= target;
                target.linkingFocusLost(fFramePosition, target);
                connect();
                ensureAnnotationModelInstalled();
                if (fCurrentTarget !is null)
                    fCurrentTarget.linkingFocusGained(pos, fCurrentTarget);
            }
        }
    }

    private void select() {
        ITextViewer viewer= fCurrentTarget.getViewer();
        if ( cast(ITextViewerExtension5)viewer ) {
            ITextViewerExtension5 extension5= cast(ITextViewerExtension5) viewer;
            extension5.exposeModelRange(new Region(fFramePosition.offset, fFramePosition.length));
        } else if (!viewer.overlapsWithVisibleRegion(fFramePosition.offset, fFramePosition.length)) {
            viewer.resetVisibleRegion();
        }
        viewer.revealRange(fFramePosition.offset, fFramePosition.length);
        viewer.setSelectedRange(fFramePosition.offset, fFramePosition.length);
    }

    private void redraw() {
        if (fCurrentTarget.fAnnotationModel !is null)
            fCurrentTarget.fAnnotationModel.switchToPosition(fModel, fFramePosition);
    }

    private void connect() {
        Assert.isNotNull(fCurrentTarget);
        ITextViewer viewer= fCurrentTarget.getViewer();
        Assert.isNotNull(viewer);
        fCurrentTarget.fWidget= viewer.getTextWidget();
        if (fCurrentTarget.fWidget is null)
            leave(ILinkedModeListener.EXIT_ALL);

        if (fCurrentTarget.fKeyListener is null) {
            fCurrentTarget.fKeyListener= new KeyListener();
            (cast(ITextViewerExtension) viewer).prependVerifyKeyListener(fCurrentTarget.fKeyListener);
        } else
            fCurrentTarget.fKeyListener.setEnabled(true);

        registerAutoEditVetoer(viewer);

        (cast(IPostSelectionProvider) viewer).addPostSelectionChangedListener(fSelectionListener);

        createAnnotationModel();

        showSelection();

        fCurrentTarget.fShell= fCurrentTarget.fWidget.getShell();
        if (fCurrentTarget.fShell is null)
            leave(ILinkedModeListener.EXIT_ALL);
        fCurrentTarget.fShell.addShellListener(fCloser);

        fAssistant.install(viewer);

        viewer.addTextInputListener(fCloser);

        viewer.getDocument().addDocumentListener(fDocumentListener);
    }

    /**
     * Reveals the selection on the current target's widget, if it is valid.
     */
    private void showSelection() {
        final StyledText widget= fCurrentTarget.fWidget;
        if (widget is null || widget.isDisposed())
            return;

        // See https://bugs.eclipse.org/bugs/show_bug.cgi?id=132263
        widget.getDisplay().asyncExec(new class()  Runnable {
            public void run() {
                if (!widget.isDisposed())
                    try {
                    widget.showSelection();
                    } catch (IllegalArgumentException e) {
                        /*
                         * See https://bugs.eclipse.org/bugs/show_bug.cgi?id=66914
                         * if the StyledText is in setRedraw(false) mode, its
                         * selection may not be up2date and calling showSelection
                         * will throw an IAE.
                         * We don't have means to find out whether the selection is valid
                         * or whether the widget is redrawing or not therefore we try
                         * and ignore an IAE.
                         */
                    }
            }
        });
    }

    /**
     * Registers our auto edit vetoer with the viewer.
     *
     * @param viewer the viewer we want to veto ui-triggered changes within
     *        linked positions
     */
    private void registerAutoEditVetoer(ITextViewer viewer) {
        try {
            String[] contentTypes= getContentTypes(viewer.getDocument());
            if ( cast(ITextViewerExtension2)viewer ) {
                ITextViewerExtension2 vExtension= (cast(ITextViewerExtension2) viewer);
                for (int i= 0; i < contentTypes.length; i++) {
                    vExtension.prependAutoEditStrategy(fAutoEditVetoer, contentTypes[i]);
                }
            } else {
                Assert.isTrue(false);
            }

        } catch (BadPartitioningException e) {
            leave(ILinkedModeListener.EXIT_ALL);
        }
    }

    private void unregisterAutoEditVetoer(ITextViewer viewer) {
        try {
            String[] contentTypes= getContentTypes(viewer.getDocument());
            if ( cast(ITextViewerExtension2)viewer ) {
                ITextViewerExtension2 vExtension= (cast(ITextViewerExtension2) viewer);
                for (int i= 0; i < contentTypes.length; i++) {
                    vExtension.removeAutoEditStrategy(fAutoEditVetoer, contentTypes[i]);
                }
            } else {
                Assert.isTrue(false);
            }
        } catch (BadPartitioningException e) {
            leave(ILinkedModeListener.EXIT_ALL);
        }
    }

    /**
     * Returns all possible content types of <code>document</code>.
     *
     * @param document the document
     * @return all possible content types of <code>document</code>
     * @throws BadPartitioningException
     * @since 3.1
     */
    private String[] getContentTypes(IDocument document)  {
        if ( cast(IDocumentExtension3)document ) {
            IDocumentExtension3 ext= cast(IDocumentExtension3) document;
            String[] partitionings= ext.getPartitionings();
            Set contentTypes= new HashSet(20);
            for (int i= 0; i < partitionings.length; i++) {
                contentTypes.addAll(Arrays.asList(ext.getLegalContentTypes(partitionings[i])));
            }
            contentTypes.add(IDocument.DEFAULT_CONTENT_TYPE);
            return (String[]) contentTypes.toArray(new String[contentTypes.size()]);
        }
        return document.getLegalContentTypes();
    }

    private void createAnnotationModel() {
        if (fCurrentTarget.fAnnotationModel is null) {
            LinkedPositionAnnotations lpa= new LinkedPositionAnnotations();
            if (fSimple) {
                lpa.markExitTarget(true);
                lpa.markFocus(false);
                lpa.markSlaves(false);
                lpa.markTargets(false);
            }
            lpa.setTargets(fIterator.getPositions());
            lpa.setExitTarget(fExitPosition);
            lpa.connect(fCurrentTarget.getViewer().getDocument());
            fCurrentTarget.fAnnotationModel= lpa;
        }
    }

    private String getUniqueKey() {
        return "linked.annotationmodelkey."+toString(); //$NON-NLS-1$
    }

    private void disconnect() {
        Assert.isNotNull(fCurrentTarget);
        ITextViewer viewer= fCurrentTarget.getViewer();
        Assert.isNotNull(viewer);

        viewer.getDocument().removeDocumentListener(fDocumentListener);

        fAssistant.uninstall();
        fAssistant.removeProposalListener(fProposalListener);

        fCurrentTarget.fWidget= null;

        Shell shell= fCurrentTarget.fShell;
        fCurrentTarget.fShell= null;

        if (shell !is null && !shell.isDisposed())
            shell.removeShellListener(fCloser);

        // this one is asymmetric: we don't install the model in
        // connect, but leave it to its callers to ensure they
        // have the model installed if they need it
        uninstallAnnotationModel(fCurrentTarget);

        unregisterAutoEditVetoer(viewer);

        // don't remove the verify key listener to let it keep its position
        // in the listener queue
        if (fCurrentTarget.fKeyListener !is null)
            fCurrentTarget.fKeyListener.setEnabled(false);

        (cast(IPostSelectionProvider) viewer).removePostSelectionChangedListener(fSelectionListener);

        redraw();
    }

    void leave(final int flags) {
        if (!fIsActive)
            return;
        fIsActive= false;

        endCompoundChange();

        Display display= null;
        if (fCurrentTarget.fWidget !is null && !fCurrentTarget.fWidget.isDisposed())
            display= fCurrentTarget.fWidget.getDisplay();

        if (fCurrentTarget.fAnnotationModel !is null)
            fCurrentTarget.fAnnotationModel.removeAllAnnotations();
        disconnect();

        for (int i= 0; i < fTargets.length; i++) {
            LinkedModeUITarget target= fTargets[i];
            ITextViewer viewer= target.getViewer();
            if (target.fKeyListener !is null) {
                (cast(ITextViewerExtension) viewer).removeVerifyKeyListener(target.fKeyListener);
                target.fKeyListener= null;
            }

            viewer.removeTextInputListener(fCloser);
        }

        for (int i= 0; i < fTargets.length; i++) {

            if (fTargets[i].fAnnotationModel !is null) {
                fTargets[i].fAnnotationModel.removeAllAnnotations();
                fTargets[i].fAnnotationModel.disconnect(fTargets[i].getViewer().getDocument());
                fTargets[i].fAnnotationModel= null;
            }

            uninstallAnnotationModel(fTargets[i]);
        }


        if ((flags & ILinkedModeListener.UPDATE_CARET) !is 0 && fExitPosition !is null && fFramePosition !is fExitPosition && !fExitPosition.isDeleted())
            switchPosition(fExitPosition, true, false);

        final List docs= new ArrayList();
        for (int i= 0; i < fTargets.length; i++) {
            IDocument doc= fTargets[i].getViewer().getDocument();
            if (doc !is null)
                docs.add(doc);
        }

        fModel.stopForwarding(flags);

        Runnable runnable= new class()  Runnable {
            public void run() {
                if (fExitPosition !is null)
                    fExitPosition.getDocument().removePosition(fExitPosition);

                for (Iterator iter = docs.iterator(); iter.hasNext(); ) {
                    IDocument doc= cast(IDocument) iter.next();
                    doc.removePositionUpdater(fPositionUpdater);
                    bool uninstallCat= false;
                    String[] cats= doc.getPositionCategories();
                    for (int j= 0; j < cats.length; j++) {
                        if (getCategory().equals(cats[j])) {
                            uninstallCat= true;
                            break;
                        }
                    }
                    if (uninstallCat)
                        try {
                            doc.removePositionCategory(getCategory());
                        } catch (BadPositionCategoryException e) {
                            // ignore
                        }
                }
                fModel.exit(flags);
            }
        };

        // remove positions (both exit positions AND linked positions in the
        // model) asynchronously to make sure that the annotation painter
        // gets correct document offsets.
        if (display !is null)
            display.asyncExec(runnable);
        else
            runnable.run();
    }

    private void endCompoundChange() {
        if (fHasOpenCompoundChange) {
            ITextViewerExtension extension= cast(ITextViewerExtension) fCurrentTarget.getViewer();
            IRewriteTarget target= extension.getRewriteTarget();
            target.endCompoundChange();
            fHasOpenCompoundChange= false;
        }
    }

    private void beginCompoundChange() {
        if (!fHasOpenCompoundChange) {
            ITextViewerExtension extension= cast(ITextViewerExtension) fCurrentTarget.getViewer();
            IRewriteTarget target= extension.getRewriteTarget();
            target.beginCompoundChange();
            fHasOpenCompoundChange= true;
        }
    }

    /**
     * Returns the currently selected region or <code>null</code>.
     *
     * @return the currently selected region or <code>null</code>
     */
    public IRegion getSelectedRegion() {
        if (fFramePosition !is null)
            return new Region(fFramePosition.getOffset(), fFramePosition.getLength());
        if (fExitPosition !is null)
            return new Region(fExitPosition.getOffset(), fExitPosition.getLength());
        return null;
    }

    private String getCategory() {
        return toString();
    }

    /**
     * Sets the context info property. If set to <code>true</code>, context
     * info will be invoked on the current target's viewer whenever a position
     * is switched.
     *
     * @param doContextInfo <code>true</code> if context information should be
     *        displayed
     */
    public void setDoContextInfo(bool doContextInfo) {
        fDoContextInfo= doContextInfo;
    }

    /**
     * Sets the focus callback which will get informed when the focus of the
     * linked mode UI changes.
     * <p>
     * If there is a listener installed already, it will be replaced.
     * </p>
     *
     * @param listener the new listener, never <code>null</code>.
     */
    protected void setPositionListener(ILinkedModeUIFocusListener listener) {
        Assert.isNotNull(listener);
        fPositionListener= listener;
    }

    /**
     * Sets the "simple" mode of the receiver. A linked mode UI in simple mode
     * merely draws the exit position, but not the target, focus, and slave
     * positions. Default is <code>false</code>. This method must be called
     * before it is entered.
     *
     * @param simple <code>true</code> if the UI should be in simple mode.
     */
    public void setSimpleMode(bool simple) {
        fSimple= simple;
    }

    /**
     * Enables the support for colored labels in the proposal popup.
     * <p>Completion proposals can implement {@link ICompletionProposalExtension6}
     * to provide colored proposal labels.</p>
     *
     * @param isEnabled if <code>true</code> the support for colored labels is enabled in the proposal popup
     * @since 3.4
     */
    public void enableColoredLabels(bool isEnabled) {
        fIsColoredLabelsSupportEnabled= isEnabled;
    }

}