/*
* @(#)ToolTipManager.java 1.74 06/05/12
*
* Copyright 2006 Sun Microsystems, Inc. All rights reserved.
* SUN PROPRIETARY/CONFIDENTIAL. Use is subject to license terms.
*/
package javax.swing;
import java.awt.event.*;
import java.applet.*;
import java.awt.*;
import java.io.Serializable;
import sun.swing.UIAction;
/**
* Manages all the <code>ToolTips</code> in the system.
* <p>
* ToolTipManager contains numerous properties for configuring how long it
* will take for the tooltips to become visible, and how long till they
* hide. Consider a component that has a different tooltip based on where
* the mouse is, such as JTree. When the mouse moves into the JTree and
* over a region that has a valid tooltip, the tooltip will become
* visibile after <code>initialDelay</code> milliseconds. After
* <code>dismissDelay</code> milliseconds the tooltip will be hidden. If
* the mouse is over a region that has a valid tooltip, and the tooltip
* is currently visible, when the mouse moves to a region that doesn't have
* a valid tooltip the tooltip will be hidden. If the mouse then moves back
* into a region that has a valid tooltip within <code>reshowDelay</code>
* milliseconds, the tooltip will immediately be shown, otherwise the
* tooltip will be shown again after <code>initialDelay</code> milliseconds.
*
* @see JComponent#createToolTip
* @version 1.74 05/12/06
* @author Dave Moore
* @author Rich Schiavi
*/
public class ToolTipManager extends MouseAdapter implements MouseMotionListener {
Timer enterTimer, exitTimer, insideTimer;
String toolTipText;
Point preferredLocation;
JComponent insideComponent;
MouseEvent mouseEvent;
boolean showImmediately;
final static ToolTipManager sharedInstance = new ToolTipManager();
transient Popup tipWindow;
/** The Window tip is being displayed in. This will be non-null if
* the Window tip is in differs from that of insideComponent's Window.
*/
private Window window;
JToolTip tip;
private Rectangle popupRect = null;
private Rectangle popupFrameRect = null;
boolean enabled = true;
private boolean tipShowing = false;
private KeyStroke postTip,hideTip;
private Action postTipAction, hideTipAction;
private FocusListener focusChangeListener = null;
private MouseMotionListener moveBeforeEnterListener = null;
// PENDING(ges)
protected boolean lightWeightPopupEnabled = true;
protected boolean heavyWeightPopupEnabled = false;
ToolTipManager() {
enterTimer = new Timer(750, new insideTimerAction());
enterTimer.setRepeats(false);
exitTimer = new Timer(500, new outsideTimerAction());
exitTimer.setRepeats(false);
insideTimer = new Timer(4000, new stillInsideTimerAction());
insideTimer.setRepeats(false);
// create accessibility actions
postTip = KeyStroke.getKeyStroke(KeyEvent.VK_F1,Event.CTRL_MASK);
postTipAction = new Actions(Actions.SHOW);
hideTip = KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE,0);
hideTipAction = new Actions(Actions.HIDE);
moveBeforeEnterListener = new MoveBeforeEnterListener();
}
/**
* Enables or disables the tooltip.
*
* @param flag true to enable the tip, false otherwise
*/
public void setEnabled(boolean flag) {
enabled = flag;
if (!flag) {
hideTipWindow();
}
}
/**
* Returns true if this object is enabled.
*
* @return true if this object is enabled, false otherwise
*/
public boolean isEnabled() {
return enabled;
}
/**
* When displaying the <code>JToolTip</code>, the
* <code>ToolTipManager</code> chooses to use a lightweight
* <code>JPanel</code> if it fits. This method allows you to
* disable this feature. You have to do disable it if your
* application mixes light weight and heavy weights components.
*
* @param aFlag true if a lightweight panel is desired, false otherwise
*
*/
public void setLightWeightPopupEnabled(boolean aFlag){
lightWeightPopupEnabled = aFlag;
}
/**
* Returns true if lightweight (all-Java) <code>Tooltips</code>
* are in use, or false if heavyweight (native peer)
* <code>Tooltips</code> are being used.
*
* @return true if lightweight <code>ToolTips</code> are in use
*/
public boolean isLightWeightPopupEnabled() {
return lightWeightPopupEnabled;
}
/**
* Specifies the initial delay value.
*
* @param milliseconds the number of milliseconds to delay
* (after the cursor has paused) before displaying the
* tooltip
* @see #getInitialDelay
*/
public void setInitialDelay(int milliseconds) {
enterTimer.setInitialDelay(milliseconds);
}
/**
* Returns the initial delay value.
*
* @return an integer representing the initial delay value,
* in milliseconds
* @see #setInitialDelay
*/
public int getInitialDelay() {
return enterTimer.getInitialDelay();
}
/**
* Specifies the dismissal delay value.
*
* @param milliseconds the number of milliseconds to delay
* before taking away the tooltip
* @see #getDismissDelay
*/
public void setDismissDelay(int milliseconds) {
insideTimer.setInitialDelay(milliseconds);
}
/**
* Returns the dismissal delay value.
*
* @return an integer representing the dismissal delay value,
* in milliseconds
* @see #setDismissDelay
*/
public int getDismissDelay() {
return insideTimer.getInitialDelay();
}
/**
* Used to specify the amount of time before the user has to wait
* <code>initialDelay</code> milliseconds before a tooltip will be
* shown. That is, if the tooltip is hidden, and the user moves into
* a region of the same Component that has a valid tooltip within
* <code>milliseconds</code> milliseconds the tooltip will immediately
* be shown. Otherwise, if the user moves into a region with a valid
* tooltip after <code>milliseconds</code> milliseconds, the user
* will have to wait an additional <code>initialDelay</code>
* milliseconds before the tooltip is shown again.
*
* @param milliseconds time in milliseconds
* @see #getReshowDelay
*/
public void setReshowDelay(int milliseconds) {
exitTimer.setInitialDelay(milliseconds);
}
/**
* Returns the reshow delay property.
*
* @return reshown delay property
* @see #setReshowDelay
*/
public int getReshowDelay() {
return exitTimer.getInitialDelay();
}
void showTipWindow() {
if(insideComponent == null || !insideComponent.isShowing())
return;
String mode = UIManager.getString("ToolTipManager.enableToolTipMode");
if ("activeApplication".equals(mode)) {
KeyboardFocusManager kfm =
KeyboardFocusManager.getCurrentKeyboardFocusManager();
if (kfm.getFocusedWindow() == null) {
return;
}
}
if (enabled) {
Dimension size;
Point screenLocation = insideComponent.getLocationOnScreen();
Point location = new Point();
GraphicsConfiguration gc;
gc = insideComponent.getGraphicsConfiguration();
Rectangle sBounds = gc.getBounds();
Insets screenInsets = Toolkit.getDefaultToolkit()
.getScreenInsets(gc);
// Take into account screen insets, decrease viewport
sBounds.x += screenInsets.left;
sBounds.y += screenInsets.top;
sBounds.width -= (screenInsets.left + screenInsets.right);
sBounds.height -= (screenInsets.top + screenInsets.bottom);
boolean leftToRight
= SwingUtilities.isLeftToRight(insideComponent);
// Just to be paranoid
hideTipWindow();
tip = insideComponent.createToolTip();
tip.setTipText(toolTipText);
size = tip.getPreferredSize();
if(preferredLocation != null) {
location.x = screenLocation.x + preferredLocation.x;
location.y = screenLocation.y + preferredLocation.y;
if (!leftToRight) {
location.x -= size.width;
}
} else {
location.x = screenLocation.x + mouseEvent.getX();
location.y = screenLocation.y + mouseEvent.getY() + 20;
if (!leftToRight) {
if(location.x - size.width>=0) {
location.x -= size.width;
}
}
}
// we do not adjust x/y when using awt.Window tips
if (popupRect == null){
popupRect = new Rectangle();
}
popupRect.setBounds(location.x,location.y,
size.width,size.height);
// Fit as much of the tooltip on screen as possible
if (location.x < sBounds.x) {
location.x = sBounds.x;
}
else if (location.x - sBounds.x + size.width > sBounds.width) {
location.x = sBounds.x + Math.max(0, sBounds.width - size.width)
;
}
if (location.y < sBounds.y) {
location.y = sBounds.y;
}
else if (location.y - sBounds.y + size.height > sBounds.height) {
location.y = sBounds.y + Math.max(0, sBounds.height - size.height);
}
PopupFactory popupFactory = PopupFactory.getSharedInstance();
if (lightWeightPopupEnabled) {
int y = getPopupFitHeight(popupRect, insideComponent);
int x = getPopupFitWidth(popupRect,insideComponent);
if (x>0 || y>0) {
popupFactory.setPopupType(PopupFactory.MEDIUM_WEIGHT_POPUP);
} else {
popupFactory.setPopupType(PopupFactory.LIGHT_WEIGHT_POPUP);
}
}
else {
popupFactory.setPopupType(PopupFactory.MEDIUM_WEIGHT_POPUP);
}
tipWindow = popupFactory.getPopup(insideComponent, tip,
location.x,
location.y);
popupFactory.setPopupType(PopupFactory.LIGHT_WEIGHT_POPUP);
tipWindow.show();
Window componentWindow = SwingUtilities.windowForComponent(
insideComponent);
window = SwingUtilities.windowForComponent(tip);
if (window != null && window != componentWindow) {
window.addMouseListener(this);
}
else {
window = null;
}
insideTimer.start();
tipShowing = true;
}
}
void hideTipWindow() {
if (tipWindow != null) {
if (window != null) {
window.removeMouseListener(this);
window = null;
}
tipWindow.hide();
tipWindow = null;
tipShowing = false;
tip = null;
insideTimer.stop();
}
}
/**
* Returns a shared <code>ToolTipManager</code> instance.
*
* @return a shared <code>ToolTipManager</code> object
*/
public static ToolTipManager sharedInstance() {
return sharedInstance;
}
// add keylistener here to trigger tip for access
/**
* Registers a component for tooltip management.
* <p>
* This will register key bindings to show and hide the tooltip text
* only if <code>component</code> has focus bindings. This is done
* so that components that are not normally focus traversable, such
* as <code>JLabel</code>, are not made focus traversable as a result
* of invoking this method.
*
* @param component a <code>JComponent</code> object to add
* @see JComponent#isFocusTraversable
*/
public void registerComponent(JComponent component) {
component.removeMouseListener(this);
component.addMouseListener(this);
component.removeMouseMotionListener(moveBeforeEnterListener);
component.addMouseMotionListener(moveBeforeEnterListener);
if (shouldRegisterBindings(component)) {
// register our accessibility keybindings for this component
// this will apply globally across L&F
// Post Tip: Ctrl+F1
// Unpost Tip: Esc and Ctrl+F1
InputMap inputMap = component.getInputMap(JComponent.WHEN_FOCUSED);
ActionMap actionMap = component.getActionMap();
if (inputMap != null && actionMap != null) {
inputMap.put(postTip, "postTip");
inputMap.put(hideTip, "hideTip");
actionMap.put("postTip", postTipAction);
actionMap.put("hideTip", hideTipAction);
}
}
}
/**
* Removes a component from tooltip control.
*
* @param component a <code>JComponent</code> object to remove
*/
public void unregisterComponent(JComponent component) {
component.removeMouseListener(this);
component.removeMouseMotionListener(moveBeforeEnterListener);
if (shouldRegisterBindings(component)) {
InputMap inputMap = component.getInputMap(JComponent.WHEN_FOCUSED);
ActionMap actionMap = component.getActionMap();
if (inputMap != null && actionMap != null) {
inputMap.remove(postTip);
inputMap.remove(hideTip);
actionMap.remove("postTip");
actionMap.remove("hideTip");
}
}
}
/**
* Returns whether or not bindings should be registered on the given
* <code>JComponent</code>. This is implemented to return true if the
* tool tip manager has a binding in any one of the
* <code>InputMaps</code> registered under the condition
* <code>WHEN_FOCUSED</code>.
* <p>
* This does not use <code>isFocusTraversable</code> as
* some components may override <code>isFocusTraversable</code> and
* base the return value on something other than bindings. For example,
* <code>JButton</code> bases its return value on its enabled state.
*
* @param component the <code>JComponent</code> in question
*/
private boolean shouldRegisterBindings(JComponent component) {
InputMap inputMap = component.getInputMap(JComponent.WHEN_FOCUSED,
false);
while (inputMap != null && inputMap.size() == 0) {
inputMap = inputMap.getParent();
}
return (inputMap != null);
}
// implements java.awt.event.MouseListener
/**
* Called when the mouse enters the region of a component.
* This determines whether the tool tip should be shown.
*
* @param event the event in question
*/
public void mouseEntered(MouseEvent event) {
initiateToolTip(event);
}
private void initiateToolTip(MouseEvent event) {
if (event.getSource() == window) {
return;
}
JComponent component = (JComponent)event.getSource();
component.removeMouseMotionListener(moveBeforeEnterListener);
exitTimer.stop();
Point location = event.getPoint();
// ensure tooltip shows only in proper place
if (location.x < 0 ||
location.x >=component.getWidth() ||
location.y < 0 ||
location.y >= component.getHeight()) {
return;
}
if (insideComponent != null) {
enterTimer.stop();
}
// A component in an unactive internal frame is sent two
// mouseEntered events, make sure we don't end up adding
// ourselves an extra time.
component.removeMouseMotionListener(this);
component.addMouseMotionListener(this);
boolean sameComponent = (insideComponent == component);
insideComponent = component;
if (tipWindow != null){
mouseEvent = event;
if (showImmediately) {
String newToolTipText = component.getToolTipText(event);
Point newPreferredLocation = component.getToolTipLocation(
event);
boolean sameLoc = (preferredLocation != null) ?
preferredLocation.equals(newPreferredLocation) :
(newPreferredLocation == null);
if (!sameComponent || !toolTipText.equals(newToolTipText) ||
!sameLoc) {
toolTipText = newToolTipText;
preferredLocation = newPreferredLocation;
showTipWindow();
}
} else {
enterTimer.start();
}
}
}
// implements java.awt.event.MouseListener
/**
* Called when the mouse exits the region of a component.
* Any tool tip showing should be hidden.
*
* @param event the event in question
*/
public void mouseExited(MouseEvent event) {
boolean shouldHide = true;
if (insideComponent == null) {
// Drag exit
}
if (window != null && event.getSource() == window) {
// if we get an exit and have a heavy window
// we need to check if it if overlapping the inside component
Container insideComponentWindow = insideComponent.getTopLevelAncestor();
Point location = event.getPoint();
SwingUtilities.convertPointToScreen(location, window);
location.x -= insideComponentWindow.getX();
location.y -= insideComponentWindow.getY();
location = SwingUtilities.convertPoint(null,location,insideComponent);
if (location.x >= 0 && location.x < insideComponent.getWidth() &&
location.y >= 0 && location.y < insideComponent.getHeight()) {
shouldHide = false;
} else {
shouldHide = true;
}
} else if(event.getSource() == insideComponent && tipWindow != null) {
Window win = SwingUtilities.getWindowAncestor(insideComponent);
if (win != null) { // insideComponent may have been hidden (e.g. in a menu)
Point location = SwingUtilities.convertPoint(insideComponent,
event.getPoint(),
win);
Rectangle bounds = insideComponent.getTopLevelAncestor().getBounds();
location.x += bounds.x;
location.y += bounds.y;
Point loc = new Point(0, 0);
SwingUtilities.convertPointToScreen(loc, tip);
bounds.x = loc.x;
bounds.y = loc.y;
bounds.width = tip.getWidth();
bounds.height = tip.getHeight();
if (location.x >= bounds.x && location.x < (bounds.x + bounds.width) &&
location.y >= bounds.y && location.y < (bounds.y + bounds.height)) {
shouldHide = false;
} else {
shouldHide = true;
}
}
}
if (shouldHide) {
enterTimer.stop();
if (insideComponent != null) {
insideComponent.removeMouseMotionListener(this);
}
insideComponent = null;
toolTipText = null;
mouseEvent = null;
hideTipWindow();
exitTimer.restart();
}
}
// implements java.awt.event.MouseListener
/**
* Called when the mouse is pressed.
* Any tool tip showing should be hidden.
*
* @param event the event in question
*/
public void mousePressed(MouseEvent event) {
hideTipWindow();
enterTimer.stop();
showImmediately = false;
insideComponent = null;
mouseEvent = null;
}
// implements java.awt.event.MouseMotionListener
/**
* Called when the mouse is pressed and dragged.
* Does nothing.
*
* @param event the event in question
*/
public void mouseDragged(MouseEvent event) {
}
// implements java.awt.event.MouseMotionListener
/**
* Called when the mouse is moved.
* Determines whether the tool tip should be displayed.
*
* @param event the event in question
*/
public void mouseMoved(MouseEvent event) {
if (tipShowing) {
checkForTipChange(event);
}
else if (showImmediately) {
JComponent component = (JComponent)event.getSource();
toolTipText = component.getToolTipText(event);
if (toolTipText != null) {
preferredLocation = component.getToolTipLocation(event);
mouseEvent = event;
insideComponent = component;
exitTimer.stop();
showTipWindow();
}
}
else {
// Lazily lookup the values from within insideTimerAction
insideComponent = (JComponent)event.getSource();
mouseEvent = event;
toolTipText = null;
enterTimer.restart();
}
}
/**
* Checks to see if the tooltip needs to be changed in response to
* the MouseMoved event <code>event</code>.
*/
private void checkForTipChange(MouseEvent event) {
JComponent component = (JComponent)event.getSource();
String newText = component.getToolTipText(event);
Point newPreferredLocation = component.getToolTipLocation(event);
if (newText != null || newPreferredLocation != null) {
mouseEvent = event;
if (((newText != null && newText.equals(toolTipText)) || newText == null) &&
((newPreferredLocation != null && newPreferredLocation.equals(preferredLocation))
|| newPreferredLocation == null)) {
if (tipWindow != null) {
insideTimer.restart();
} else {
enterTimer.restart();
}
} else {
toolTipText = newText;
preferredLocation = newPreferredLocation;
if (showImmediately) {
hideTipWindow();
showTipWindow();
exitTimer.stop();
} else {
enterTimer.restart();
}
}
} else {
toolTipText = null;
preferredLocation = null;
mouseEvent = null;
insideComponent = null;
hideTipWindow();
enterTimer.stop();
exitTimer.restart();
}
}
protected class insideTimerAction implements ActionListener {
public void actionPerformed(ActionEvent e) {
if(insideComponent != null && insideComponent.isShowing()) {
// Lazy lookup
if (toolTipText == null && mouseEvent != null) {
toolTipText = insideComponent.getToolTipText(mouseEvent);
preferredLocation = insideComponent.getToolTipLocation(
mouseEvent);
}
if(toolTipText != null) {
showImmediately = true;
showTipWindow();
}
else {
insideComponent = null;
toolTipText = null;
preferredLocation = null;
mouseEvent = null;
hideTipWindow();
}
}
}
}
protected class outsideTimerAction implements ActionListener {
public void actionPerformed(ActionEvent e) {
showImmediately = false;
}
}
protected class stillInsideTimerAction implements ActionListener {
public void actionPerformed(ActionEvent e) {
hideTipWindow();
enterTimer.stop();
showImmediately = false;
insideComponent = null;
mouseEvent = null;
}
}
/* This listener is registered when the tooltip is first registered
* on a component in order to catch the situation where the tooltip
* was turned on while the mouse was already within the bounds of
* the component. This way, the tooltip will be initiated on a
* mouse-entered or mouse-moved, whichever occurs first. Once the
* tooltip has been initiated, we can remove this listener and rely
* solely on mouse-entered to initiate the tooltip.
*/
private class MoveBeforeEnterListener extends MouseMotionAdapter {
public void mouseMoved(MouseEvent e) {
initiateToolTip(e);
}
}
static Frame frameForComponent(Component component) {
while (!(component instanceof Frame)) {
component = component.getParent();
}
return (Frame)component;
}
private FocusListener createFocusChangeListener(){
return new FocusAdapter(){
public void focusLost(FocusEvent evt){
hideTipWindow();
insideComponent = null;
JComponent c = (JComponent)evt.getSource();
c.removeFocusListener(focusChangeListener);
}
};
}
// Returns: 0 no adjust
// -1 can't fit
// >0 adjust value by amount returned
private int getPopupFitWidth(Rectangle popupRectInScreen, Component invoker){
if (invoker != null){
Container parent;
for (parent = invoker.getParent(); parent != null; parent = parent.getParent()){
// fix internal frame size bug: 4139087 - 4159012
if(parent instanceof JFrame || parent instanceof JDialog ||
parent instanceof JWindow) { // no check for awt.Frame since we use Heavy tips
return getWidthAdjust(parent.getBounds(),popupRectInScreen);
} else if (parent instanceof JApplet || parent instanceof JInternalFrame) {
if (popupFrameRect == null){
popupFrameRect = new Rectangle();
}
Point p = parent.getLocationOnScreen();
popupFrameRect.setBounds(p.x,p.y,
parent.getBounds().width,
parent.getBounds().height);
return getWidthAdjust(popupFrameRect,popupRectInScreen);
}
}
}
return 0;
}
// Returns: 0 no adjust
// >0 adjust by value return
private int getPopupFitHeight(Rectangle popupRectInScreen, Component invoker){
if (invoker != null){
Container parent;
for (parent = invoker.getParent(); parent != null; parent = parent.getParent()){
if(parent instanceof JFrame || parent instanceof JDialog ||
parent instanceof JWindow) {
return getHeightAdjust(parent.getBounds(),popupRectInScreen);
} else if (parent instanceof JApplet || parent instanceof JInternalFrame) {
if (popupFrameRect == null){
popupFrameRect = new Rectangle();
}
Point p = parent.getLocationOnScreen();
popupFrameRect.setBounds(p.x,p.y,
parent.getBounds().width,
parent.getBounds().height);
return getHeightAdjust(popupFrameRect,popupRectInScreen);
}
}
}
return 0;
}
private int getHeightAdjust(Rectangle a, Rectangle b){
if (b.y >= a.y && (b.y + b.height) <= (a.y + a.height))
return 0;
else
return (((b.y + b.height) - (a.y + a.height)) + 5);
}
// Return the number of pixels over the edge we are extending.
// If we are over the edge the ToolTipManager can adjust.
// REMIND: what if the Tooltip is just too big to fit at all - we currently will just clip
private int getWidthAdjust(Rectangle a, Rectangle b){
// System.out.println("width b.x/b.width: " + b.x + "/" + b.width +
// "a.x/a.width: " + a.x + "/" + a.width);
if (b.x >= a.x && (b.x + b.width) <= (a.x + a.width)){
return 0;
}
else {
return (((b.x + b.width) - (a.x +a.width)) + 5);
}
}
//
// Actions
//
private void show(JComponent source) {
if (tipWindow != null) { // showing we unshow
hideTipWindow();
insideComponent = null;
}
else {
hideTipWindow(); // be safe
enterTimer.stop();
exitTimer.stop();
insideTimer.stop();
insideComponent = source;
if (insideComponent != null){
toolTipText = insideComponent.getToolTipText();
preferredLocation = new Point(10,insideComponent.getHeight()+
10); // manual set
showTipWindow();
// put a focuschange listener on to bring the tip down
if (focusChangeListener == null){
focusChangeListener = createFocusChangeListener();
}
insideComponent.addFocusListener(focusChangeListener);
}
}
}
private void hide(JComponent source) {
hideTipWindow();
source.removeFocusListener(focusChangeListener);
preferredLocation = null;
insideComponent = null;
}
private static class Actions extends UIAction {
private static String SHOW = "SHOW";
private static String HIDE = "HIDE";
Actions(String key) {
super(key);
}
public void actionPerformed(ActionEvent e) {
String key = getName();
JComponent source = (JComponent)e.getSource();
if (key == SHOW) {
ToolTipManager.sharedInstance().show(source);
}
else if (key == HIDE) {
ToolTipManager.sharedInstance().hide(source);
}
}
public boolean isEnabled(Object sender) {
if (getName() == SHOW) {
return true;
}
return ToolTipManager.sharedInstance().tipShowing;
}
}
}