blob: 145474e9900b1cd7d6825f8336a6e17d1b9c40c2 [file] [log] [blame]
/*******************************************************************************
* 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;
}
}
}
}
}