/* * JavaXYQ Source Code * LightweightToolTipManager LightweightToolTipManager.groovy * by kylixs 2009-10 * All Rights Reserved. * Please see also http://javaxyq.cn or http://javaxyq.googlecode.com. * Please email to javaxyq@qq.com. */ package com.javaxyq.ui; import java.awt.Component; import java.awt.Container; import java.awt.Dimension; import java.awt.Event; import java.awt.Frame; import java.awt.GraphicsConfiguration; import java.awt.Insets; import java.awt.KeyboardFocusManager; import java.awt.Point; import java.awt.Rectangle; import java.awt.Toolkit; import java.awt.Window; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.FocusAdapter; import java.awt.event.FocusEvent; import java.awt.event.FocusListener; import java.awt.event.KeyEvent; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import java.awt.event.MouseMotionAdapter; import java.awt.event.MouseMotionListener; import javax.swing.Action; import javax.swing.ActionMap; import javax.swing.InputMap; import javax.swing.JComponent; import javax.swing.JToolTip; import javax.swing.KeyStroke; import javax.swing.Popup; import javax.swing.PopupFactory; import javax.swing.SwingUtilities; import javax.swing.Timer; import javax.swing.UIManager; import sun.swing.UIAction; /** * @author dewitt * */ public class LightweightToolTipManager extends MouseAdapter implements MouseMotionListener { static final LightweightToolTipManager sharedInstance = new LightweightToolTipManager(); public static LightweightToolTipManager sharedInstance() { return sharedInstance; } 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; LightweightToolTipManager() { 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; // ensure tooltip location ensureApplicableLocation(); 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 = true;// 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(); } } // 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); 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) { LightweightToolTipManager.sharedInstance().show(source); } else if (key == HIDE) { LightweightToolTipManager.sharedInstance().hide(source); } } public boolean isEnabled(Object sender) { if (getName() == SHOW) { return true; } return LightweightToolTipManager.sharedInstance().tipShowing; } } protected void ensureApplicableLocation() { Container top = insideComponent.getTopLevelAncestor(); tip = insideComponent.createToolTip(); tip.setPreferredSize(null); tip.setTipText(insideComponent.getToolTipText()); Dimension size = tip.getPreferredSize(); Point topLocation = top.getLocationOnScreen(); if (preferredLocation == null) { preferredLocation = mouseEvent.getPoint(); preferredLocation.translate(10, 25); } SwingUtilities.convertPointToScreen(preferredLocation, insideComponent); //System.out.println("tip's size "+size+", on "+preferredLocation.x); int maxX = preferredLocation.x + size.width + 10; int maxY = preferredLocation.y + size.height + 10; int right = topLocation.x + top.getWidth(); if (maxX > right) { preferredLocation.x = right - size.width - 10; } int bottom = topLocation.y + top.getHeight(); //System.out.println(" tip("+maxX+","+maxY+") , edge("+right+","+bottom+")"); if (maxY > bottom) { preferredLocation.y = bottom - size.height - 10; } //System.out.println("\t=>"+preferredLocation); SwingUtilities.convertPointFromScreen(preferredLocation, insideComponent); } }