1 /*
   2  * Copyright (c) 1997, 2016, Oracle and/or its affiliates. All rights reserved.
   3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   4  *
   5  * This code is free software; you can redistribute it and/or modify it
   6  * under the terms of the GNU General Public License version 2 only, as
   7  * published by the Free Software Foundation.  Oracle designates this
   8  * particular file as subject to the "Classpath" exception as provided
   9  * by Oracle in the LICENSE file that accompanied this code.
  10  *
  11  * This code is distributed in the hope that it will be useful, but WITHOUT
  12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  14  * version 2 for more details (a copy is included in the LICENSE file that
  15  * accompanied this code).
  16  *
  17  * You should have received a copy of the GNU General Public License version
  18  * 2 along with this work; if not, write to the Free Software Foundation,
  19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  20  *
  21  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  22  * or visit www.oracle.com if you need additional information or have any
  23  * questions.
  24  */
  25 
  26 
  27 package javax.swing;
  28 
  29 import java.awt.event.*;
  30 import java.awt.*;
  31 import java.util.Objects;
  32 
  33 /**
  34  * Manages all the <code>ToolTips</code> in the system.
  35  * <p>
  36  * ToolTipManager contains numerous properties for configuring how long it
  37  * will take for the tooltips to become visible, and how long till they
  38  * hide. Consider a component that has a different tooltip based on where
  39  * the mouse is, such as JTree. When the mouse moves into the JTree and
  40  * over a region that has a valid tooltip, the tooltip will become
  41  * visible after <code>initialDelay</code> milliseconds. After
  42  * <code>dismissDelay</code> milliseconds the tooltip will be hidden. If
  43  * the mouse is over a region that has a valid tooltip, and the tooltip
  44  * is currently visible, when the mouse moves to a region that doesn't have
  45  * a valid tooltip the tooltip will be hidden. If the mouse then moves back
  46  * into a region that has a valid tooltip within <code>reshowDelay</code>
  47  * milliseconds, the tooltip will immediately be shown, otherwise the
  48  * tooltip will be shown again after <code>initialDelay</code> milliseconds.
  49  *
  50  * @see JComponent#createToolTip
  51  * @author Dave Moore
  52  * @author Rich Schiavi
  53  * @since 1.2
  54  */
  55 public class ToolTipManager extends MouseAdapter implements MouseMotionListener  {
  56     Timer enterTimer, exitTimer, insideTimer;
  57     String toolTipText;
  58     Point  preferredLocation;
  59     JComponent insideComponent;
  60     MouseEvent mouseEvent;
  61     boolean showImmediately;
  62     private static final Object TOOL_TIP_MANAGER_KEY = new Object();
  63     transient Popup tipWindow;
  64     /** The Window tip is being displayed in. This will be non-null if
  65      * the Window tip is in differs from that of insideComponent's Window.
  66      */
  67     private Window window;
  68     JToolTip tip;
  69 
  70     private Rectangle popupRect = null;
  71     private Rectangle popupFrameRect = null;
  72 
  73     boolean enabled = true;
  74     private boolean tipShowing = false;
  75 
  76     private FocusListener focusChangeListener = null;
  77     private MouseMotionListener moveBeforeEnterListener = null;
  78     private KeyListener accessibilityKeyListener = null;
  79 
  80     private KeyStroke postTip;
  81     private KeyStroke hideTip;
  82 
  83     /**
  84      * Lightweight popup enabled.
  85      */
  86     protected boolean lightWeightPopupEnabled = true;
  87     /**
  88      * Heavyweight popup enabled.
  89      */
  90     protected boolean heavyWeightPopupEnabled = false;
  91 
  92     ToolTipManager() {
  93         enterTimer = new Timer(750, new insideTimerAction());
  94         enterTimer.setRepeats(false);
  95         exitTimer = new Timer(500, new outsideTimerAction());
  96         exitTimer.setRepeats(false);
  97         insideTimer = new Timer(4000, new stillInsideTimerAction());
  98         insideTimer.setRepeats(false);
  99 
 100         moveBeforeEnterListener = new MoveBeforeEnterListener();
 101         accessibilityKeyListener = new AccessibilityKeyListener();
 102 
 103         postTip = KeyStroke.getKeyStroke(KeyEvent.VK_F1, InputEvent.CTRL_MASK);
 104         hideTip =  KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0);
 105     }
 106 
 107     /**
 108      * Enables or disables the tooltip.
 109      *
 110      * @param flag  true to enable the tip, false otherwise
 111      */
 112     public void setEnabled(boolean flag) {
 113         enabled = flag;
 114         if (!flag) {
 115             hideTipWindow();
 116         }
 117     }
 118 
 119     /**
 120      * Returns true if this object is enabled.
 121      *
 122      * @return true if this object is enabled, false otherwise
 123      */
 124     public boolean isEnabled() {
 125         return enabled;
 126     }
 127 
 128     /**
 129      * When displaying the <code>JToolTip</code>, the
 130      * <code>ToolTipManager</code> chooses to use a lightweight
 131      * <code>JPanel</code> if it fits. This method allows you to
 132      * disable this feature. You have to do disable it if your
 133      * application mixes light weight and heavy weights components.
 134      *
 135      * @param aFlag true if a lightweight panel is desired, false otherwise
 136      *
 137      */
 138     public void setLightWeightPopupEnabled(boolean aFlag){
 139         lightWeightPopupEnabled = aFlag;
 140     }
 141 
 142     /**
 143      * Returns true if lightweight (all-Java) <code>Tooltips</code>
 144      * are in use, or false if heavyweight (native peer)
 145      * <code>Tooltips</code> are being used.
 146      *
 147      * @return true if lightweight <code>ToolTips</code> are in use
 148      */
 149     public boolean isLightWeightPopupEnabled() {
 150         return lightWeightPopupEnabled;
 151     }
 152 
 153 
 154     /**
 155      * Specifies the initial delay value.
 156      *
 157      * @param milliseconds  the number of milliseconds to delay
 158      *        (after the cursor has paused) before displaying the
 159      *        tooltip
 160      * @see #getInitialDelay
 161      */
 162     public void setInitialDelay(int milliseconds) {
 163         enterTimer.setInitialDelay(milliseconds);
 164     }
 165 
 166     /**
 167      * Returns the initial delay value.
 168      *
 169      * @return an integer representing the initial delay value,
 170      *          in milliseconds
 171      * @see #setInitialDelay
 172      */
 173     public int getInitialDelay() {
 174         return enterTimer.getInitialDelay();
 175     }
 176 
 177     /**
 178      * Specifies the dismissal delay value.
 179      *
 180      * @param milliseconds  the number of milliseconds to delay
 181      *        before taking away the tooltip
 182      * @see #getDismissDelay
 183      */
 184     public void setDismissDelay(int milliseconds) {
 185         insideTimer.setInitialDelay(milliseconds);
 186     }
 187 
 188     /**
 189      * Returns the dismissal delay value.
 190      *
 191      * @return an integer representing the dismissal delay value,
 192      *          in milliseconds
 193      * @see #setDismissDelay
 194      */
 195     public int getDismissDelay() {
 196         return insideTimer.getInitialDelay();
 197     }
 198 
 199     /**
 200      * Used to specify the amount of time before the user has to wait
 201      * <code>initialDelay</code> milliseconds before a tooltip will be
 202      * shown. That is, if the tooltip is hidden, and the user moves into
 203      * a region of the same Component that has a valid tooltip within
 204      * <code>milliseconds</code> milliseconds the tooltip will immediately
 205      * be shown. Otherwise, if the user moves into a region with a valid
 206      * tooltip after <code>milliseconds</code> milliseconds, the user
 207      * will have to wait an additional <code>initialDelay</code>
 208      * milliseconds before the tooltip is shown again.
 209      *
 210      * @param milliseconds time in milliseconds
 211      * @see #getReshowDelay
 212      */
 213     public void setReshowDelay(int milliseconds) {
 214         exitTimer.setInitialDelay(milliseconds);
 215     }
 216 
 217     /**
 218      * Returns the reshow delay property.
 219      *
 220      * @return reshown delay property
 221      * @see #setReshowDelay
 222      */
 223     public int getReshowDelay() {
 224         return exitTimer.getInitialDelay();
 225     }
 226 
 227     // Returns GraphicsConfiguration instance that toFind belongs to or null
 228     // if drawing point is set to a point beyond visible screen area (e.g.
 229     // Point(20000, 20000))
 230     private GraphicsConfiguration getDrawingGC(Point toFind) {
 231         GraphicsEnvironment env = GraphicsEnvironment.getLocalGraphicsEnvironment();
 232         GraphicsDevice devices[] = env.getScreenDevices();
 233         for (GraphicsDevice device : devices) {
 234             GraphicsConfiguration config = device.getDefaultConfiguration();
 235             Rectangle rect = config.getBounds();
 236             if (rect.contains(toFind)) {
 237                 return config;
 238             }
 239         }
 240 
 241         return null;
 242     }
 243 
 244     void showTipWindow() {
 245         if(insideComponent == null || !insideComponent.isShowing())
 246             return;
 247         String mode = UIManager.getString("ToolTipManager.enableToolTipMode");
 248         if ("activeApplication".equals(mode)) {
 249             KeyboardFocusManager kfm =
 250                     KeyboardFocusManager.getCurrentKeyboardFocusManager();
 251             if (kfm.getFocusedWindow() == null) {
 252                 return;
 253             }
 254         }
 255         if (enabled) {
 256             Dimension size;
 257             Point screenLocation = insideComponent.getLocationOnScreen();
 258             Point location;
 259 
 260             Point toFind;
 261             if (preferredLocation != null) {
 262                 toFind = new Point(screenLocation.x + preferredLocation.x,
 263                         screenLocation.y + preferredLocation.y);
 264             } else {
 265                 toFind = mouseEvent.getLocationOnScreen();
 266             }
 267 
 268             GraphicsConfiguration gc = getDrawingGC(toFind);
 269             if (gc == null) {
 270                 toFind = mouseEvent.getLocationOnScreen();
 271                 gc = getDrawingGC(toFind);
 272                 if (gc == null) {
 273                     gc = insideComponent.getGraphicsConfiguration();
 274                 }
 275             }
 276 
 277             Rectangle sBounds = gc.getBounds();
 278             Insets screenInsets = Toolkit.getDefaultToolkit()
 279                                              .getScreenInsets(gc);
 280             // Take into account screen insets, decrease viewport
 281             sBounds.x += screenInsets.left;
 282             sBounds.y += screenInsets.top;
 283             sBounds.width -= (screenInsets.left + screenInsets.right);
 284             sBounds.height -= (screenInsets.top + screenInsets.bottom);
 285         boolean leftToRight
 286                 = SwingUtilities.isLeftToRight(insideComponent);
 287 
 288             // Just to be paranoid
 289             hideTipWindow();
 290 
 291             tip = insideComponent.createToolTip();
 292             tip.setTipText(toolTipText);
 293             size = tip.getPreferredSize();
 294 
 295             if(preferredLocation != null) {
 296                 location = toFind;
 297         if (!leftToRight) {
 298             location.x -= size.width;
 299         }
 300             } else {
 301                 location = new Point(screenLocation.x + mouseEvent.getX(),
 302                         screenLocation.y + mouseEvent.getY() + 20);
 303         if (!leftToRight) {
 304             if(location.x - size.width>=0) {
 305                 location.x -= size.width;
 306             }
 307         }
 308 
 309             }
 310 
 311         // we do not adjust x/y when using awt.Window tips
 312         if (popupRect == null){
 313         popupRect = new Rectangle();
 314         }
 315         popupRect.setBounds(location.x,location.y,
 316                 size.width,size.height);
 317 
 318         // Fit as much of the tooltip on screen as possible
 319             if (location.x < sBounds.x) {
 320                 location.x = sBounds.x;
 321             }
 322             else if (location.x - sBounds.x + size.width > sBounds.width) {
 323                 location.x = sBounds.x + Math.max(0, sBounds.width - size.width)
 324 ;
 325             }
 326             if (location.y < sBounds.y) {
 327                 location.y = sBounds.y;
 328             }
 329             else if (location.y - sBounds.y + size.height > sBounds.height) {
 330                 location.y = sBounds.y + Math.max(0, sBounds.height - size.height);
 331             }
 332 
 333             PopupFactory popupFactory = PopupFactory.getSharedInstance();
 334 
 335             if (lightWeightPopupEnabled) {
 336         int y = getPopupFitHeight(popupRect, insideComponent);
 337         int x = getPopupFitWidth(popupRect,insideComponent);
 338         if (x>0 || y>0) {
 339             popupFactory.setPopupType(PopupFactory.MEDIUM_WEIGHT_POPUP);
 340         } else {
 341             popupFactory.setPopupType(PopupFactory.LIGHT_WEIGHT_POPUP);
 342         }
 343             }
 344             else {
 345                 popupFactory.setPopupType(PopupFactory.MEDIUM_WEIGHT_POPUP);
 346             }
 347         tipWindow = popupFactory.getPopup(insideComponent, tip,
 348                           location.x,
 349                           location.y);
 350             popupFactory.setPopupType(PopupFactory.LIGHT_WEIGHT_POPUP);
 351 
 352         tipWindow.show();
 353 
 354             Window componentWindow = SwingUtilities.windowForComponent(
 355                                                     insideComponent);
 356 
 357             window = SwingUtilities.windowForComponent(tip);
 358             if (window != null && window != componentWindow) {
 359                 window.addMouseListener(this);
 360             }
 361             else {
 362                 window = null;
 363             }
 364 
 365             insideTimer.start();
 366         tipShowing = true;
 367         }
 368     }
 369 
 370     void hideTipWindow() {
 371         if (tipWindow != null) {
 372             if (window != null) {
 373                 window.removeMouseListener(this);
 374                 window = null;
 375             }
 376             tipWindow.hide();
 377             tipWindow = null;
 378             tipShowing = false;
 379             tip = null;
 380             insideTimer.stop();
 381         }
 382     }
 383 
 384     /**
 385      * Returns a shared <code>ToolTipManager</code> instance.
 386      *
 387      * @return a shared <code>ToolTipManager</code> object
 388      */
 389     public static ToolTipManager sharedInstance() {
 390         Object value = SwingUtilities.appContextGet(TOOL_TIP_MANAGER_KEY);
 391         if (value instanceof ToolTipManager) {
 392             return (ToolTipManager) value;
 393         }
 394         ToolTipManager manager = new ToolTipManager();
 395         SwingUtilities.appContextPut(TOOL_TIP_MANAGER_KEY, manager);
 396         return manager;
 397     }
 398 
 399     // add keylistener here to trigger tip for access
 400     /**
 401      * Registers a component for tooltip management.
 402      * <p>
 403      * This will register key bindings to show and hide the tooltip text
 404      * only if <code>component</code> has focus bindings. This is done
 405      * so that components that are not normally focus traversable, such
 406      * as <code>JLabel</code>, are not made focus traversable as a result
 407      * of invoking this method.
 408      *
 409      * @param component  a <code>JComponent</code> object to add
 410      * @see JComponent#isFocusTraversable
 411      */
 412     public void registerComponent(JComponent component) {
 413         component.removeMouseListener(this);
 414         component.addMouseListener(this);
 415         component.removeMouseMotionListener(moveBeforeEnterListener);
 416         component.addMouseMotionListener(moveBeforeEnterListener);
 417         component.removeKeyListener(accessibilityKeyListener);
 418         component.addKeyListener(accessibilityKeyListener);
 419     }
 420 
 421     /**
 422      * Removes a component from tooltip control.
 423      *
 424      * @param component  a <code>JComponent</code> object to remove
 425      */
 426     public void unregisterComponent(JComponent component) {
 427         component.removeMouseListener(this);
 428         component.removeMouseMotionListener(moveBeforeEnterListener);
 429         component.removeKeyListener(accessibilityKeyListener);
 430     }
 431 
 432     // implements java.awt.event.MouseListener
 433     /**
 434      *  Called when the mouse enters the region of a component.
 435      *  This determines whether the tool tip should be shown.
 436      *
 437      *  @param event  the event in question
 438      */
 439     public void mouseEntered(MouseEvent event) {
 440         initiateToolTip(event);
 441     }
 442 
 443     private void initiateToolTip(MouseEvent event) {
 444         if (event.getSource() == window) {
 445             return;
 446         }
 447         JComponent component = (JComponent)event.getSource();
 448         component.removeMouseMotionListener(moveBeforeEnterListener);
 449 
 450         exitTimer.stop();
 451 
 452         Point location = event.getPoint();
 453         // ensure tooltip shows only in proper place
 454         if (location.x < 0 ||
 455             location.x >=component.getWidth() ||
 456             location.y < 0 ||
 457             location.y >= component.getHeight()) {
 458             return;
 459         }
 460 
 461         if (insideComponent != null) {
 462             enterTimer.stop();
 463         }
 464         // A component in an unactive internal frame is sent two
 465         // mouseEntered events, make sure we don't end up adding
 466         // ourselves an extra time.
 467         component.removeMouseMotionListener(this);
 468         component.addMouseMotionListener(this);
 469 
 470         boolean sameComponent = (insideComponent == component);
 471 
 472         insideComponent = component;
 473     if (tipWindow != null){
 474             mouseEvent = event;
 475             if (showImmediately) {
 476                 String newToolTipText = component.getToolTipText(event);
 477                 Point newPreferredLocation = component.getToolTipLocation(
 478                                                          event);
 479                 boolean sameLoc = (preferredLocation != null) ?
 480                             preferredLocation.equals(newPreferredLocation) :
 481                             (newPreferredLocation == null);
 482 
 483                 if (!sameComponent || !Objects.equals(toolTipText, newToolTipText)
 484                         || !sameLoc) {
 485                     toolTipText = newToolTipText;
 486                     preferredLocation = newPreferredLocation;
 487                     showTipWindow();
 488                 }
 489             } else {
 490                 enterTimer.start();
 491             }
 492         }
 493     }
 494 
 495     // implements java.awt.event.MouseListener
 496     /**
 497      *  Called when the mouse exits the region of a component.
 498      *  Any tool tip showing should be hidden.
 499      *
 500      *  @param event  the event in question
 501      */
 502     public void mouseExited(MouseEvent event) {
 503         boolean shouldHide = true;
 504         if (insideComponent == null) {
 505             // Drag exit
 506         }
 507         if (window != null && event.getSource() == window && insideComponent != null) {
 508           // if we get an exit and have a heavy window
 509           // we need to check if it if overlapping the inside component
 510             Container insideComponentWindow = insideComponent.getTopLevelAncestor();
 511             // insideComponent may be removed after tooltip is made visible
 512             if (insideComponentWindow != null) {
 513                 Point location = event.getPoint();
 514                 SwingUtilities.convertPointToScreen(location, window);
 515 
 516                 location.x -= insideComponentWindow.getX();
 517                 location.y -= insideComponentWindow.getY();
 518 
 519                 location = SwingUtilities.convertPoint(null, location, insideComponent);
 520                 if (location.x >= 0 && location.x < insideComponent.getWidth() &&
 521                         location.y >= 0 && location.y < insideComponent.getHeight()) {
 522                     shouldHide = false;
 523                 } else {
 524                     shouldHide = true;
 525                 }
 526             }
 527         } else if(event.getSource() == insideComponent && tipWindow != null) {
 528             Window win = SwingUtilities.getWindowAncestor(insideComponent);
 529             if (win != null) {  // insideComponent may have been hidden (e.g. in a menu)
 530                 Point location = SwingUtilities.convertPoint(insideComponent,
 531                                                              event.getPoint(),
 532                                                              win);
 533                 Rectangle bounds = insideComponent.getTopLevelAncestor().getBounds();
 534                 location.x += bounds.x;
 535                 location.y += bounds.y;
 536 
 537                 Point loc = new Point(0, 0);
 538                 SwingUtilities.convertPointToScreen(loc, tip);
 539                 bounds.x = loc.x;
 540                 bounds.y = loc.y;
 541                 bounds.width = tip.getWidth();
 542                 bounds.height = tip.getHeight();
 543 
 544                 if (location.x >= bounds.x && location.x < (bounds.x + bounds.width) &&
 545                     location.y >= bounds.y && location.y < (bounds.y + bounds.height)) {
 546                     shouldHide = false;
 547                 } else {
 548                     shouldHide = true;
 549                 }
 550             }
 551         }
 552 
 553         if (shouldHide) {
 554             enterTimer.stop();
 555         if (insideComponent != null) {
 556                 insideComponent.removeMouseMotionListener(this);
 557             }
 558             insideComponent = null;
 559             toolTipText = null;
 560             mouseEvent = null;
 561             hideTipWindow();
 562             exitTimer.restart();
 563         }
 564     }
 565 
 566     // implements java.awt.event.MouseListener
 567     /**
 568      *  Called when the mouse is pressed.
 569      *  Any tool tip showing should be hidden.
 570      *
 571      *  @param event  the event in question
 572      */
 573     public void mousePressed(MouseEvent event) {
 574         hideTipWindow();
 575         enterTimer.stop();
 576         showImmediately = false;
 577         insideComponent = null;
 578         mouseEvent = null;
 579     }
 580 
 581     // implements java.awt.event.MouseMotionListener
 582     /**
 583      *  Called when the mouse is pressed and dragged.
 584      *  Does nothing.
 585      *
 586      *  @param event  the event in question
 587      */
 588     public void mouseDragged(MouseEvent event) {
 589     }
 590 
 591     // implements java.awt.event.MouseMotionListener
 592     /**
 593      *  Called when the mouse is moved.
 594      *  Determines whether the tool tip should be displayed.
 595      *
 596      *  @param event  the event in question
 597      */
 598     public void mouseMoved(MouseEvent event) {
 599         if (tipShowing) {
 600             checkForTipChange(event);
 601         }
 602         else if (showImmediately) {
 603             JComponent component = (JComponent)event.getSource();
 604             toolTipText = component.getToolTipText(event);
 605             if (toolTipText != null) {
 606                 preferredLocation = component.getToolTipLocation(event);
 607                 mouseEvent = event;
 608                 insideComponent = component;
 609                 exitTimer.stop();
 610                 showTipWindow();
 611             }
 612         }
 613         else {
 614             // Lazily lookup the values from within insideTimerAction
 615             insideComponent = (JComponent)event.getSource();
 616             mouseEvent = event;
 617             toolTipText = null;
 618             enterTimer.restart();
 619         }
 620     }
 621 
 622     /**
 623      * Checks to see if the tooltip needs to be changed in response to
 624      * the MouseMoved event <code>event</code>.
 625      */
 626     private void checkForTipChange(MouseEvent event) {
 627         JComponent component = (JComponent)event.getSource();
 628         String newText = component.getToolTipText(event);
 629         Point  newPreferredLocation = component.getToolTipLocation(event);
 630 
 631         if (newText != null || newPreferredLocation != null) {
 632             mouseEvent = event;
 633             if (((newText != null && newText.equals(toolTipText)) || newText == null) &&
 634                 ((newPreferredLocation != null && newPreferredLocation.equals(preferredLocation))
 635                  || newPreferredLocation == null)) {
 636                 if (tipWindow != null) {
 637                     insideTimer.restart();
 638                 } else {
 639                     enterTimer.restart();
 640                 }
 641             } else {
 642                 toolTipText = newText;
 643                 preferredLocation = newPreferredLocation;
 644                 if (showImmediately) {
 645                     hideTipWindow();
 646                     showTipWindow();
 647                     exitTimer.stop();
 648                 } else {
 649                     enterTimer.restart();
 650                 }
 651             }
 652         } else {
 653             toolTipText = null;
 654             preferredLocation = null;
 655             mouseEvent = null;
 656             insideComponent = null;
 657             hideTipWindow();
 658             enterTimer.stop();
 659             exitTimer.restart();
 660         }
 661     }
 662 
 663     /**
 664      * Inside timer action.
 665      */
 666     protected class insideTimerAction implements ActionListener {
 667         /**
 668          * {@inheritDoc}
 669          */
 670         public void actionPerformed(ActionEvent e) {
 671             if(insideComponent != null && insideComponent.isShowing()) {
 672                 // Lazy lookup
 673                 if (toolTipText == null && mouseEvent != null) {
 674                     toolTipText = insideComponent.getToolTipText(mouseEvent);
 675                     preferredLocation = insideComponent.getToolTipLocation(
 676                                               mouseEvent);
 677                 }
 678                 if(toolTipText != null) {
 679                     showImmediately = true;
 680                     showTipWindow();
 681                 }
 682                 else {
 683                     insideComponent = null;
 684                     toolTipText = null;
 685                     preferredLocation = null;
 686                     mouseEvent = null;
 687                     hideTipWindow();
 688                 }
 689             }
 690         }
 691     }
 692 
 693     /**
 694      * Outside timer action.
 695      */
 696     protected class outsideTimerAction implements ActionListener {
 697         /**
 698          * {@inheritDoc}
 699          */
 700         public void actionPerformed(ActionEvent e) {
 701             showImmediately = false;
 702         }
 703     }
 704 
 705     /**
 706      * Still inside timer action.
 707      */
 708     protected class stillInsideTimerAction implements ActionListener {
 709         /**
 710          * {@inheritDoc}
 711          */
 712         public void actionPerformed(ActionEvent e) {
 713             hideTipWindow();
 714             enterTimer.stop();
 715             showImmediately = false;
 716             insideComponent = null;
 717             mouseEvent = null;
 718         }
 719     }
 720 
 721   /* This listener is registered when the tooltip is first registered
 722    * on a component in order to catch the situation where the tooltip
 723    * was turned on while the mouse was already within the bounds of
 724    * the component.  This way, the tooltip will be initiated on a
 725    * mouse-entered or mouse-moved, whichever occurs first.  Once the
 726    * tooltip has been initiated, we can remove this listener and rely
 727    * solely on mouse-entered to initiate the tooltip.
 728    */
 729     private class MoveBeforeEnterListener extends MouseMotionAdapter {
 730         public void mouseMoved(MouseEvent e) {
 731             initiateToolTip(e);
 732         }
 733     }
 734 
 735     static Frame frameForComponent(Component component) {
 736         while (!(component instanceof Frame)) {
 737             component = component.getParent();
 738         }
 739         return (Frame)component;
 740     }
 741 
 742   private FocusListener createFocusChangeListener(){
 743     return new FocusAdapter(){
 744       public void focusLost(FocusEvent evt){
 745         hideTipWindow();
 746         insideComponent = null;
 747         JComponent c = (JComponent)evt.getSource();
 748         c.removeFocusListener(focusChangeListener);
 749       }
 750     };
 751   }
 752 
 753   // Returns: 0 no adjust
 754   //         -1 can't fit
 755   //         >0 adjust value by amount returned
 756  @SuppressWarnings("deprecation")
 757   private int getPopupFitWidth(Rectangle popupRectInScreen, Component invoker){
 758     if (invoker != null){
 759       Container parent;
 760       for (parent = invoker.getParent(); parent != null; parent = parent.getParent()){
 761         // fix internal frame size bug: 4139087 - 4159012
 762         if(parent instanceof JFrame || parent instanceof JDialog ||
 763            parent instanceof JWindow) { // no check for awt.Frame since we use Heavy tips
 764           return getWidthAdjust(parent.getBounds(),popupRectInScreen);
 765         } else if (parent instanceof JApplet || parent instanceof JInternalFrame) {
 766           if (popupFrameRect == null){
 767             popupFrameRect = new Rectangle();
 768           }
 769           Point p = parent.getLocationOnScreen();
 770           popupFrameRect.setBounds(p.x,p.y,
 771                                    parent.getBounds().width,
 772                                    parent.getBounds().height);
 773           return getWidthAdjust(popupFrameRect,popupRectInScreen);
 774         }
 775       }
 776     }
 777     return 0;
 778   }
 779 
 780   // Returns:  0 no adjust
 781   //          >0 adjust by value return
 782   @SuppressWarnings("deprecation")
 783   private int getPopupFitHeight(Rectangle popupRectInScreen, Component invoker){
 784     if (invoker != null){
 785       Container parent;
 786       for (parent = invoker.getParent(); parent != null; parent = parent.getParent()){
 787         if(parent instanceof JFrame || parent instanceof JDialog ||
 788            parent instanceof JWindow) {
 789           return getHeightAdjust(parent.getBounds(),popupRectInScreen);
 790         } else if (parent instanceof JApplet || parent instanceof JInternalFrame) {
 791           if (popupFrameRect == null){
 792             popupFrameRect = new Rectangle();
 793           }
 794           Point p = parent.getLocationOnScreen();
 795           popupFrameRect.setBounds(p.x,p.y,
 796                                    parent.getBounds().width,
 797                                    parent.getBounds().height);
 798           return getHeightAdjust(popupFrameRect,popupRectInScreen);
 799         }
 800       }
 801     }
 802     return 0;
 803   }
 804 
 805   private int getHeightAdjust(Rectangle a, Rectangle b){
 806     if (b.y >= a.y && (b.y + b.height) <= (a.y + a.height))
 807       return 0;
 808     else
 809       return (((b.y + b.height) - (a.y + a.height)) + 5);
 810   }
 811 
 812   // Return the number of pixels over the edge we are extending.
 813   // If we are over the edge the ToolTipManager can adjust.
 814   // REMIND: what if the Tooltip is just too big to fit at all - we currently will just clip
 815   private int getWidthAdjust(Rectangle a, Rectangle b){
 816     //    System.out.println("width b.x/b.width: " + b.x + "/" + b.width +
 817     //                 "a.x/a.width: " + a.x + "/" + a.width);
 818     if (b.x >= a.x && (b.x + b.width) <= (a.x + a.width)){
 819       return 0;
 820     }
 821     else {
 822       return (((b.x + b.width) - (a.x +a.width)) + 5);
 823     }
 824   }
 825 
 826 
 827     //
 828     // Actions
 829     //
 830     private void show(JComponent source) {
 831         if (tipWindow != null) { // showing we unshow
 832             hideTipWindow();
 833             insideComponent = null;
 834         }
 835         else {
 836             hideTipWindow(); // be safe
 837             enterTimer.stop();
 838             exitTimer.stop();
 839             insideTimer.stop();
 840             insideComponent = source;
 841             if (insideComponent != null){
 842                 toolTipText = insideComponent.getToolTipText();
 843                 preferredLocation = new Point(10,insideComponent.getHeight()+
 844                                               10);  // manual set
 845                 showTipWindow();
 846                 // put a focuschange listener on to bring the tip down
 847                 if (focusChangeListener == null){
 848                     focusChangeListener = createFocusChangeListener();
 849                 }
 850                 insideComponent.addFocusListener(focusChangeListener);
 851             }
 852         }
 853     }
 854 
 855     private void hide(JComponent source) {
 856         hideTipWindow();
 857         source.removeFocusListener(focusChangeListener);
 858         preferredLocation = null;
 859         insideComponent = null;
 860     }
 861 
 862     /* This listener is registered when the tooltip is first registered
 863      * on a component in order to process accessibility keybindings.
 864      * This will apply globally across L&F
 865      *
 866      * Post Tip: Ctrl+F1
 867      * Unpost Tip: Esc and Ctrl+F1
 868      */
 869     private class AccessibilityKeyListener extends KeyAdapter {
 870         public void keyPressed(KeyEvent e) {
 871             if (!e.isConsumed()) {
 872                 JComponent source = (JComponent) e.getComponent();
 873                 KeyStroke keyStrokeForEvent = KeyStroke.getKeyStrokeForEvent(e);
 874                 if (hideTip.equals(keyStrokeForEvent)) {
 875                     if (tipWindow != null) {
 876                         hide(source);
 877                         e.consume();
 878                     }
 879                 } else if (postTip.equals(keyStrokeForEvent)) {
 880                     // Shown tooltip will be hidden
 881                     ToolTipManager.this.show(source);
 882                     e.consume();
 883                 }
 884             }
 885         }
 886     }
 887 }