| /******************************************************************************* |
| * Copyright (c) 2006, 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: |
| * Tom Schindl <tom.schindl@bestsolution.at> - initial API and implementation |
| * bugfix in: 195137, 198089, 225190 |
| *******************************************************************************/ |
| |
| package org.eclipse.jface.window; |
| |
| import java.util.HashMap; |
| |
| import org.eclipse.jface.viewers.ColumnViewer; |
| import org.eclipse.jface.viewers.ViewerCell; |
| import org.eclipse.swt.SWT; |
| import org.eclipse.swt.events.DisposeEvent; |
| import org.eclipse.swt.events.DisposeListener; |
| import org.eclipse.swt.graphics.Point; |
| import org.eclipse.swt.graphics.Rectangle; |
| import org.eclipse.swt.layout.FillLayout; |
| import org.eclipse.swt.widgets.Composite; |
| import org.eclipse.swt.widgets.Control; |
| import org.eclipse.swt.widgets.Display; |
| import org.eclipse.swt.widgets.Event; |
| import org.eclipse.swt.widgets.Listener; |
| import org.eclipse.swt.widgets.Monitor; |
| import org.eclipse.swt.widgets.Shell; |
| |
| /** |
| * This class gives implementors to provide customized tooltips for any control. |
| * |
| * @since 3.3 |
| */ |
| public abstract class ToolTip { |
| private Control control; |
| |
| private int xShift = 3; |
| |
| private int yShift = 0; |
| |
| private int popupDelay = 0; |
| |
| private int hideDelay = 0; |
| |
| private ToolTipOwnerControlListener listener; |
| |
| private HashMap data; |
| |
| // Ensure that only one tooltip is active in time |
| private static Shell CURRENT_TOOLTIP; |
| |
| /** |
| * Recreate the tooltip on every mouse move |
| */ |
| public static final int RECREATE = 1; |
| |
| /** |
| * Don't recreate the tooltip as long the mouse doesn't leave the area |
| * triggering the Tooltip creation |
| */ |
| public static final int NO_RECREATE = 1 << 1; |
| |
| private TooltipHideListener hideListener = new TooltipHideListener(); |
| |
| private Listener shellListener; |
| |
| private boolean hideOnMouseDown = true; |
| |
| private boolean respectDisplayBounds = true; |
| |
| private boolean respectMonitorBounds = true; |
| |
| private int style; |
| |
| private Object currentArea; |
| |
| private static final boolean IS_OSX = SWT.getPlatform().equals("carbon"); //$NON-NLS-1$ |
| |
| /** |
| * Create new instance which add TooltipSupport to the widget |
| * |
| * @param control |
| * the control on whose action the tooltip is shown |
| */ |
| public ToolTip(Control control) { |
| this(control, RECREATE, false); |
| } |
| |
| /** |
| * @param control |
| * the control to which the tooltip is bound |
| * @param style |
| * style passed to control tooltip behavior |
| * |
| * @param manualActivation |
| * <code>true</code> if the activation is done manually using |
| * {@link #show(Point)} |
| * @see #RECREATE |
| * @see #NO_RECREATE |
| */ |
| public ToolTip(Control control, int style, boolean manualActivation) { |
| this.control = control; |
| this.style = style; |
| this.control.addDisposeListener(new DisposeListener() { |
| |
| public void widgetDisposed(DisposeEvent e) { |
| data = null; |
| deactivate(); |
| } |
| |
| }); |
| |
| this.listener = new ToolTipOwnerControlListener(); |
| this.shellListener = new Listener() { |
| public void handleEvent(final Event event) { |
| if( ToolTip.this.control != null && ! ToolTip.this.control.isDisposed() ) { |
| ToolTip.this.control.getDisplay().asyncExec(new Runnable() { |
| |
| public void run() { |
| // Check if the new active shell is the tooltip itself |
| if( ToolTip.this.control.getDisplay().getActiveShell() != CURRENT_TOOLTIP) { |
| toolTipHide(CURRENT_TOOLTIP, event); |
| } |
| } |
| |
| }); |
| } |
| } |
| }; |
| |
| if (!manualActivation) { |
| activate(); |
| } |
| } |
| |
| /** |
| * Restore arbitary data under the given key |
| * |
| * @param key |
| * the key |
| * @param value |
| * the value |
| */ |
| public void setData(String key, Object value) { |
| if (data == null) { |
| data = new HashMap(); |
| } |
| data.put(key, value); |
| } |
| |
| /** |
| * Get the data restored under the key |
| * |
| * @param key |
| * the key |
| * @return data or <code>null</code> if no entry is restored under the key |
| */ |
| public Object getData(String key) { |
| if (data != null) { |
| return data.get(key); |
| } |
| return null; |
| } |
| |
| /** |
| * Set the shift (from the mouse position triggered the event) used to |
| * display the tooltip. By default the tooltip is shifted 3 pixels to the |
| * left |
| * |
| * @param p |
| * the new shift |
| */ |
| public void setShift(Point p) { |
| xShift = p.x; |
| yShift = p.y; |
| } |
| |
| /** |
| * Activate tooltip support for this control |
| */ |
| public void activate() { |
| deactivate(); |
| control.addListener(SWT.Dispose, listener); |
| control.addListener(SWT.MouseHover, listener); |
| control.addListener(SWT.MouseMove, listener); |
| control.addListener(SWT.MouseExit, listener); |
| control.addListener(SWT.MouseDown, listener); |
| } |
| |
| /** |
| * Deactivate tooltip support for the underlying control |
| */ |
| public void deactivate() { |
| control.removeListener(SWT.Dispose, listener); |
| control.removeListener(SWT.MouseHover, listener); |
| control.removeListener(SWT.MouseMove, listener); |
| control.removeListener(SWT.MouseExit, listener); |
| control.removeListener(SWT.MouseDown, listener); |
| } |
| |
| /** |
| * Return whther the tooltip respects bounds of the display. |
| * |
| * @return <code>true</code> if the tooltip respects bounds of the display |
| */ |
| public boolean isRespectDisplayBounds() { |
| return respectDisplayBounds; |
| } |
| |
| /** |
| * Set to <code>false</code> if display bounds should not be respected or |
| * to <code>true</code> if the tooltip is should repositioned to not |
| * overlap the display bounds. |
| * <p> |
| * Default is <code>true</code> |
| * </p> |
| * |
| * @param respectDisplayBounds |
| */ |
| public void setRespectDisplayBounds(boolean respectDisplayBounds) { |
| this.respectDisplayBounds = respectDisplayBounds; |
| } |
| |
| /** |
| * Return whther the tooltip respects bounds of the monitor. |
| * |
| * @return <code>true</code> if tooltip respects the bounds of the monitor |
| */ |
| public boolean isRespectMonitorBounds() { |
| return respectMonitorBounds; |
| } |
| |
| /** |
| * Set to <code>false</code> if monitor bounds should not be respected or |
| * to <code>true</code> if the tooltip is should repositioned to not |
| * overlap the monitors bounds. The monitor the tooltip belongs to is the |
| * same is control's monitor the tooltip is shown for. |
| * <p> |
| * Default is <code>true</code> |
| * </p> |
| * |
| * @param respectMonitorBounds |
| */ |
| public void setRespectMonitorBounds(boolean respectMonitorBounds) { |
| this.respectMonitorBounds = respectMonitorBounds; |
| } |
| |
| /** |
| * Should the tooltip displayed because of the given event. |
| * <p> |
| * <b>Subclasses may overwrite this to get custom behaviour</b> |
| * </p> |
| * |
| * @param event |
| * the event |
| * @return <code>true</code> if tooltip should be displayed |
| */ |
| protected boolean shouldCreateToolTip(Event event) { |
| if ((style & NO_RECREATE) != 0) { |
| Object tmp = getToolTipArea(event); |
| |
| // No new area close the current tooltip |
| if (tmp == null) { |
| hide(); |
| return false; |
| } |
| |
| boolean rv = !tmp.equals(currentArea); |
| return rv; |
| } |
| |
| return true; |
| } |
| |
| /** |
| * This method is called before the tooltip is hidden |
| * |
| * @param event |
| * the event trying to hide the tooltip |
| * @return <code>true</code> if the tooltip should be hidden |
| */ |
| private boolean shouldHideToolTip(Event event) { |
| if (event != null && event.type == SWT.MouseMove |
| && (style & NO_RECREATE) != 0) { |
| Object tmp = getToolTipArea(event); |
| |
| // No new area close the current tooltip |
| if (tmp == null) { |
| hide(); |
| return false; |
| } |
| |
| boolean rv = !tmp.equals(currentArea); |
| return rv; |
| } |
| |
| return true; |
| } |
| |
| /** |
| * This method is called to check for which area the tooltip is |
| * created/hidden for. In case of {@link #NO_RECREATE} this is used to |
| * decide if the tooltip is hidden recreated. |
| * |
| * <code>By the default it is the widget the tooltip is created for but could be any object. To decide if |
| * the area changed the {@link Object#equals(Object)} method is used.</code> |
| * |
| * @param event |
| * the event |
| * @return the area responsible for the tooltip creation or |
| * <code>null</code> this could be any object describing the area |
| * (e.g. the {@link Control} onto which the tooltip is bound to, a |
| * part of this area e.g. for {@link ColumnViewer} this could be a |
| * {@link ViewerCell}) |
| */ |
| protected Object getToolTipArea(Event event) { |
| return control; |
| } |
| |
| /** |
| * Start up the tooltip programmatically |
| * |
| * @param location |
| * the location relative to the control the tooltip is shown |
| */ |
| public void show(Point location) { |
| Event event = new Event(); |
| event.x = location.x; |
| event.y = location.y; |
| event.widget = control; |
| toolTipCreate(event); |
| } |
| |
| private Shell toolTipCreate(final Event event) { |
| if (shouldCreateToolTip(event)) { |
| Shell shell = new Shell(control.getShell(), SWT.ON_TOP | SWT.TOOL |
| | SWT.NO_FOCUS); |
| shell.setLayout(new FillLayout()); |
| |
| toolTipOpen(shell, event); |
| |
| return shell; |
| } |
| |
| return null; |
| } |
| |
| private void toolTipShow(Shell tip, Event event) { |
| if (!tip.isDisposed()) { |
| currentArea = getToolTipArea(event); |
| createToolTipContentArea(event, tip); |
| if (isHideOnMouseDown()) { |
| toolTipHookBothRecursively(tip); |
| } else { |
| toolTipHookByTypeRecursively(tip, true, SWT.MouseExit); |
| } |
| |
| tip.pack(); |
| Point size = tip.getSize(); |
| Point location = fixupDisplayBounds(size, getLocation(size, event)); |
| |
| // Need to adjust a bit more if the mouse cursor.y == tip.y and |
| // the cursor.x is inside the tip |
| Point cursorLocation = tip.getDisplay().getCursorLocation(); |
| |
| if( cursorLocation.y == location.y && location.x < cursorLocation.x && location.x + size.x > cursorLocation.x ) { |
| location.y -= 2; |
| } |
| |
| tip.setLocation(location); |
| tip.setVisible(true); |
| } |
| } |
| |
| private Point fixupDisplayBounds(Point tipSize, Point location) { |
| if (respectDisplayBounds || respectMonitorBounds) { |
| Rectangle bounds; |
| Point rightBounds = new Point(tipSize.x + location.x, tipSize.y |
| + location.y); |
| |
| Monitor[] ms = control.getDisplay().getMonitors(); |
| |
| if (respectMonitorBounds && ms.length > 1) { |
| // By default present in the monitor of the control |
| bounds = control.getMonitor().getBounds(); |
| Point p = new Point(location.x, location.y); |
| |
| // Search on which monitor the event occurred |
| Rectangle tmp; |
| for (int i = 0; i < ms.length; i++) { |
| tmp = ms[i].getBounds(); |
| if (tmp.contains(p)) { |
| bounds = tmp; |
| break; |
| } |
| } |
| |
| } else { |
| bounds = control.getDisplay().getBounds(); |
| } |
| |
| if (!(bounds.contains(location) && bounds.contains(rightBounds))) { |
| if (rightBounds.x > bounds.x + bounds.width) { |
| location.x -= rightBounds.x - (bounds.x + bounds.width); |
| } |
| |
| if (rightBounds.y > bounds.y + bounds.height) { |
| location.y -= rightBounds.y - (bounds.y + bounds.height); |
| } |
| |
| if (location.x < bounds.x) { |
| location.x = bounds.x; |
| } |
| |
| if (location.y < bounds.y) { |
| location.y = bounds.y; |
| } |
| } |
| } |
| |
| return location; |
| } |
| |
| /** |
| * Get the display relative location where the tooltip is displayed. |
| * Subclasses may overwrite to implement custom positioning. |
| * |
| * @param tipSize |
| * the size of the tooltip to be shown |
| * @param event |
| * the event triggered showing the tooltip |
| * @return the absolute position on the display |
| */ |
| public Point getLocation(Point tipSize, Event event) { |
| return control.toDisplay(event.x + xShift, event.y + yShift); |
| } |
| |
| private void toolTipHide(Shell tip, Event event) { |
| if (tip != null && !tip.isDisposed() && shouldHideToolTip(event)) { |
| control.getShell().removeListener(SWT.Deactivate, shellListener); |
| currentArea = null; |
| passOnEvent(tip,event); |
| tip.dispose(); |
| CURRENT_TOOLTIP = null; |
| afterHideToolTip(event); |
| } |
| } |
| |
| private void passOnEvent(Shell tip,Event event) { |
| if ( control != null && ! control.isDisposed() && event != null && event.widget != control && event.type == SWT.MouseDown) { |
| final Display display = control.getDisplay(); |
| Point newPt = display.map(tip, null, new Point(event.x, event.y)); |
| |
| final Event newEvent = new Event(); |
| newEvent.button=event.button; |
| newEvent.character=event.character; |
| newEvent.count = event.count; |
| newEvent.data=event.data; |
| newEvent.detail=event.detail; |
| newEvent.display=event.display; |
| newEvent.doit=event.doit; |
| newEvent.end=event.end; |
| newEvent.gc=event.gc; |
| newEvent.height=event.height; |
| newEvent.index=event.index; |
| newEvent.item=event.item; |
| newEvent.keyCode=event.keyCode; |
| newEvent.start=event.start; |
| newEvent.stateMask=event.stateMask; |
| newEvent.text=event.text; |
| newEvent.time=event.time; |
| newEvent.type=event.type; |
| newEvent.widget=event.widget; |
| newEvent.width=event.width; |
| newEvent.x = newPt.x; |
| newEvent.y = newPt.y; |
| |
| tip.close(); |
| display.asyncExec(new Runnable() { |
| public void run() { |
| if( IS_OSX ) { |
| try { |
| Thread.sleep(300); |
| } catch (InterruptedException e) { |
| |
| } |
| |
| display.post(newEvent); |
| newEvent.type = SWT.MouseUp; |
| display.post(newEvent); |
| } else { |
| display.post(newEvent); |
| } |
| } |
| }); |
| } |
| } |
| |
| private void toolTipOpen(final Shell shell, final Event event) { |
| // Ensure that only one Tooltip is shown in time |
| if (CURRENT_TOOLTIP != null) { |
| toolTipHide(CURRENT_TOOLTIP, null); |
| } |
| |
| CURRENT_TOOLTIP = shell; |
| |
| control.getShell().addListener(SWT.Deactivate, shellListener); |
| |
| if (popupDelay > 0) { |
| control.getDisplay().timerExec(popupDelay, new Runnable() { |
| public void run() { |
| toolTipShow(shell, event); |
| } |
| }); |
| } else { |
| toolTipShow(CURRENT_TOOLTIP, event); |
| } |
| |
| if (hideDelay > 0) { |
| control.getDisplay().timerExec(popupDelay + hideDelay, |
| new Runnable() { |
| |
| public void run() { |
| toolTipHide(shell, null); |
| } |
| }); |
| } |
| } |
| |
| private void toolTipHookByTypeRecursively(Control c, boolean add, int type) { |
| if (add) { |
| c.addListener(type, hideListener); |
| } else { |
| c.removeListener(type, hideListener); |
| } |
| |
| if (c instanceof Composite) { |
| Control[] children = ((Composite) c).getChildren(); |
| for (int i = 0; i < children.length; i++) { |
| toolTipHookByTypeRecursively(children[i], add, type); |
| } |
| } |
| } |
| |
| private void toolTipHookBothRecursively(Control c) { |
| c.addListener(SWT.MouseDown, hideListener); |
| c.addListener(SWT.MouseExit, hideListener); |
| |
| if (c instanceof Composite) { |
| Control[] children = ((Composite) c).getChildren(); |
| for (int i = 0; i < children.length; i++) { |
| toolTipHookBothRecursively(children[i]); |
| } |
| } |
| } |
| |
| /** |
| * Creates the content area of the the tooltip. |
| * |
| * @param event |
| * the event that triggered the activation of the tooltip |
| * @param parent |
| * the parent of the content area |
| * @return the content area created |
| */ |
| protected abstract Composite createToolTipContentArea(Event event, |
| Composite parent); |
| |
| /** |
| * This method is called after a Tooltip is hidden. |
| * <p> |
| * <b>Subclasses may override to clean up requested system resources</b> |
| * </p> |
| * |
| * @param event |
| * event triggered the hiding action (may be <code>null</code> |
| * if event wasn't triggered by user actions directly) |
| */ |
| protected void afterHideToolTip(Event event) { |
| |
| } |
| |
| /** |
| * Set the hide delay. |
| * |
| * @param hideDelay |
| * the delay before the tooltip is hidden. If <code>0</code> |
| * the tooltip is shown until user moves to other item |
| */ |
| public void setHideDelay(int hideDelay) { |
| this.hideDelay = hideDelay; |
| } |
| |
| /** |
| * Set the popup delay. |
| * |
| * @param popupDelay |
| * the delay before the tooltip is shown to the user. If |
| * <code>0</code> the tooltip is shown immediately |
| */ |
| public void setPopupDelay(int popupDelay) { |
| this.popupDelay = popupDelay; |
| } |
| |
| /** |
| * Return if hiding on mouse down is set. |
| * |
| * @return <code>true</code> if hiding on mouse down in the tool tip is on |
| */ |
| public boolean isHideOnMouseDown() { |
| return hideOnMouseDown; |
| } |
| |
| /** |
| * If you don't want the tool tip to be hidden when the user clicks inside |
| * the tool tip set this to <code>false</code>. You maybe also need to |
| * hide the tool tip yourself depending on what you do after clicking in the |
| * tooltip (e.g. if you open a new {@link Shell}) |
| * |
| * @param hideOnMouseDown |
| * flag to indicate of tooltip is hidden automatically on mouse |
| * down inside the tool tip |
| */ |
| public void setHideOnMouseDown(final boolean hideOnMouseDown) { |
| // Only needed if there's currently a tooltip active |
| if (CURRENT_TOOLTIP != null && !CURRENT_TOOLTIP.isDisposed()) { |
| // Only change if value really changed |
| if (hideOnMouseDown != this.hideOnMouseDown) { |
| control.getDisplay().syncExec(new Runnable() { |
| |
| public void run() { |
| if (CURRENT_TOOLTIP != null |
| && CURRENT_TOOLTIP.isDisposed()) { |
| toolTipHookByTypeRecursively(CURRENT_TOOLTIP, |
| hideOnMouseDown, SWT.MouseDown); |
| } |
| } |
| |
| }); |
| } |
| } |
| |
| this.hideOnMouseDown = hideOnMouseDown; |
| } |
| |
| /** |
| * Hide the currently active tool tip |
| */ |
| public void hide() { |
| toolTipHide(CURRENT_TOOLTIP, null); |
| } |
| |
| private class ToolTipOwnerControlListener implements Listener { |
| public void handleEvent(Event event) { |
| switch (event.type) { |
| case SWT.Dispose: |
| case SWT.KeyDown: |
| case SWT.MouseDown: |
| case SWT.MouseMove: |
| toolTipHide(CURRENT_TOOLTIP, event); |
| break; |
| case SWT.MouseHover: |
| toolTipCreate(event); |
| break; |
| case SWT.MouseExit: |
| /* |
| * Check if the mouse exit happend because we move over the |
| * tooltip |
| */ |
| if (CURRENT_TOOLTIP != null && !CURRENT_TOOLTIP.isDisposed()) { |
| if (CURRENT_TOOLTIP.getBounds().contains( |
| control.toDisplay(event.x, event.y))) { |
| break; |
| } |
| } |
| |
| toolTipHide(CURRENT_TOOLTIP, event); |
| break; |
| } |
| } |
| } |
| |
| private class TooltipHideListener implements Listener { |
| public void handleEvent(Event event) { |
| if (event.widget instanceof Control) { |
| |
| Control c = (Control) event.widget; |
| Shell shell = c.getShell(); |
| |
| switch (event.type) { |
| case SWT.MouseDown: |
| if (isHideOnMouseDown()) { |
| toolTipHide(shell, event); |
| } |
| break; |
| case SWT.MouseExit: |
| /* |
| * Give some insets to ensure we get exit informations from |
| * a wider area ;-) |
| */ |
| Rectangle rect = shell.getBounds(); |
| rect.x += 5; |
| rect.y += 5; |
| rect.width -= 10; |
| rect.height -= 10; |
| |
| if (!rect.contains(c.getDisplay().getCursorLocation())) { |
| toolTipHide(shell, event); |
| } |
| |
| break; |
| } |
| } |
| } |
| } |
| } |