/* * Copyright (c) 1997, 2013, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it * under the terms of the GNU General Public License version 2 only, as * published by the Free Software Foundation. Oracle designates this * particular file as subject to the "Classpath" exception as provided * by Oracle in the LICENSE file that accompanied this code. * * This code is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License * version 2 for more details (a copy is included in the LICENSE file that * accompanied this code). * * You should have received a copy of the GNU General Public License version * 2 along with this work; if not, write to the Free Software Foundation, * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. * * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA * or visit www.oracle.com if you need additional information or have any * questions. */ package javax.swing; import java.awt.event.*; import java.awt.*; /** * Manages all the ToolTips in the system. *

* 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 * visible after initialDelay milliseconds. After * dismissDelay 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 reshowDelay * milliseconds, the tooltip will immediately be shown, otherwise the * tooltip will be shown again after initialDelay milliseconds. * * @see JComponent#createToolTip * @author Dave Moore * @author Rich Schiavi * @since 1.2 */ public class ToolTipManager extends MouseAdapter implements MouseMotionListener { Timer enterTimer, exitTimer, insideTimer; String toolTipText; Point preferredLocation; JComponent insideComponent; MouseEvent mouseEvent; boolean showImmediately; private static final Object TOOL_TIP_MANAGER_KEY = new Object(); 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 FocusListener focusChangeListener = null; private MouseMotionListener moveBeforeEnterListener = null; private KeyListener accessibilityKeyListener = null; private KeyStroke postTip; private KeyStroke hideTip; // 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); moveBeforeEnterListener = new MoveBeforeEnterListener(); accessibilityKeyListener = new AccessibilityKeyListener(); postTip = KeyStroke.getKeyStroke(KeyEvent.VK_F1, InputEvent.CTRL_MASK); hideTip = KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0); } /** * 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 JToolTip, the * ToolTipManager chooses to use a lightweight * JPanel 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) Tooltips * are in use, or false if heavyweight (native peer) * Tooltips are being used. * * @return true if lightweight ToolTips 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 * initialDelay 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 * milliseconds milliseconds the tooltip will immediately * be shown. Otherwise, if the user moves into a region with a valid * tooltip after milliseconds milliseconds, the user * will have to wait an additional initialDelay * 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(); } // Returns GraphicsConfiguration instance that toFind belongs to or null // if drawing point is set to a point beyond visible screen area (e.g. // Point(20000, 20000)) private GraphicsConfiguration getDrawingGC(Point toFind) { GraphicsEnvironment env = GraphicsEnvironment.getLocalGraphicsEnvironment(); GraphicsDevice devices[] = env.getScreenDevices(); for (GraphicsDevice device : devices) { GraphicsConfiguration configs[] = device.getConfigurations(); for (GraphicsConfiguration config : configs) { Rectangle rect = config.getBounds(); if (rect.contains(toFind)) { return config; } } } return null; } 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; Point toFind; if (preferredLocation != null) { toFind = new Point(screenLocation.x + preferredLocation.x, screenLocation.y + preferredLocation.y); } else { toFind = mouseEvent.getLocationOnScreen(); } GraphicsConfiguration gc = getDrawingGC(toFind); if (gc == null) { toFind = mouseEvent.getLocationOnScreen(); gc = getDrawingGC(toFind); if (gc == null) { 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 = toFind; if (!leftToRight) { location.x -= size.width; } } else { location = new Point(screenLocation.x + mouseEvent.getX(), 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 ToolTipManager instance. * * @return a shared ToolTipManager object */ public static ToolTipManager sharedInstance() { Object value = SwingUtilities.appContextGet(TOOL_TIP_MANAGER_KEY); if (value instanceof ToolTipManager) { return (ToolTipManager) value; } ToolTipManager manager = new ToolTipManager(); SwingUtilities.appContextPut(TOOL_TIP_MANAGER_KEY, manager); return manager; } // add keylistener here to trigger tip for access /** * Registers 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. * * @param component a JComponent object to add * @see JComponent#isFocusTraversable */ public void registerComponent(JComponent component) { component.removeMouseListener(this); component.addMouseListener(this); component.removeMouseMotionListener(moveBeforeEnterListener); component.addMouseMotionListener(moveBeforeEnterListener); component.removeKeyListener(accessibilityKeyListener); component.addKeyListener(accessibilityKeyListener); } /** * Removes a component from tooltip control. * * @param component a JComponent object to remove */ public void unregisterComponent(JComponent component) { component.removeMouseListener(this); component.removeMouseMotionListener(moveBeforeEnterListener); component.removeKeyListener(accessibilityKeyListener); } // 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 && insideComponent != null) { // 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(); // insideComponent may be removed after tooltip is made visible if (insideComponentWindow != null) { 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 event. */ 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; } /* This listener is registered when the tooltip is first registered * on a component in order to process accessibility keybindings. * This will apply globally across L&F * * Post Tip: Ctrl+F1 * Unpost Tip: Esc and Ctrl+F1 */ private class AccessibilityKeyListener extends KeyAdapter { public void keyPressed(KeyEvent e) { if (!e.isConsumed()) { JComponent source = (JComponent) e.getComponent(); KeyStroke keyStrokeForEvent = KeyStroke.getKeyStrokeForEvent(e); if (hideTip.equals(keyStrokeForEvent)) { if (tipWindow != null) { hide(source); e.consume(); } } else if (postTip.equals(keyStrokeForEvent)) { // Shown tooltip will be hidden ToolTipManager.this.show(source); e.consume(); } } } } }