/******************************************************************************* * Copyright (c) 2006, 2015 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.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.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<String, Object> 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; /** * 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.listener = new ToolTipOwnerControlListener(); this.shellListener = event -> { if (ToolTip.this.control != null && !ToolTip.this.control.isDisposed()) { ToolTip.this.control.getDisplay().asyncExec(() -> { // Check if the new active shell is the tooltip // itself if (ToolTip.this.control != null && !ToolTip.this.control.isDisposed() && ToolTip.this.control.getDisplay().getActiveShell() != CURRENT_TOOLTIP) { toolTipHide(CURRENT_TOOLTIP, event); } }); } }; if (!manualActivation) { activate(); } } /** * Restore arbitrary 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. * <p> * By default the tooltip is shifted 3 pixels to the right. * </p> * * @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); control.addListener(SWT.MouseWheel, 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); control.removeListener(SWT.MouseWheel, listener); } /** * Return whether 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 whether 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 behavior</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 (Monitor element : ms) { tmp = element.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); } if (event != null && event.type == SWT.Dispose) { deactivate(); data = null; } } private void passOnEvent(Shell tip, Event event) { if (control != null && !control.isDisposed() && event != null && event.widget != control && event.type == SWT.MouseDown) { // the following was left in order to fix bug 298770 with minimal change. In 3.7, the complete method should be removed. tip.close(); } } 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, () -> toolTipShow(shell, event)); } else { toolTipShow(CURRENT_TOOLTIP, event); } if (hideDelay > 0) { control.getDisplay().timerExec(popupDelay + hideDelay, () -> 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 (Control element : children) { toolTipHookByTypeRecursively(element, 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 (Control element : children) { toolTipHookBothRecursively(element); } } } /** * 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(() -> { 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 { @Override public void handleEvent(Event event) { switch (event.type) { case SWT.Dispose: case SWT.KeyDown: case SWT.MouseDown: case SWT.MouseMove: case SWT.MouseWheel: toolTipHide(CURRENT_TOOLTIP, event); break; case SWT.MouseHover: toolTipCreate(event); break; case SWT.MouseExit: /* * Check if the mouse exit happened 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 { @Override 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; } } } } }