001/* ToolTipManager.java --
002   Copyright (C) 2002, 2004, 2006, Free Software Foundation, Inc.
003
004This file is part of GNU Classpath.
005
006GNU Classpath is free software; you can redistribute it and/or modify
007it under the terms of the GNU General Public License as published by
008the Free Software Foundation; either version 2, or (at your option)
009any later version.
010
011GNU Classpath is distributed in the hope that it will be useful, but
012WITHOUT ANY WARRANTY; without even the implied warranty of
013MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
014General Public License for more details.
015
016You should have received a copy of the GNU General Public License
017along with GNU Classpath; see the file COPYING.  If not, write to the
018Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
01902110-1301 USA.
020
021Linking this library statically or dynamically with other modules is
022making a combined work based on this library.  Thus, the terms and
023conditions of the GNU General Public License cover the whole
024combination.
025
026As a special exception, the copyright holders of this library give you
027permission to link this library with independent modules to produce an
028executable, regardless of the license terms of these independent
029modules, and to copy and distribute the resulting executable under
030terms of your choice, provided that you also meet, for each linked
031independent module, the terms and conditions of the license of that
032module.  An independent module is a module which is not derived from
033or based on this library.  If you modify this library, you may extend
034this exception to your version of the library, but you are not
035obligated to do so.  If you do not wish to do so, delete this
036exception statement from your version. */
037
038package javax.swing;
039
040import java.awt.Component;
041import java.awt.Container;
042import java.awt.Dimension;
043import java.awt.Point;
044import java.awt.event.ActionEvent;
045import java.awt.event.ActionListener;
046import java.awt.event.MouseAdapter;
047import java.awt.event.MouseEvent;
048import 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 */
054public 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}