001    /* ToolTipManager.java --
002       Copyright (C) 2002, 2004, 2006, Free Software Foundation, Inc.
003    
004    This file is part of GNU Classpath.
005    
006    GNU Classpath is free software; you can redistribute it and/or modify
007    it under the terms of the GNU General Public License as published by
008    the Free Software Foundation; either version 2, or (at your option)
009    any later version.
010    
011    GNU Classpath is distributed in the hope that it will be useful, but
012    WITHOUT ANY WARRANTY; without even the implied warranty of
013    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
014    General Public License for more details.
015    
016    You should have received a copy of the GNU General Public License
017    along with GNU Classpath; see the file COPYING.  If not, write to the
018    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
019    02110-1301 USA.
020    
021    Linking this library statically or dynamically with other modules is
022    making a combined work based on this library.  Thus, the terms and
023    conditions of the GNU General Public License cover the whole
024    combination.
025    
026    As a special exception, the copyright holders of this library give you
027    permission to link this library with independent modules to produce an
028    executable, regardless of the license terms of these independent
029    modules, and to copy and distribute the resulting executable under
030    terms of your choice, provided that you also meet, for each linked
031    independent module, the terms and conditions of the license of that
032    module.  An independent module is a module which is not derived from
033    or based on this library.  If you modify this library, you may extend
034    this exception to your version of the library, but you are not
035    obligated to do so.  If you do not wish to do so, delete this
036    exception statement from your version. */
037    
038    package javax.swing;
039    
040    import java.awt.Component;
041    import java.awt.Container;
042    import java.awt.Dimension;
043    import java.awt.Point;
044    import java.awt.event.ActionEvent;
045    import java.awt.event.ActionListener;
046    import java.awt.event.MouseAdapter;
047    import java.awt.event.MouseEvent;
048    import java.awt.event.MouseMotionListener;
049    
050    /**
051     * This class is responsible for the registration of JToolTips to Components
052     * and for displaying them when appropriate.
053     */
054    public class ToolTipManager extends MouseAdapter implements MouseMotionListener
055    {
056      /**
057       * This ActionListener is associated with the Timer that listens to whether
058       * the JToolTip can be hidden after four seconds.
059       */
060      protected class stillInsideTimerAction implements ActionListener
061      {
062        /**
063         * This method creates a new stillInsideTimerAction object.
064         */
065        protected stillInsideTimerAction()
066        {
067          // Nothing to do here.
068        }
069    
070        /**
071         * This method hides the JToolTip when the Timer has finished.
072         *
073         * @param event The ActionEvent.
074         */
075        public void actionPerformed(ActionEvent event)
076        {
077          hideTip();
078        }
079      }
080    
081      /**
082       * This Actionlistener is associated with the Timer that listens to whether
083       * the mouse cursor has re-entered the JComponent in time for an immediate
084       * redisplay of the JToolTip.
085       */
086      protected class outsideTimerAction implements ActionListener
087      {
088        /**
089         * This method creates a new outsideTimerAction object.
090         */
091        protected outsideTimerAction()
092        {
093          // Nothing to do here.
094        }
095    
096        /**
097         * This method is called when the Timer that listens to whether the mouse
098         * cursor has re-entered the JComponent has run out.
099         *
100         * @param event The ActionEvent.
101         */
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         */
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         */
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       */
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       */
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       */
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       */
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       */
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       */
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       */
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       */
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       */
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       */
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       */
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       */
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       */
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       */
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       */
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       */
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       */
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       */
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       */
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       */
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       */
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       */
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       */
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       */
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    }