/* * @(#)ToolTipManager.java 1.40 99/04/22 * * Copyright 1997-1999 by Sun Microsystems, Inc., * 901 San Antonio Road, Palo Alto, California, 94303, U.S.A. * All rights reserved. * * This software is the confidential and proprietary information * of Sun Microsystems, Inc. ("Confidential Information"). You * shall not disclose such Confidential Information and shall use * it only in accordance with the terms of the license agreement * you entered into with Sun. */ import javax.swing.*; import java.awt.event.*; import java.applet.*; import java.awt.*; /** * Manages all the ToolTips in the system. * * @see JComponent#createToolTip * @version 1.40 04/22/99 * @author Dave Moore * @author Rich Schiavi */ public class HeavyToolTipManager extends MouseAdapter implements MouseMotionListener { Timer enterTimer, exitTimer, insideTimer; String toolTipText; Point preferredLocation; JComponent insideComponent; MouseEvent mouseEvent; boolean showImmediately; Popup tipWindow; JToolTip tip; private Rectangle popupRect = null; private Rectangle popupFrameRect = null; boolean enabled = true; boolean mouseAboveToolTip = false; private boolean tipShowing = false; private long timerEnter = 0; private KeyStroke postTip,hideTip; private AbstractAction postTipAction, hideTipAction; private FocusListener focusChangeListener = null; protected boolean lightWeightPopupEnabled = true; protected boolean heavyWeightPopupEnabled = false; final static HeavyToolTipManager sharedInstance = new HeavyToolTipManager(); /** Returns a shared HeavyToolTipManager instance. */ public static HeavyToolTipManager sharedInstance() { return sharedInstance; } //=======================================================================// // Constructor //=======================================================================// HeavyToolTipManager() { 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 AbstractAction() { public void actionPerformed(ActionEvent e) { if (tipWindow != null) // showing we unshow hideTipWindow(); else { hideTipWindow(); // be safe enterTimer.stop(); exitTimer.stop(); insideTimer.stop(); insideComponent = (JComponent)e.getSource(); 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); } } } }; hideTip = KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE,0); hideTipAction = new AbstractAction() { public void actionPerformed(ActionEvent e) { hideTipWindow(); JComponent jc = (JComponent)e.getSource(); jc.removeFocusListener(focusChangeListener); preferredLocation = null; } public boolean isEnabled() { // Only enable when the tooltip is showing, otherwise // we will get in the way of any UI actions. return tipShowing; } }; } //=======================================================================// // Properties //=======================================================================// /** Enables or disables the tooltip. */ public void setEnabled(boolean flag) { enabled = flag; if (!flag) hideTipWindow(); } /** Returns true if this object is enabled. */ public boolean isEnabled() { return enabled; } /** Specifies the initial delay value. */ public void setInitialDelay(int microSeconds) { enterTimer.setInitialDelay(microSeconds); } /** Returns the initial delay value. */ public int getInitialDelay() { return enterTimer.getInitialDelay(); } /** Specifies the dismisal delay value. */ public void setDismissDelay(int microSeconds) { insideTimer.setInitialDelay(microSeconds); } /** Returns the dismisal delay value. */ public int getDismissDelay() { return insideTimer.getInitialDelay(); } /** Specifies the time to delay before reshowing the tooltip. */ public void setReshowDelay(int microSeconds) { exitTimer.setInitialDelay(microSeconds); } /** Returns the reshow delay value. */ public int getReshowDelay() { return exitTimer.getInitialDelay(); } //=======================================================================// // Action //=======================================================================// void showTipWindow() { if (insideComponent == null || !insideComponent.isShowing()) return; if (enabled) { Dimension size; Point screenLocation = insideComponent.getLocationOnScreen(); Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize(); Point location = new Point(); // Just to be paranoid hideTipWindow(); tip = insideComponent.createToolTip(); tip.setTipText(toolTipText); size = tip.getPreferredSize(); // support only heavy tooltips tipWindow = new WindowPopup((frameForComponent(insideComponent)),tip,size); tipWindow.addMouseListener(this); if (preferredLocation != null) { location.x = screenLocation.x + preferredLocation.x; location.y = screenLocation.y + preferredLocation.y; } else { location.x = screenLocation.x + mouseEvent.getX(); location.y = screenLocation.y + mouseEvent.getY() + 20; if (location.x + size.width > screenSize.width) location.x -= size.width; if (location.y + size.height > screenSize.height) location.y -= (size.height + 20); } tipWindow.show(insideComponent,location.x,location.y); insideTimer.start(); timerEnter = System.currentTimeMillis(); tipShowing = true; } } void hideTipWindow() { if (tipWindow != null) { tipWindow.removeMouseListener(this); tipWindow.hide(); tipWindow = null; tipShowing = false; timerEnter = 0; (tip.getUI()).uninstallUI(tip); tip = null; insideTimer.stop(); } } /** * Register a component for tooltip management. *
This will register key bindings to show and hide the tooltip text
* only if component has focus bindings. This is done
* so that components that are not normally focus traversable, such
* as JLabel, are not made focus traversable as a result of invoking
* this method.
*/
public void registerComponent(JComponent component)
{
component.removeMouseListener(this);
component.addMouseListener(this);
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);
}
}
}
/** Remove a component from tooltip control. */
public void unregisterComponent(JComponent component)
{
component.removeMouseListener(this);
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
* Component. This is implemented to return true if the receiver has
* a binding in any one of the InputMaps registered under the condition
* WHEN_FOCUSED.
*
* This does not use isFocusTraversable as
* some components may override isFocusTraversable and
* base the return value on something other than bindings. For example,
* JButton bases its return value on its enabled state.
*/
private boolean shouldRegisterBindings(JComponent component)
{
InputMap inputMap = component.getInputMap(JComponent.WHEN_FOCUSED);
while (inputMap != null && inputMap.size() == 0)
{
inputMap = inputMap.getParent();
}
return (inputMap != null);
}
//=======================================================================//
// Subsidiary functions
//=======================================================================//
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();
JComponent c = (JComponent)evt.getSource();
c.removeFocusListener(focusChangeListener);
}
};
}
//=======================================================================//
// MouseListener interface
//=======================================================================//
public void mouseEntered(MouseEvent event)
{
// this is here for a workaround for a Solaris *application* only bug
// in which an extra MouseExit/Enter events are generated when a Panel
// initially is shown
if (tipShowing)
{
if (System.currentTimeMillis() - timerEnter < 200)
return;
}
if (event.getSource() == tipWindow)
return;
JComponent component = (JComponent)event.getSource();
toolTipText = component.getToolTipText(event);
preferredLocation = component.getToolTipLocation(event);
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();
insideComponent = null;
}
component.addMouseMotionListener(this);
insideComponent = component;
if (tipWindow != null)
return;
}
public void mouseExited(MouseEvent event)
{
// this is here for a workaround for a Solaris *application* only bug
// when Panels are used
if (tipShowing)
{
if (System.currentTimeMillis() - timerEnter < 200)
return;
}
boolean shouldHide = true;
if(event.getSource() == tipWindow)
{
// 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();
Rectangle b = tipWindow.getBounds();
Point location = event.getPoint();
location.x += b.x;
location.y += b.y;
b = insideComponentWindow.getBounds();
location.x -= b.x;
location.y -= b.y;
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)
{
Point location = SwingUtilities.convertPoint(insideComponent, event.getPoint(), null);
Rectangle bounds = insideComponent.getTopLevelAncestor().getBounds();
location.x += bounds.x;
location.y += bounds.y;
bounds = tipWindow.getBounds();
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.start();
}
}
public void mousePressed(MouseEvent event)
{
hideTipWindow();
enterTimer.stop();
showImmediately = false;
}
//=======================================================================//
// MouseMotionListener interface
//=======================================================================//
public void mouseDragged(MouseEvent event)
{
}
public void mouseMoved(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();
}
else
enterTimer.restart();
}
}
else
{
toolTipText = null;
preferredLocation = null;
mouseEvent = null;
hideTipWindow();
enterTimer.stop();
exitTimer.start();
}
}
//=======================================================================//
// Class insideTimerAction
//=======================================================================//
protected class insideTimerAction implements ActionListener
{
public void actionPerformed(ActionEvent e)
{
if (insideComponent != null && insideComponent.isShowing())
{
showImmediately = true;
showTipWindow();
}
}
}
//=======================================================================//
// Class insideTimerAction
//=======================================================================//
protected class outsideTimerAction implements ActionListener
{
public void actionPerformed(ActionEvent e)
{
showImmediately = false;
}
}
//=======================================================================//
// Class insideTimerAction
//=======================================================================//
protected class stillInsideTimerAction implements ActionListener
{
public void actionPerformed(ActionEvent e)
{
hideTipWindow();
enterTimer.stop();
showImmediately = false;
}
}
//=======================================================================//
// Interface Popup
//=======================================================================//
/*
* The following interface describes what a popup should implement.
* We do this because the ToolTip manager uses popup that can be windows or
* panels. The reason is two-fold: We'd like to use panels mostly, but when the
* panel (or tooltip) would not fit, we need to use a Window to avoid the panel
* being clipped or not shown.
*
*/
private interface Popup
{
public void show(JComponent invoker, int x, int y);
public void hide();
public void addMouseListener(HeavyToolTipManager c);
public void removeMouseListener(HeavyToolTipManager c);
public Rectangle getBounds();
}
//=======================================================================//
// Class WindowPopup
//=======================================================================//
class WindowPopup extends Window implements Popup
{
boolean firstShow = true;
JComponent tip;
Frame frame;
public WindowPopup(Frame f,JComponent t, Dimension size)
{
super(f);
this.tip = t;
this.frame = f;
add(t, BorderLayout.CENTER);
pack();
// setSize(size);
}
public Rectangle getBounds()
{
return super.getBounds();
}
public void show(JComponent invoker, int x, int y)
{
this.setLocation(x,y);
this.setVisible(true);
/** This hack is to workaround a bug on Solaris where the windows does not really show
* the first time
* It causes a side effect of MS JVM reporting IllegalArumentException: null source
* fairly frequently - also happens if you use HeavyWeight JPopup, ie JComboBox
*/
if(firstShow)
{
this.hide();
this.setVisible(true);
firstShow = false;
}
}
public void hide()
{
super.hide();
/** We need to call removeNotify() here because hide() does something only if
* Component.visible is true. When the app frame is miniaturized, the parent
* frame of this frame is invisible, causing AWT to believe that this frame
* is invisible and causing hide() to do nothing
*/
removeNotify();
}
public void addMouseListener(HeavyToolTipManager c)
{
super.addMouseListener(c);
}
public void removeMouseListener(HeavyToolTipManager c)
{
super.removeMouseListener(c);
}
}
}