1 /* ToolTipManager.java --
2    Copyright (C) 2002, 2004, 2006, Free Software Foundation, Inc.
3 
4 This file is part of GNU Classpath.
5 
6 GNU Classpath is free software; you can redistribute it and/or modify
7 it under the terms of the GNU General Public License as published by
8 the Free Software Foundation; either version 2, or (at your option)
9 any later version.
10 
11 GNU Classpath is distributed in the hope that it will be useful, but
12 WITHOUT ANY WARRANTY; without even the implied warranty of
13 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
14 General Public License for more details.
15 
16 You should have received a copy of the GNU General Public License
17 along with GNU Classpath; see the file COPYING.  If not, write to the
18 Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
19 02110-1301 USA.
20 
21 Linking this library statically or dynamically with other modules is
22 making a combined work based on this library.  Thus, the terms and
23 conditions of the GNU General Public License cover the whole
24 combination.
25 
26 As a special exception, the copyright holders of this library give you
27 permission to link this library with independent modules to produce an
28 executable, regardless of the license terms of these independent
29 modules, and to copy and distribute the resulting executable under
30 terms of your choice, provided that you also meet, for each linked
31 independent module, the terms and conditions of the license of that
32 module.  An independent module is a module which is not derived from
33 or based on this library.  If you modify this library, you may extend
34 this exception to your version of the library, but you are not
35 obligated to do so.  If you do not wish to do so, delete this
36 exception statement from your version. */
37 
38 package javax.swing;
39 
40 import java.awt.Component;
41 import java.awt.Container;
42 import java.awt.Dimension;
43 import java.awt.Point;
44 import java.awt.event.ActionEvent;
45 import java.awt.event.ActionListener;
46 import java.awt.event.MouseAdapter;
47 import java.awt.event.MouseEvent;
48 import java.awt.event.MouseMotionListener;
49 
50 /**
51  * This class is responsible for the registration of JToolTips to Components
52  * and for displaying them when appropriate.
53  */
54 public class ToolTipManager extends MouseAdapter implements MouseMotionListener
55 {
56   /**
57    * This ActionListener is associated with the Timer that listens to whether
58    * the JToolTip can be hidden after four seconds.
59    */
60   protected class stillInsideTimerAction implements ActionListener
61   {
62     /**
63      * This method creates a new stillInsideTimerAction object.
64      */
stillInsideTimerAction()65     protected stillInsideTimerAction()
66     {
67       // Nothing to do here.
68     }
69 
70     /**
71      * This method hides the JToolTip when the Timer has finished.
72      *
73      * @param event The ActionEvent.
74      */
actionPerformed(ActionEvent event)75     public void actionPerformed(ActionEvent event)
76     {
77       hideTip();
78     }
79   }
80 
81   /**
82    * This Actionlistener is associated with the Timer that listens to whether
83    * the mouse cursor has re-entered the JComponent in time for an immediate
84    * redisplay of the JToolTip.
85    */
86   protected class outsideTimerAction implements ActionListener
87   {
88     /**
89      * This method creates a new outsideTimerAction object.
90      */
outsideTimerAction()91     protected outsideTimerAction()
92     {
93       // Nothing to do here.
94     }
95 
96     /**
97      * This method is called when the Timer that listens to whether the mouse
98      * cursor has re-entered the JComponent has run out.
99      *
100      * @param event The ActionEvent.
101      */
actionPerformed(ActionEvent event)102     public void actionPerformed(ActionEvent event)
103     {
104       // TODO: What should be done here, if anything?
105     }
106   }
107 
108   /**
109    * This ActionListener is associated with the Timer that listens to whether
110    * it is time for the JToolTip to be displayed after the mouse has entered
111    * the JComponent.
112    */
113   protected class insideTimerAction implements ActionListener
114   {
115     /**
116      * This method creates a new insideTimerAction object.
117      */
insideTimerAction()118     protected insideTimerAction()
119     {
120       // Nothing to do here.
121     }
122 
123     /**
124      * This method displays the JToolTip when the Mouse has been still for the
125      * delay.
126      *
127      * @param event The ActionEvent.
128      */
actionPerformed(ActionEvent event)129     public void actionPerformed(ActionEvent event)
130     {
131       showTip();
132     }
133   }
134 
135   /**
136    * The Timer that determines whether the Mouse has been still long enough
137    * for the JToolTip to be displayed.
138    */
139   Timer enterTimer;
140 
141   /**
142    * The Timer that determines whether the Mouse has re-entered the JComponent
143    * quickly enough for the JToolTip to be displayed immediately.
144    */
145   Timer exitTimer;
146 
147   /**
148    * The Timer that determines whether the JToolTip has been displayed long
149    * enough for it to be hidden.
150    */
151   Timer insideTimer;
152 
153   /** A global enabled setting for the ToolTipManager. */
154   private transient boolean enabled = true;
155 
156   /** lightWeightPopupEnabled */
157   protected boolean lightWeightPopupEnabled = true;
158 
159   /** heavyWeightPopupEnabled */
160   protected boolean heavyWeightPopupEnabled = false;
161 
162   /** The shared instance of the ToolTipManager. */
163   private static ToolTipManager shared;
164 
165   /** The current component the tooltip is being displayed for. */
166   private JComponent currentComponent;
167 
168   /** The current tooltip. */
169   private JToolTip currentTip;
170 
171   /**
172    * The tooltip text.
173    */
174   private String toolTipText;
175 
176   /** The last known position of the mouse cursor. */
177   private Point currentPoint;
178 
179   /**  */
180   private Popup popup;
181 
182   /**
183    * Creates a new ToolTipManager and sets up the timers.
184    */
ToolTipManager()185   ToolTipManager()
186   {
187     enterTimer = new Timer(750, new insideTimerAction());
188     enterTimer.setRepeats(false);
189 
190     insideTimer = new Timer(4000, new stillInsideTimerAction());
191     insideTimer.setRepeats(false);
192 
193     exitTimer = new Timer(500, new outsideTimerAction());
194     exitTimer.setRepeats(false);
195   }
196 
197   /**
198    * This method returns the shared instance of ToolTipManager used by all
199    * JComponents.
200    *
201    * @return The shared instance of ToolTipManager.
202    */
sharedInstance()203   public static ToolTipManager sharedInstance()
204   {
205     if (shared == null)
206       shared = new ToolTipManager();
207 
208     return shared;
209   }
210 
211   /**
212    * This method sets whether ToolTips are enabled or disabled for all
213    * JComponents.
214    *
215    * @param enabled Whether ToolTips are enabled or disabled for all
216    *        JComponents.
217    */
setEnabled(boolean enabled)218   public void setEnabled(boolean enabled)
219   {
220     if (! enabled)
221       {
222         enterTimer.stop();
223         exitTimer.stop();
224         insideTimer.stop();
225       }
226 
227     this.enabled = enabled;
228   }
229 
230   /**
231    * This method returns whether ToolTips are enabled.
232    *
233    * @return Whether ToolTips are enabled.
234    */
isEnabled()235   public boolean isEnabled()
236   {
237     return enabled;
238   }
239 
240   /**
241    * This method returns whether LightweightToolTips are enabled.
242    *
243    * @return Whether LighweightToolTips are enabled.
244    */
isLightWeightPopupEnabled()245   public boolean isLightWeightPopupEnabled()
246   {
247     return lightWeightPopupEnabled;
248   }
249 
250   /**
251    * This method sets whether LightweightToolTips are enabled. If you mix
252    * Lightweight and Heavyweight components, you must set this to false to
253    * ensure that the ToolTips popup above all other components.
254    *
255    * @param enabled Whether LightweightToolTips will be enabled.
256    */
setLightWeightPopupEnabled(boolean enabled)257   public void setLightWeightPopupEnabled(boolean enabled)
258   {
259     lightWeightPopupEnabled = enabled;
260     heavyWeightPopupEnabled = ! enabled;
261   }
262 
263   /**
264    * This method returns the initial delay before the ToolTip is shown when
265    * the mouse enters a Component.
266    *
267    * @return The initial delay before the ToolTip is shown.
268    */
getInitialDelay()269   public int getInitialDelay()
270   {
271     return enterTimer.getDelay();
272   }
273 
274   /**
275    * Sets the initial delay before the ToolTip is shown when the
276    * mouse enters a Component.
277    *
278    * @param delay The initial delay before the ToolTip is shown.
279    *
280    * @throws IllegalArgumentException if <code>delay</code> is less than zero.
281    */
setInitialDelay(int delay)282   public void setInitialDelay(int delay)
283   {
284     enterTimer.setDelay(delay);
285   }
286 
287   /**
288    * This method returns the time the ToolTip will be shown before being
289    * hidden.
290    *
291    * @return The time the ToolTip will be shown before being hidden.
292    */
getDismissDelay()293   public int getDismissDelay()
294   {
295     return insideTimer.getDelay();
296   }
297 
298   /**
299    * Sets the time the ToolTip will be shown before being hidden.
300    *
301    * @param delay  the delay (in milliseconds) before tool tips are hidden.
302    *
303    * @throws IllegalArgumentException if <code>delay</code> is less than zero.
304    */
setDismissDelay(int delay)305   public void setDismissDelay(int delay)
306   {
307     insideTimer.setDelay(delay);
308   }
309 
310   /**
311    * This method returns the amount of delay where if the mouse re-enters a
312    * Component, the tooltip will be shown immediately.
313    *
314    * @return The reshow delay.
315    */
getReshowDelay()316   public int getReshowDelay()
317   {
318     return exitTimer.getDelay();
319   }
320 
321   /**
322    * Sets the amount of delay where if the mouse re-enters a
323    * Component, the tooltip will be shown immediately.
324    *
325    * @param delay The reshow delay (in milliseconds).
326    *
327    * @throws IllegalArgumentException if <code>delay</code> is less than zero.
328    */
setReshowDelay(int delay)329   public void setReshowDelay(int delay)
330   {
331     exitTimer.setDelay(delay);
332   }
333 
334   /**
335    * This method registers a JComponent with the ToolTipManager.
336    *
337    * @param component The JComponent to register with the ToolTipManager.
338    */
registerComponent(JComponent component)339   public void registerComponent(JComponent component)
340   {
341     component.addMouseListener(this);
342     component.addMouseMotionListener(this);
343   }
344 
345   /**
346    * This method unregisters a JComponent with the ToolTipManager.
347    *
348    * @param component The JComponent to unregister with the ToolTipManager.
349    */
unregisterComponent(JComponent component)350   public void unregisterComponent(JComponent component)
351   {
352     component.removeMouseMotionListener(this);
353     component.removeMouseListener(this);
354   }
355 
356   /**
357    * This method is called whenever the mouse enters a JComponent registered
358    * with the ToolTipManager. When the mouse enters within the period of time
359    * specified by the reshow delay, the tooltip will be displayed
360    * immediately. Otherwise, it must wait for the initial delay before
361    * displaying the tooltip.
362    *
363    * @param event The MouseEvent.
364    */
mouseEntered(MouseEvent event)365   public void mouseEntered(MouseEvent event)
366   {
367     if (currentComponent != null
368         && getContentPaneDeepestComponent(event) == currentComponent)
369       return;
370     currentPoint = event.getPoint();
371 
372     currentComponent = (JComponent) event.getSource();
373     toolTipText = currentComponent.getToolTipText(event);
374     if (exitTimer.isRunning())
375       {
376         exitTimer.stop();
377         showTip();
378         return;
379       }
380     // This should always be stopped unless we have just fake-exited.
381     if (!enterTimer.isRunning())
382       enterTimer.start();
383   }
384 
385   /**
386    * This method is called when the mouse exits a JComponent registered with the
387    * ToolTipManager. When the mouse exits, the tooltip should be hidden
388    * immediately.
389    *
390    * @param event
391    *          The MouseEvent.
392    */
mouseExited(MouseEvent event)393   public void mouseExited(MouseEvent event)
394   {
395     if (getContentPaneDeepestComponent(event) == currentComponent)
396       return;
397 
398     currentPoint = event.getPoint();
399     currentComponent = null;
400     hideTip();
401 
402     if (! enterTimer.isRunning())
403       exitTimer.start();
404     if (enterTimer.isRunning())
405       enterTimer.stop();
406     if (insideTimer.isRunning())
407       insideTimer.stop();
408   }
409 
410   /**
411    * This method is called when the mouse is pressed on a JComponent
412    * registered with the ToolTipManager. When the mouse is pressed, the
413    * tooltip (if it is shown) must be hidden immediately.
414    *
415    * @param event The MouseEvent.
416    */
mousePressed(MouseEvent event)417   public void mousePressed(MouseEvent event)
418   {
419     currentPoint = event.getPoint();
420     if (enterTimer.isRunning())
421       enterTimer.restart();
422     else if (insideTimer.isRunning())
423       {
424         insideTimer.stop();
425         hideTip();
426       }
427   }
428 
429   /**
430    * This method is called when the mouse is dragged in a JComponent
431    * registered with the ToolTipManager.
432    *
433    * @param event The MouseEvent.
434    */
mouseDragged(MouseEvent event)435   public void mouseDragged(MouseEvent event)
436   {
437     currentPoint = event.getPoint();
438     if (enterTimer.isRunning())
439       enterTimer.restart();
440   }
441 
442   /**
443    * This method is called when the mouse is moved in a JComponent registered
444    * with the ToolTipManager.
445    *
446    * @param event The MouseEvent.
447    */
mouseMoved(MouseEvent event)448   public void mouseMoved(MouseEvent event)
449   {
450     currentPoint = event.getPoint();
451     if (currentTip != null && currentTip.isShowing())
452       checkTipUpdate(event);
453     else
454       {
455         if (enterTimer.isRunning())
456           enterTimer.restart();
457       }
458   }
459 
460   /**
461    * Checks if the tooltip's text or location changes when the mouse is moved
462    * over the component.
463    */
checkTipUpdate(MouseEvent ev)464   private void checkTipUpdate(MouseEvent ev)
465   {
466     JComponent comp = (JComponent) ev.getSource();
467     String newText = comp.getToolTipText(ev);
468     String oldText = toolTipText;
469     if (newText != null)
470       {
471         if (((newText != null && newText.equals(oldText)) || newText == null))
472           {
473             // No change at all. Restart timers.
474             if (popup == null)
475               enterTimer.restart();
476             else
477               insideTimer.restart();
478           }
479         else
480           {
481             // Update the tooltip.
482             toolTipText = newText;
483             hideTip();
484             showTip();
485             exitTimer.stop();
486           }
487       }
488     else
489       {
490         // Hide tooltip.
491         currentTip = null;
492         currentPoint = null;
493         hideTip();
494         enterTimer.stop();
495         exitTimer.stop();
496       }
497   }
498 
499   /**
500    * This method displays the ToolTip. It can figure out the method needed to
501    * show it as well (whether to display it in heavyweight/lightweight panel
502    * or a window.)  This is package-private to avoid an accessor method.
503    */
showTip()504   void showTip()
505   {
506     if (!enabled || currentComponent == null || !currentComponent.isEnabled()
507         || !currentComponent.isShowing())
508       {
509         popup = null;
510         return;
511       }
512 
513     if (currentTip == null || currentTip.getComponent() != currentComponent)
514       currentTip = currentComponent.createToolTip();
515     currentTip.setTipText(toolTipText);
516 
517     Point p = currentPoint;
518     Point cP = currentComponent.getLocationOnScreen();
519     Dimension dims = currentTip.getPreferredSize();
520 
521     JLayeredPane pane = null;
522     JRootPane r = ((JRootPane) SwingUtilities.getAncestorOfClass(JRootPane.class,
523                                                                  currentComponent));
524     if (r != null)
525       pane = r.getLayeredPane();
526     if (pane == null)
527       return;
528 
529     p.translate(cP.x, cP.y);
530     adjustLocation(p, pane, dims);
531 
532     currentTip.setBounds(0, 0, dims.width, dims.height);
533 
534     PopupFactory factory = PopupFactory.getSharedInstance();
535     popup = factory.getPopup(currentComponent, currentTip, p.x, p.y);
536     popup.show();
537   }
538 
539   /**
540    * Adjusts the point to a new location on the component,
541    * using the currentTip's dimensions.
542    *
543    * @param p - the point to convert.
544    * @param c - the component the point is on.
545    * @param d - the dimensions of the currentTip.
546    */
adjustLocation(Point p, Component c, Dimension d)547   private Point adjustLocation(Point p, Component c, Dimension d)
548   {
549     if (p.x + d.width > c.getWidth())
550       p.x -= d.width;
551     if (p.x < 0)
552       p.x = 0;
553     if (p.y + d.height < c.getHeight())
554       p.y += d.height;
555     if (p.y + d.height > c.getHeight())
556       p.y -= d.height;
557 
558     return p;
559   }
560 
561   /**
562    * This method hides the ToolTip.
563    * This is package-private to avoid an accessor method.
564    */
hideTip()565   void hideTip()
566   {
567     if (popup != null)
568       popup.hide();
569   }
570 
571   /**
572    * This method returns the deepest component in the content pane for the
573    * first RootPaneContainer up from the currentComponent. This method is
574    * used in conjunction with one of the mouseXXX methods.
575    *
576    * @param e The MouseEvent.
577    *
578    * @return The deepest component in the content pane.
579    */
getContentPaneDeepestComponent(MouseEvent e)580   private Component getContentPaneDeepestComponent(MouseEvent e)
581   {
582     Component source = (Component) e.getSource();
583     Container parent = SwingUtilities.getAncestorOfClass(JRootPane.class,
584                                                          currentComponent);
585     if (parent == null)
586       return null;
587     parent = ((JRootPane) parent).getContentPane();
588     Point p = e.getPoint();
589     p = SwingUtilities.convertPoint(source, p, parent);
590     Component target = SwingUtilities.getDeepestComponentAt(parent, p.x, p.y);
591     return target;
592   }
593 }
594