diff dwtx/jface/text/link/LinkedModeUI.d @ 129:eb30df5ca28b

Added JFace Text sources
author Frank Benoit <benoit@tionex.de>
date Sat, 23 Aug 2008 19:10:48 +0200
parents
children c4fb132a086c
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/dwtx/jface/text/link/LinkedModeUI.d	Sat Aug 23 19:10:48 2008 +0200
@@ -0,0 +1,1298 @@
+/*******************************************************************************
+ * 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 dwt.dwthelper.utils;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Set;
+
+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 final 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 final 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 final 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 EmptyTarget(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 ExitFlags(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 Runnable() {
+                    public void run() {
+                        if (fIsActive && viewer instanceof IEditingSupportRegistry) {
+                            IEditingSupport[] helpers= ((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 && viewer instanceof IEditingSupportRegistry) {
+                        IEditingSupport[] helpers= ((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 (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 (selection instanceof ITextSelection) {
+                ITextSelection textsel= (ITextSelection) selection;
+                if (event.getSelectionProvider() instanceof ITextViewer) {
+                    IDocument doc= ((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 final 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 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 LinkedModeUI(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 LinkedModeUI(LinkedModeModel model, ITextViewer viewer) {
+        constructor(model, new LinkedModeUITarget[]{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 LinkedModeUI(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 LinkedModeUI(LinkedModeModel model, LinkedModeUITarget target) {
+        constructor(model, new LinkedModeUITarget[]{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) throws BadLocationException {
+        // 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) throws BadLocationException {
+        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 (fFramePosition instanceof ProposalPosition) {
+            ProposalPosition pp= (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 (viewer instanceof ISourceViewer) {
+                ISourceViewer sv= (ISourceViewer) viewer;
+                IAnnotationModel model= sv.getAnnotationModel();
+                if (model instanceof IAnnotationModelExtension) {
+                    IAnnotationModelExtension ext= (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 (viewer instanceof ISourceViewer) {
+            ISourceViewer sv= (ISourceViewer) viewer;
+            IAnnotationModel model= sv.getAnnotationModel();
+            if (model instanceof IAnnotationModelExtension) {
+                IAnnotationModelExtension ext= (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 (viewer instanceof ITextViewerExtension5) {
+            ITextViewerExtension5 extension5= (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();
+            ((ITextViewerExtension) viewer).prependVerifyKeyListener(fCurrentTarget.fKeyListener);
+        } else
+            fCurrentTarget.fKeyListener.setEnabled(true);
+
+        registerAutoEditVetoer(viewer);
+
+        ((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 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 (viewer instanceof ITextViewerExtension2) {
+                ITextViewerExtension2 vExtension= ((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 (viewer instanceof ITextViewerExtension2) {
+                ITextViewerExtension2 vExtension= ((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) throws BadPartitioningException {
+        if (document instanceof IDocumentExtension3) {
+            IDocumentExtension3 ext= (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);
+
+        ((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) {
+                ((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 Runnable() {
+            public void run() {
+                if (fExitPosition !is null)
+                    fExitPosition.getDocument().removePosition(fExitPosition);
+
+                for (Iterator iter = docs.iterator(); iter.hasNext(); ) {
+                    IDocument doc= (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= (ITextViewerExtension) fCurrentTarget.getViewer();
+            IRewriteTarget target= extension.getRewriteTarget();
+            target.endCompoundChange();
+            fHasOpenCompoundChange= false;
+        }
+    }
+
+    private void beginCompoundChange() {
+        if (!fHasOpenCompoundChange) {
+            ITextViewerExtension extension= (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;
+    }
+
+}