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