diff dwtx/jface/text/AbstractHoverInformationControlManager.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/AbstractHoverInformationControlManager.d	Sat Aug 23 19:10:48 2008 +0200
@@ -0,0 +1,987 @@
+/*******************************************************************************
+ * 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.AbstractHoverInformationControlManager;
+
+import dwt.dwthelper.utils;
+
+
+
+
+import dwt.DWT;
+import dwt.events.ControlEvent;
+import dwt.events.ControlListener;
+import dwt.events.KeyEvent;
+import dwt.events.KeyListener;
+import dwt.events.MouseEvent;
+import dwt.events.MouseListener;
+import dwt.events.MouseMoveListener;
+import dwt.events.MouseTrackListener;
+import dwt.events.SelectionEvent;
+import dwt.events.SelectionListener;
+import dwt.events.ShellAdapter;
+import dwt.events.ShellEvent;
+import dwt.graphics.Point;
+import dwt.graphics.Rectangle;
+import dwt.widgets.Control;
+import dwt.widgets.Display;
+import dwt.widgets.Event;
+import dwt.widgets.Listener;
+import dwt.widgets.ScrollBar;
+import dwt.widgets.Scrollable;
+import dwtx.core.runtime.Assert;
+import dwtx.core.runtime.IProgressMonitor;
+import dwtx.core.runtime.IStatus;
+import dwtx.core.runtime.Status;
+import dwtx.core.runtime.jobs.Job;
+import dwtx.jface.internal.text.DelayedInputChangeListener;
+import dwtx.jface.internal.text.InformationControlReplacer;
+import dwtx.jface.internal.text.InternalAccessor;
+import dwtx.jface.text.ITextViewerExtension8.EnrichMode;
+import dwtx.jface.text.source.AnnotationBarHoverManager;
+import dwtx.jface.util.Geometry;
+
+
+/**
+ * An information control manager that shows information in response to mouse
+ * hover events. The mouse hover events are caught by registering a
+ * {@link dwt.events.MouseTrackListener} on the manager's subject
+ * control. The manager has by default an information control closer that closes
+ * the information control as soon as the mouse pointer leaves the subject area,
+ * the user presses a key, or the subject control is resized, moved, or
+ * deactivated.
+ * <p>
+ * When being activated by a mouse hover event, the manager disables itself,
+ * until the mouse leaves the subject area. Thus, the manager is usually still
+ * disabled, when the information control has already been closed by the closer.
+ *
+ * @see dwt.events.MouseTrackListener
+ * @since 2.0
+ */
+abstract public class AbstractHoverInformationControlManager : AbstractInformationControlManager {
+    
+    /**
+     * The  information control closer for the hover information. Closes the information control as
+     * soon as the mouse pointer leaves the subject area (unless "move into hover" is enabled),
+     * a mouse button is pressed, the user presses a key, or the subject control is resized, moved, or loses focus.
+     */
+    class Closer : IInformationControlCloser, MouseListener, MouseMoveListener, ControlListener, KeyListener, SelectionListener, Listener {
+        
+        /** The closer's subject control */
+        private Control fSubjectControl;
+        /** The subject area */
+        private Rectangle fSubjectArea;
+        /** Indicates whether this closer is active */
+        private bool fIsActive= false;
+        /**
+         * The cached display.
+         * @since 3.1
+         */
+        private Display fDisplay;
+
+
+        /**
+         * Creates a new information control closer.
+         */
+        public Closer() {
+        }
+
+        /*
+         * @see IInformationControlCloser#setSubjectControl(Control)
+         */
+        public void setSubjectControl(Control control) {
+            fSubjectControl= control;
+        }
+
+        /*
+         * @see IInformationControlCloser#setHoverControl(IHoverControl)
+         */
+        public void setInformationControl(IInformationControl control) {
+            // NOTE: we use getCurrentInformationControl() from the outer class
+        }
+
+        /*
+         * @see IInformationControlCloser#start(Rectangle)
+         */
+        public void start(Rectangle subjectArea) {
+
+            if (fIsActive)
+                return;
+            fIsActive= true;
+            fWaitForMouseUp= false;
+
+            fSubjectArea= subjectArea;
+
+            if (fSubjectControl !is null && !fSubjectControl.isDisposed()) {
+                fSubjectControl.addMouseListener(this);
+                fSubjectControl.addMouseMoveListener(this);
+                fSubjectControl.addControlListener(this);
+                fSubjectControl.addKeyListener(this);
+                if (fSubjectControl instanceof Scrollable) {
+                    Scrollable scrollable= (Scrollable) fSubjectControl;
+                    ScrollBar vBar= scrollable.getVerticalBar();
+                    if (vBar !is null)
+                        vBar.addSelectionListener(this);
+                    ScrollBar hBar= scrollable.getHorizontalBar();
+                    if (hBar !is null)
+                        hBar.addSelectionListener(this);
+                }
+                
+                fDisplay= fSubjectControl.getDisplay();
+                if (!fDisplay.isDisposed()) {
+                    fDisplay.addFilter(DWT.Activate, this);
+                    fDisplay.addFilter(DWT.MouseWheel, this);
+                    
+                    fDisplay.addFilter(DWT.FocusOut, this);
+                    
+                    fDisplay.addFilter(DWT.MouseDown, this);
+                    fDisplay.addFilter(DWT.MouseUp, this);
+                    
+                    fDisplay.addFilter(DWT.MouseMove, this);
+                    fDisplay.addFilter(DWT.MouseEnter, this);
+                    fDisplay.addFilter(DWT.MouseExit, this);
+                }
+            }
+        }
+
+        /*
+         * @see IInformationControlCloser#stop()
+         */
+        public void stop() {
+            if (!fIsActive)
+                return;
+
+            fIsActive= false;
+
+            if (DEBUG)
+                System.out.println("AbstractHoverInformationControlManager.Closer stopped"); //$NON-NLS-1$
+
+            if (fSubjectControl !is null && !fSubjectControl.isDisposed()) {
+                fSubjectControl.removeMouseListener(this);
+                fSubjectControl.removeMouseMoveListener(this);
+                fSubjectControl.removeControlListener(this);
+                fSubjectControl.removeKeyListener(this);
+                if (fSubjectControl instanceof Scrollable) {
+                    Scrollable scrollable= (Scrollable) fSubjectControl;
+                    ScrollBar vBar= scrollable.getVerticalBar();
+                    if (vBar !is null)
+                        vBar.removeSelectionListener(this);
+                    ScrollBar hBar= scrollable.getHorizontalBar();
+                    if (hBar !is null)
+                        hBar.removeSelectionListener(this);
+                }
+            }
+
+            if (fDisplay !is null && !fDisplay.isDisposed()) {
+                fDisplay.removeFilter(DWT.Activate, this);
+                fDisplay.removeFilter(DWT.MouseWheel, this);
+                
+                fDisplay.removeFilter(DWT.FocusOut, this);
+                
+                fDisplay.removeFilter(DWT.MouseDown, this);
+                fDisplay.removeFilter(DWT.MouseUp, this);
+                
+                fDisplay.removeFilter(DWT.MouseMove, this);
+                fDisplay.removeFilter(DWT.MouseEnter, this);
+                fDisplay.removeFilter(DWT.MouseExit, this);
+            }
+            fDisplay= null;
+        }
+
+        /*
+         * @see dwt.events.MouseMoveListener#mouseMove(dwt.events.MouseEvent)
+         */
+        public void mouseMove(MouseEvent event) {
+            if (!hasInformationControlReplacer() || !canMoveIntoInformationControl(getCurrentInformationControl())) {
+                if (!fSubjectArea.contains(event.x, event.y)) {
+                    hideInformationControl();
+                }
+
+            } else if (getCurrentInformationControl() !is null && !getCurrentInformationControl().isFocusControl()) {
+                if (!inKeepUpZone(event.x, event.y, fSubjectControl, fSubjectArea, true)) {
+                    hideInformationControl();
+                }
+            }
+        }
+
+        /*
+         * @see dwt.events.MouseListener#mouseUp(dwt.events.MouseEvent)
+         */
+        public void mouseUp(MouseEvent event) {
+        }
+
+        /*
+         * @see MouseListener#mouseDown(MouseEvent)
+         */
+        public void mouseDown(MouseEvent event) {
+            hideInformationControl();
+        }
+
+        /*
+         * @see MouseListener#mouseDoubleClick(MouseEvent)
+         */
+        public void mouseDoubleClick(MouseEvent event) {
+            hideInformationControl();
+        }
+
+        /*
+         * @see ControlListener#controlResized(ControlEvent)
+         */
+        public void controlResized(ControlEvent event) {
+            hideInformationControl();
+        }
+
+        /*
+         * @see ControlListener#controlMoved(ControlEvent)
+         */
+        public void controlMoved(ControlEvent event) {
+            hideInformationControl();
+        }
+
+        /*
+         * @see KeyListener#keyReleased(KeyEvent)
+         */
+        public void keyReleased(KeyEvent event) {
+        }
+
+        /*
+         * @see KeyListener#keyPressed(KeyEvent)
+         */
+        public void keyPressed(KeyEvent event) {
+            hideInformationControl();
+        }
+
+        /*
+         * @see dwt.events.SelectionListener#widgetSelected(dwt.events.SelectionEvent)
+         */
+        public void widgetSelected(SelectionEvent e) {
+            hideInformationControl();
+        }
+        
+        /*
+         * @see dwt.events.SelectionListener#widgetDefaultSelected(dwt.events.SelectionEvent)
+         */
+        public void widgetDefaultSelected(SelectionEvent e) {
+        }
+        
+        /*
+         * @see dwt.widgets.Listener#handleEvent(dwt.widgets.Event)
+         * @since 3.1
+         */
+        public void handleEvent(Event event) {
+            switch (event.type) {
+                case DWT.Activate:
+                case DWT.MouseWheel:
+                    if (!hasInformationControlReplacer())
+                        hideInformationControl();
+                    else if (!isReplaceInProgress()) {
+                        IInformationControl infoControl= getCurrentInformationControl();
+                        // During isReplaceInProgress(), events can come from the replacing information control
+                        if (event.widget instanceof Control && infoControl instanceof IInformationControlExtension5) {
+                            Control control= (Control) event.widget;
+                            IInformationControlExtension5 iControl5= (IInformationControlExtension5) infoControl;
+                            if (!(iControl5.containsControl(control)))
+                                hideInformationControl();
+                            else if (event.type is DWT.MouseWheel && cancelReplacingDelay())
+                                replaceInformationControl(false);
+                        } else if (infoControl !is null && infoControl.isFocusControl() && cancelReplacingDelay()) {
+                            replaceInformationControl(true);
+                        }
+                    }
+                    break;
+                    
+                case DWT.MouseUp:
+                case DWT.MouseDown:
+                    if (!hasInformationControlReplacer())
+                        hideInformationControl();
+                    else if (!isReplaceInProgress()) {
+                        IInformationControl infoControl= getCurrentInformationControl();
+                        if (event.widget instanceof Control && infoControl instanceof IInformationControlExtension5) {
+                            Control control= (Control) event.widget;
+                            final IInformationControlExtension5 iControl5= (IInformationControlExtension5) infoControl;
+                            if (!(iControl5.containsControl(control))) {
+                                hideInformationControl();
+                            } else if (cancelReplacingDelay()) {
+                                if (event.type is DWT.MouseUp) {
+                                    stop(); // avoid that someone else replaces the info control before the async is exec'd
+                                    if (infoControl instanceof IDelayedInputChangeProvider) {
+                                        final IDelayedInputChangeProvider delayedICP= (IDelayedInputChangeProvider) infoControl;
+                                        final IInputChangedListener inputChangeListener= new DelayedInputChangeListener(delayedICP, getInformationControlReplacer());
+                                        delayedICP.setDelayedInputChangeListener(inputChangeListener);
+                                        // cancel automatic input updating after a small timeout:
+                                        control.getShell().getDisplay().timerExec(1000, new Runnable() {
+                                            public void run() {
+                                                delayedICP.setDelayedInputChangeListener(null);
+                                            }
+                                        });
+                                    }
+                                    
+                                    // XXX: workaround for https://bugs.eclipse.org/bugs/show_bug.cgi?id=212392 :
+                                    control.getShell().getDisplay().asyncExec(new Runnable() {
+                                        public void run() {
+                                            replaceInformationControl(true);
+                                        }
+                                    });
+                                } else {
+                                    fWaitForMouseUp= true;
+                                }
+                            }
+                        } else {
+                            handleMouseMove(event);
+                        }
+                    }
+                    break;
+
+                case DWT.FocusOut:
+                    IInformationControl iControl= getCurrentInformationControl();
+                    if (iControl !is null && ! iControl.isFocusControl())
+                        hideInformationControl();
+                    break;
+                    
+                case DWT.MouseMove:
+                case DWT.MouseEnter:
+                case DWT.MouseExit:
+                    handleMouseMove(event);
+                    break;
+            }
+        }
+
+        /**
+         * Handle mouse movement events.
+         * 
+         * @param event the event
+         * @since 3.4
+         */
+        private void handleMouseMove(Event event) {
+//          if (DEBUG)
+//              System.out.println("AbstractHoverInformationControl.Closer.handleMouseMove():" + event); //$NON-NLS-1$
+            
+            if (!(event.widget instanceof Control))
+                return;
+            Control eventControl= (Control) event.widget;
+            
+            //transform coordinates to subject control:
+            Point mouseLoc= event.display.map(eventControl, fSubjectControl, event.x, event.y);
+            
+            if (fSubjectArea.contains(mouseLoc))
+                return;
+            
+            IInformationControl iControl= getCurrentInformationControl();
+            if (!hasInformationControlReplacer() || !canMoveIntoInformationControl(iControl)) {
+                if (AbstractHoverInformationControlManager.this instanceof AnnotationBarHoverManager) {
+                    if (getInternalAccessor().getAllowMouseExit())
+                        return;
+                }
+                hideInformationControl();
+                return;
+            }
+            
+            IInformationControlExtension3 iControl3= (IInformationControlExtension3) iControl;
+            Rectangle controlBounds= iControl3.getBounds();
+            if (controlBounds !is null) {
+                Rectangle tooltipBounds= event.display.map(null, eventControl, controlBounds);
+                if (tooltipBounds.contains(event.x, event.y)) {
+                    if (!isReplaceInProgress() && event.type !is DWT.MouseExit)
+                        startReplaceInformationControl(event.display);
+                    return;
+                }
+                cancelReplacingDelay();
+            }
+
+            if (!fSubjectControl.getBounds().contains(mouseLoc)) {
+                /*
+                 *  Use inKeepUpZone() to make sure it also works when the hover is
+                 *  completely outside of the subject control.
+                 */
+                if (!inKeepUpZone(mouseLoc.x, mouseLoc.y, fSubjectControl, fSubjectArea, true)) {
+                    hideInformationControl();
+                    return;
+                }
+            }
+        }
+    }
+
+    /**
+     * To be installed on the manager's subject control.  Serves two different purposes:
+     * <ul>
+     * <li> start function: initiates the computation of the information to be presented. This happens on
+     *      receipt of a mouse hover event and disables the information control manager,
+     * <li> restart function: tracks mouse move and shell activation event to determine when the information
+     *      control manager needs to be reactivated.
+     * </ul>
+     */
+    class MouseTracker : ShellAdapter , MouseTrackListener, MouseMoveListener {
+
+        /** Margin around the original hover event location for computing the hover area. */
+        private final static int EPSILON= 3;
+
+        /** The area in which the original hover event occurred. */
+        private Rectangle fHoverArea;
+        /** The area for which is computed information is valid. */
+        private Rectangle fSubjectArea;
+        /** The tracker's subject control. */
+        private Control fSubjectControl;
+
+        /** Indicates whether the tracker is in restart mode ignoring hover events. */
+        private bool fIsInRestartMode= false;
+        /** Indicates whether the tracker is computing the information to be presented. */
+        private bool fIsComputing= false;
+        /** Indicates whether the mouse has been lost. */
+        private bool fMouseLostWhileComputing= false;
+        /** Indicates whether the subject control's shell has been deactivated. */
+        private bool fShellDeactivatedWhileComputing= false;
+
+        /**
+         * Creates a new mouse tracker.
+         */
+        public MouseTracker() {
+        }
+
+        /**
+         * Sets this mouse tracker's subject area, the area to be tracked in order
+         * to re-enable the information control manager.
+         *
+         * @param subjectArea the subject area
+         */
+        public void setSubjectArea(Rectangle subjectArea) {
+            Assert.isNotNull(subjectArea);
+            fSubjectArea= subjectArea;
+        }
+
+        /**
+         * Starts this mouse tracker. The given control becomes this tracker's subject control.
+         * Installs itself as mouse track listener on the subject control.
+         *
+         * @param subjectControl the subject control
+         */
+        public void start(Control subjectControl) {
+            fSubjectControl= subjectControl;
+            if (fSubjectControl !is null && !fSubjectControl.isDisposed())
+                fSubjectControl.addMouseTrackListener(this);
+
+            fIsInRestartMode= false;
+            fIsComputing= false;
+            fMouseLostWhileComputing= false;
+            fShellDeactivatedWhileComputing= false;
+        }
+
+        /**
+         * Stops this mouse tracker. Removes itself  as mouse track, mouse move, and
+         * shell listener from the subject control.
+         */
+        public void stop() {
+            if (fSubjectControl !is null && !fSubjectControl.isDisposed()) {
+                fSubjectControl.removeMouseTrackListener(this);
+                fSubjectControl.removeMouseMoveListener(this);
+                fSubjectControl.getShell().removeShellListener(this);
+            }
+        }
+
+        /**
+         * Initiates the computation of the information to be presented. Sets the initial hover area
+         * to a small rectangle around the hover event location. Adds mouse move and shell activation listeners
+         * to track whether the computed information is, after completion, useful for presentation and to
+         * implement the restart function.
+         *
+         * @param event the mouse hover event
+         */
+        public void mouseHover(MouseEvent event) {
+            if (fIsComputing || fIsInRestartMode ||
+                    (fSubjectControl !is null && !fSubjectControl.isDisposed() && fSubjectControl.getShell() !is fSubjectControl.getShell().getDisplay().getActiveShell())) {
+                if (DEBUG)
+                    System.out.println("AbstractHoverInformationControlManager...mouseHover: @ " + event.x + "/" + event.y + " : hover cancelled: fIsComputing= " + fIsComputing + ", fIsInRestartMode= " + fIsInRestartMode); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$
+                return;
+            }
+            
+            fIsInRestartMode= true;
+            fIsComputing= true;
+            fMouseLostWhileComputing= false;
+            fShellDeactivatedWhileComputing= false;
+
+            fHoverEventStateMask= event.stateMask;
+            fHoverEvent= event;
+            fHoverArea= new Rectangle(event.x - EPSILON, event.y - EPSILON, 2 * EPSILON, 2 * EPSILON );
+            if (fHoverArea.x < 0)
+                fHoverArea.x= 0;
+            if (fHoverArea.y < 0)
+                fHoverArea.y= 0;
+            setSubjectArea(fHoverArea);
+
+            if (fSubjectControl !is null && !fSubjectControl.isDisposed()) {
+                fSubjectControl.addMouseMoveListener(this);
+                fSubjectControl.getShell().addShellListener(this);
+            }
+            doShowInformation();
+        }
+
+        /**
+         * Deactivates this tracker's restart function and enables the information control
+         * manager. Does not have any effect if the tracker is still executing the start function (i.e.
+         * computing the information to be presented.
+         */
+        protected void deactivate() {
+            if (fIsComputing)
+                return;
+            
+            fIsInRestartMode= false;
+            if (fSubjectControl !is null && !fSubjectControl.isDisposed()) {
+                fSubjectControl.removeMouseMoveListener(this);
+                fSubjectControl.getShell().removeShellListener(this);
+            }
+        }
+
+        /*
+         * @see MouseTrackListener#mouseEnter(MouseEvent)
+         */
+        public void mouseEnter(MouseEvent e) {
+        }
+
+        /*
+         * @see MouseTrackListener#mouseExit(MouseEvent)
+         */
+        public void mouseExit(MouseEvent e) {
+            if (!hasInformationControlReplacer() || !canMoveIntoInformationControl(getCurrentInformationControl()) || !inKeepUpZone(e.x, e.y, fSubjectControl, fSubjectArea, false)) {
+                fMouseLostWhileComputing= true;
+                deactivate();
+            }
+        }
+
+        /*
+         * @see MouseMoveListener#mouseMove(MouseEvent)
+         */
+        public void mouseMove(MouseEvent event) {
+            if (!hasInformationControlReplacer() || !canMoveIntoInformationControl(getCurrentInformationControl())) {
+                if (!fSubjectArea.contains(event.x, event.y))
+                    deactivate();
+            } else {
+                if (!inKeepUpZone(event.x, event.y, fSubjectControl, fSubjectArea, false))
+                    deactivate();
+            }
+        }
+
+        /*
+         * @see ShellListener#shellDeactivated(ShellEvent)
+         */
+        public void shellDeactivated(ShellEvent e) {
+            fShellDeactivatedWhileComputing= true;
+            deactivate();
+        }
+
+        /*
+         * @see ShellListener#shellIconified(ShellEvent)
+         */
+        public void shellIconified(ShellEvent e) {
+            fShellDeactivatedWhileComputing= true;
+            deactivate();
+        }
+
+        /**
+         * Tells this tracker that the start function processing has been completed.
+         */
+        public void computationCompleted() {
+            fIsComputing= false;
+            fMouseLostWhileComputing= false;
+            fShellDeactivatedWhileComputing= false;
+        }
+
+        /**
+         * Determines whether the computed information is still useful for presentation.
+         * This is not the case, if the shell of the subject control has been deactivated, the mouse
+         * left the subject control, or the mouse moved on, so that it is no longer in the subject
+         * area.
+         *
+         * @return <code>true</code> if information is still useful for presentation, <code>false</code> otherwise
+         */
+        public bool isMouseLost() {
+
+            if (fMouseLostWhileComputing || fShellDeactivatedWhileComputing)
+                return true;
+
+            if (fSubjectControl !is null && !fSubjectControl.isDisposed()) {
+                Display display= fSubjectControl.getDisplay();
+                Point p= display.getCursorLocation();
+                p= fSubjectControl.toControl(p);
+                if (!fSubjectArea.contains(p) && !fHoverArea.contains(p))
+                    return true;
+            }
+
+            return false;
+        }
+    }
+
+    /**
+     * The delay in {@link ITextViewerExtension8.EnrichMode#AFTER_DELAY} mode after which
+     * the hover is enriched when the mouse has stopped moving inside the hover.
+     * @since 3.4
+     */
+    private static final long HOVER_AUTO_REPLACING_DELAY= 200;
+
+    /** The mouse tracker on the subject control */
+    private MouseTracker fMouseTracker= new MouseTracker();
+    /**
+     * The remembered hover event.
+     * @since 3.0
+     */
+    private MouseEvent fHoverEvent= null;
+    /** The remembered hover event state mask of the keyboard modifiers */
+    private int fHoverEventStateMask= 0;
+    /**
+     * The thread that delays replacing of the hover information control.
+     * To be accessed in the UI thread only!
+     * 
+     * @since 3.4
+     */
+    private Job fReplacingDelayJob;
+    
+    /**
+     * The {@link ITextViewerExtension8.EnrichMode}, may be <code>null</code>.
+     * @since 3.4
+     */
+    private EnrichMode fEnrichMode;
+    
+    /**
+     * Indicates whether we have received a MouseDown event and are waiting for a MouseUp
+     * (and don't replace the information control until that happened).
+     * @since 3.4
+     */
+    private bool fWaitForMouseUp= false;
+    
+    /**
+     * Creates a new hover information control manager using the given information control creator.
+     * By default a <code>Closer</code> instance is set as this manager's closer.
+     *
+     * @param creator the information control creator
+     */
+    protected AbstractHoverInformationControlManager(IInformationControlCreator creator) {
+        super(creator);
+        setCloser(new Closer());
+        setHoverEnrichMode(ITextViewerExtension8.EnrichMode.AFTER_DELAY);
+    }
+
+    /**
+     * Tests whether a given mouse location is within the keep-up zone.
+     * The hover should not be hidden as long as the mouse stays inside this zone.
+     * 
+     * @param x the x coordinate, relative to the <em>subject control</em>
+     * @param y the y coordinate, relative to the <em>subject control</em>
+     * @param subjectControl the subject control
+     * @param subjectArea the area for which the presented information is valid
+     * @param blowUp If <code>true</code>, then calculate for the closer, i.e. blow up the keepUp area.
+     *        If <code>false</code>, then use tight bounds for hover detection.
+     * 
+     * @return <code>true</code> iff the mouse event occurred in the keep-up zone
+     * @since 3.4
+     */
+    private bool inKeepUpZone(int x, int y, Control subjectControl, Rectangle subjectArea, bool blowUp) {
+        if (subjectArea.contains(x, y))
+            return true;
+        
+        IInformationControl iControl= getCurrentInformationControl();
+        if ((iControl instanceof IInformationControlExtension5 && !((IInformationControlExtension5) iControl).isVisible())) {
+            iControl= null;
+            if (getInformationControlReplacer() !is null) {
+                iControl= getInformationControlReplacer().getCurrentInformationControl2();
+                if ((iControl instanceof IInformationControlExtension5 && !((IInformationControlExtension5) iControl).isVisible())) {
+                    return false;
+                }
+            }
+        }
+        if (iControl instanceof IInformationControlExtension3) {
+            IInformationControlExtension3 iControl3= (IInformationControlExtension3) iControl;
+            
+            Rectangle iControlBounds= subjectControl.getDisplay().map(null, subjectControl, iControl3.getBounds());
+            Rectangle totalBounds= Geometry.copy(iControlBounds);
+            if (blowUp && isReplaceInProgress()) {
+                //Problem: blown up iControl overlaps rest of subjectArea's line
+                // solution for now: only blow up for keep up (closer), but not for further hover detection
+                int margin= getInformationControlReplacer().getKeepUpMargin();
+                Geometry.expand(totalBounds, margin, margin, margin, margin);
+            }
+            
+            if (!blowUp) {
+                if (iControlBounds.contains(x, y))
+                    return true;
+                
+                if (subjectArea.y + subjectArea.height < iControlBounds.y) {
+                    // special case for hover events: subjectArea totally above iControl:
+                    //  +-----------+
+                    //  |subjectArea|
+                    //  +-----------+
+                    //  |also keepUp|
+                    // ++-----------+-------+
+                    // | InformationControl |
+                    // +--------------------+
+                    if (subjectArea.y + subjectArea.height <= y && y <= totalBounds.y) {
+                        // is vertically between subject area and iControl
+                        if (subjectArea.x <= x && x <= subjectArea.x + subjectArea.width) {
+                            // is below subject area (in a vertical projection)
+                            return true;
+                        }
+                        // FIXME: cases when subjectArea extends to left or right of iControl?
+                    }
+                    return false;
+                    
+                } else if (iControlBounds.x + iControlBounds.width < subjectArea.x) {
+                    // special case for hover events (e.g. in overview ruler): iControl totally left of subjectArea
+                    // +--------------------+-----------+
+                    // |                    |           +-----------+
+                    // | InformationControl |also keepUp|subjectArea|
+                    // |                    |           +-----------+
+                    // +--------------------+-----------+
+                    if (iControlBounds.x + iControlBounds.width <= x && x <= subjectArea.x) {
+                        // is horizontally between iControl and subject area
+                        if (iControlBounds.y <= y && y <= iControlBounds.y + iControlBounds.height) {
+                            // is to the right of iControl (in a horizontal projection)
+                            return true;
+                        }
+                    }
+                    return false;
+                    
+                } else if (subjectArea.x + subjectArea.width < iControlBounds.x) {
+                    // special case for hover events (e.g. in annotation ruler): subjectArea totally left of iControl
+                    //             +-----------+--------------------+
+                    // +-----------+           |                    |
+                    // |subjectArea|also keepUp| InformationControl |
+                    // +-----------+           |                    |
+                    //             +-----------+--------------------+
+                    if (subjectArea.x + subjectArea.width <= x && x <= iControlBounds.x) {
+                        // is horizontally between subject area and iControl
+                        if (iControlBounds.y <= y && y <= iControlBounds.y + iControlBounds.height) {
+                            // is to the left of iControl (in a horizontal projection)
+                            return true;
+                        }
+                    }
+                    return false;
+                }
+            }
+            
+            // FIXME: should maybe use convex hull, not bounding box
+            totalBounds.add(subjectArea);
+            if (totalBounds.contains(x, y))
+                return true;
+        }
+        return false;
+    }
+    
+    /**
+     * Tests whether the given information control allows the mouse to be moved
+     * into it.
+     * 
+     * @param iControl information control or <code>null</code> if none
+     * @return <code>true</code> if information control allows mouse move into
+     *         control, <code>false</code> otherwise
+     */
+    bool canMoveIntoInformationControl(IInformationControl iControl) {
+        return fEnrichMode !is null && canReplace(iControl);
+    }
+    
+    /*
+     * @see dwtx.jface.text.AbstractInformationControlManager#hideInformationControl()
+     */
+    protected void hideInformationControl() {
+        cancelReplacingDelay();
+        super.hideInformationControl();
+    }
+
+    /**
+     * Sets the hover enrich mode. Only applicable when an information
+     * control replacer has been set with
+     * {@link #setInformationControlReplacer(InformationControlReplacer)} .
+     * 
+     * @param mode the enrich mode
+     * @since 3.4
+     * @see ITextViewerExtension8#setHoverEnrichMode(dwtx.jface.text.ITextViewerExtension8.EnrichMode)
+     */
+    void setHoverEnrichMode(EnrichMode mode) {
+        fEnrichMode= mode;
+    }
+    
+    /*
+     * @see dwtx.jface.text.AbstractInformationControlManager#replaceInformationControl(bool)
+     */
+    void replaceInformationControl(bool takeFocus) {
+        fWaitForMouseUp= false;
+        super.replaceInformationControl(takeFocus);
+    }
+    
+    /**
+     * Cancels the replacing delay job.
+     * @return <code>true</code> iff canceling was successful, <code>false</code> if replacing has already started
+     */
+    bool cancelReplacingDelay() {
+        fWaitForMouseUp= false;
+        if (fReplacingDelayJob !is null && fReplacingDelayJob.getState() !is Job.RUNNING) {
+            bool cancelled= fReplacingDelayJob.cancel();
+            fReplacingDelayJob= null;
+//          if (DEBUG)
+//              System.out.println("AbstractHoverInformationControlManager.cancelReplacingDelay(): cancelled=" + cancelled); //$NON-NLS-1$
+            return cancelled;
+        }
+//      if (DEBUG)
+//          System.out.println("AbstractHoverInformationControlManager.cancelReplacingDelay(): not delayed"); //$NON-NLS-1$
+        return true;
+    }
+    
+    /**
+     * Starts replacing the information control, considering the current
+     * {@link ITextViewerExtension8.EnrichMode}.
+     * If set to {@link ITextViewerExtension8.EnrichMode#AFTER_DELAY}, this
+     * method cancels previous requests and restarts the delay timer.
+     * 
+     * @param display the display to be used for the call to
+     *        {@link #replaceInformationControl(bool)} in the UI thread
+     */
+    private void startReplaceInformationControl(final Display display) {
+        if (fEnrichMode is EnrichMode.ON_CLICK)
+            return;
+        
+        if (fReplacingDelayJob !is null) {
+            if (fReplacingDelayJob.getState() !is Job.RUNNING) {
+                if (fReplacingDelayJob.cancel()) {
+                    if (fEnrichMode is EnrichMode.IMMEDIATELY) {
+                        fReplacingDelayJob= null;
+                        if (! fWaitForMouseUp)
+                            replaceInformationControl(false);
+                    } else {
+//                      if (DEBUG)
+//                          System.out.println("AbstractHoverInformationControlManager.startReplaceInformationControl(): rescheduled"); //$NON-NLS-1$
+                        fReplacingDelayJob.schedule(HOVER_AUTO_REPLACING_DELAY);
+                    }
+                }
+            }
+            return;
+        }
+        
+        fReplacingDelayJob= new Job("AbstractHoverInformationControlManager Replace Delayer") { //$NON-NLS-1$
+            public IStatus run(final IProgressMonitor monitor) {
+                if (monitor.isCanceled() || display.isDisposed()) {
+                    return Status.CANCEL_STATUS;
+                }
+                display.syncExec(new Runnable() {
+                    public void run() {
+                        fReplacingDelayJob= null;
+                        if (monitor.isCanceled())
+                            return;
+                        if (! fWaitForMouseUp)
+                            replaceInformationControl(false);
+                    }
+                });
+                return Status.OK_STATUS;
+            }
+        };
+        fReplacingDelayJob.setSystem(true);
+        fReplacingDelayJob.setPriority(Job.INTERACTIVE);
+//      if (DEBUG)
+//          System.out.println("AbstractHoverInformationControlManager.startReplaceInformationControl(): scheduled"); //$NON-NLS-1$
+        fReplacingDelayJob.schedule(HOVER_AUTO_REPLACING_DELAY);
+    }
+
+    /*
+     * @see dwtx.jface.text.AbstractInformationControlManager#presentInformation()
+     */
+    protected void presentInformation() {
+        if (fMouseTracker is null) {
+            super.presentInformation();
+            return;
+        }
+
+        Rectangle area= getSubjectArea();
+        if (area !is null)
+            fMouseTracker.setSubjectArea(area);
+
+        if (fMouseTracker.isMouseLost()) {
+            fMouseTracker.computationCompleted();
+            fMouseTracker.deactivate();
+        } else {
+            fMouseTracker.computationCompleted();
+            super.presentInformation();
+        }
+    }
+
+    /**
+     * {@inheritDoc}
+     * @deprecated visibility will be changed to protected
+     */
+    public void setEnabled(bool enabled) {
+
+        bool was= isEnabled();
+        super.setEnabled(enabled);
+        bool is= isEnabled();
+
+        if (was !is is && fMouseTracker !is null) {
+            if (is)
+                fMouseTracker.start(getSubjectControl());
+            else
+                fMouseTracker.stop();
+        }
+    }
+
+    /**
+     * Disposes this manager's information control.
+     */
+    public void dispose() {
+        if (fMouseTracker !is null) {
+            fMouseTracker.stop();
+            fMouseTracker.fSubjectControl= null;
+            fMouseTracker= null;
+        }
+        super.dispose();
+    }
+
+    /**
+     * Returns the location at which the most recent mouse hover event
+     * has been issued.
+     *
+     * @return the location of the most recent mouse hover event
+     */
+    protected Point getHoverEventLocation() {
+        return fHoverEvent !is null ? new Point(fHoverEvent.x, fHoverEvent.y) : new Point(-1, -1);
+    }
+
+    /**
+     * Returns the most recent mouse hover event.
+     *
+     * @return the most recent mouse hover event or <code>null</code>
+     * @since 3.0
+     */
+    protected MouseEvent getHoverEvent() {
+        return fHoverEvent;
+    }
+
+    /**
+     * Returns the DWT event state of the most recent mouse hover event.
+     *
+     * @return the DWT event state of the most recent mouse hover event
+     */
+    protected int getHoverEventStateMask() {
+        return fHoverEventStateMask;
+    }
+    
+    /**
+     * Returns an adapter that gives access to internal methods.
+     * <p>
+     * <strong>Note:</strong> This method is not intended to be referenced or overridden by clients.</p>
+     * 
+     * @return the replaceable information control accessor
+     * @since 3.4
+     * @noreference This method is not intended to be referenced by clients.
+     * @nooverride This method is not intended to be re-implemented or extended by clients.
+     */
+    public InternalAccessor getInternalAccessor() {
+        return new MyInternalAccessor() {
+            public void setHoverEnrichMode(EnrichMode mode) {
+                AbstractHoverInformationControlManager.this.setHoverEnrichMode(mode);
+            }
+        };
+    }
+
+}