Java tutorial
/* * Copyright (c) 1997, 2018, 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.*; import java.util.Objects; /** * 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 * visible 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 * @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; /** * Lightweight popup enabled. */ protected boolean lightWeightPopupEnabled = true; /** * Heavyweight popup enabled. */ protected boolean heavyWeightPopupEnabled = false; @SuppressWarnings("deprecation") 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 <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(); } // 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 config = device.getDefaultConfiguration(); 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 <code>ToolTipManager</code> instance. * * @return a shared <code>ToolTipManager</code> 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. * <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); component.removeKeyListener(accessibilityKeyListener); component.addKeyListener(accessibilityKeyListener); } /** * 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); 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 || !Objects.equals(toolTipText, 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 <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(); } } /** * Inside timer action. */ protected class insideTimerAction implements ActionListener { /** * {@inheritDoc} */ 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(); } } } } /** * Outside timer action. */ protected class outsideTimerAction implements ActionListener { /** * {@inheritDoc} */ public void actionPerformed(ActionEvent e) { showImmediately = false; } } /** * Still inside timer action. */ protected class stillInsideTimerAction implements ActionListener { /** * {@inheritDoc} */ 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 @SuppressWarnings("deprecation") 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 @SuppressWarnings("deprecation") 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(); } } } } }