/* * This file is modified by Ivan Maidanski <ivmai@ivmaisoft.com> * Project name: JCGO-SUNAWT (http://www.ivmaisoft.com/jcgo/) */ /* * @(#)ToolTipManager.java 1.65 03/01/23 * * Copyright 2003 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; /** * 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.65 01/23/03 * @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; 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 AbstractAction(){ public void actionPerformed(ActionEvent e){ if (tipWindow != null) { // showing we unshow hideTipWindow(); insideComponent = null; } 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; insideComponent = null; } public boolean isEnabled() { // Only enable when the tooltip is showing, otherwise // we will get in the way of any UI actions. return tipShowing; } }; 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; if (enabled) { Dimension size; Point screenLocation = insideComponent.getLocationOnScreen(); Point location = new Point(); Rectangle sBounds = insideComponent.getGraphicsConfiguration(). getBounds(); 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 (!heavyWeightPopupEnabled){ if (popupRect == null){ popupRect = new Rectangle(); } popupRect.setBounds(location.x,location.y, size.width,size.height); int y = getPopupFitHeight(popupRect, insideComponent); int x = getPopupFitWidth(popupRect,insideComponent); if (y > 0){ location.y -= y; } if (x > 0){ // adjust location.x -= x; } } // 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) { 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.getUI()).uninstallUI(tip); tip = null; insideTimer.stop(); } } /** * Returns a shared <code>ToolTipManager</code> instance. * * @return a shared <code>ToolTipManager</code> object */ public static synchronized ToolTipManager sharedInstance() { if (sharedInstance == null) sharedInstance = new ToolTipManager(); return sharedInstance; } private static ToolTipManager 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); } } }