Mercurial > projects > dwt-addons
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; + } + +}