1. /*
  2. * @(#)ToolTipManager.java 1.49 00/02/02
  3. *
  4. * Copyright 1997-2000 Sun Microsystems, Inc. All Rights Reserved.
  5. *
  6. * This software is the proprietary information of Sun Microsystems, Inc.
  7. * Use is subject to license terms.
  8. *
  9. */
  10. package javax.swing;
  11. import java.awt.event.*;
  12. import java.applet.*;
  13. import java.awt.*;
  14. import java.io.Serializable;
  15. /**
  16. * Manages all the ToolTips in the system.
  17. *
  18. * @see JComponent#createToolTip
  19. * @version 1.44 06/01/99
  20. * @author Dave Moore
  21. * @author Rich Schiavi
  22. */
  23. public class ToolTipManager extends MouseAdapter implements MouseMotionListener {
  24. Timer enterTimer, exitTimer, insideTimer;
  25. String toolTipText;
  26. Point preferredLocation;
  27. JComponent insideComponent;
  28. MouseEvent mouseEvent;
  29. boolean showImmediately;
  30. final static ToolTipManager sharedInstance = new ToolTipManager();
  31. Popup tipWindow;
  32. JToolTip tip;
  33. private PopupFactory popupFactory = new DefaultPopupFactory();
  34. private Rectangle popupRect = null;
  35. private Rectangle popupFrameRect = null;
  36. boolean enabled = true;
  37. boolean mouseAboveToolTip = false;
  38. private boolean tipShowing = false;
  39. private long timerEnter = 0;
  40. private KeyStroke postTip,hideTip;
  41. private Action postTipAction, hideTipAction;
  42. private FocusListener focusChangeListener = null;
  43. // PENDING(ges)
  44. protected boolean lightWeightPopupEnabled = true;
  45. protected boolean heavyWeightPopupEnabled = false;
  46. ToolTipManager() {
  47. enterTimer = new Timer(750, new insideTimerAction());
  48. enterTimer.setRepeats(false);
  49. exitTimer = new Timer(500, new outsideTimerAction());
  50. exitTimer.setRepeats(false);
  51. insideTimer = new Timer(4000, new stillInsideTimerAction());
  52. insideTimer.setRepeats(false);
  53. // create accessibility actions
  54. postTip = KeyStroke.getKeyStroke(KeyEvent.VK_F1,Event.CTRL_MASK);
  55. postTipAction = new AbstractAction(){
  56. public void actionPerformed(ActionEvent e){
  57. if (tipWindow != null) // showing we unshow
  58. hideTipWindow();
  59. else {
  60. hideTipWindow(); // be safe
  61. enterTimer.stop();
  62. exitTimer.stop();
  63. insideTimer.stop();
  64. insideComponent = (JComponent)e.getSource();
  65. if (insideComponent != null){
  66. toolTipText = insideComponent.getToolTipText();
  67. preferredLocation = new Point(10,insideComponent.getHeight()+10); // manual set
  68. showTipWindow();
  69. // put a focuschange listener on to bring the tip down
  70. if (focusChangeListener == null){
  71. focusChangeListener = createFocusChangeListener();
  72. }
  73. insideComponent.addFocusListener(focusChangeListener);
  74. }
  75. }
  76. }
  77. };
  78. hideTip = KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE,0);
  79. hideTipAction = new AbstractAction(){
  80. public void actionPerformed(ActionEvent e){
  81. hideTipWindow();
  82. JComponent jc = (JComponent)e.getSource();
  83. jc.removeFocusListener(focusChangeListener);
  84. preferredLocation = null;
  85. }
  86. public boolean isEnabled() {
  87. // Only enable when the tooltip is showing, otherwise
  88. // we will get in the way of any UI actions.
  89. return tipShowing;
  90. }
  91. };
  92. }
  93. /**
  94. * Enables or disables the tooltip.
  95. *
  96. * @param flag true to enable the tip
  97. */
  98. public void setEnabled(boolean flag) {
  99. enabled = flag;
  100. if (!flag) {
  101. hideTipWindow();
  102. }
  103. }
  104. /**
  105. * Returns true if this object is enabled.
  106. *
  107. * @return true if this object is enabled
  108. */
  109. public boolean isEnabled() {
  110. return enabled;
  111. }
  112. /**
  113. * When displaying the JToolTip, the ToolTipManager choose to use a light weight JPanel if
  114. * it fits. This method allows you to disable this feature. You have to do disable
  115. * it if your application mixes light weight and heavy weights components.
  116. *
  117. */
  118. public void setLightWeightPopupEnabled(boolean aFlag){
  119. popupFactory.setLightWeightPopupEnabled(aFlag);
  120. }
  121. /**
  122. * Returns true if lightweight (all-Java) Tooltips are in use,
  123. * or false if heavyweight (native peer) Tooltips are being used.
  124. *
  125. * @return true if lightweight ToolTips are in use
  126. */
  127. public boolean isLightWeightPopupEnabled() {
  128. return popupFactory.isLightWeightPopupEnabled();
  129. }
  130. /**
  131. * Specifies the initial delay value.
  132. *
  133. * @param milliseconds the number of milliseconds
  134. * to delay (after the cursor has paused) before displaying the
  135. * tooltip
  136. * @see #getInitialDelay
  137. */
  138. public void setInitialDelay(int milliseconds) {
  139. enterTimer.setInitialDelay(milliseconds);
  140. }
  141. /**
  142. * Returns the initial delay value.
  143. *
  144. * @return an int representing the initial delay value
  145. * @see #setInitialDelay
  146. */
  147. public int getInitialDelay() {
  148. return enterTimer.getInitialDelay();
  149. }
  150. /**
  151. * Specifies the dismisal delay value.
  152. *
  153. * @param milliseconds the number of milliseconds
  154. * to delay (after the cursor has moved on) before taking away
  155. * the tooltip
  156. * @see #getDismissDelay
  157. */
  158. public void setDismissDelay(int milliseconds) {
  159. insideTimer.setInitialDelay(milliseconds);
  160. }
  161. /**
  162. * Returns the dismisal delay value.
  163. *
  164. * @return an int representing the dismisal delay value
  165. * @see #setDismissDelay
  166. */
  167. public int getDismissDelay() {
  168. return insideTimer.getInitialDelay();
  169. }
  170. /**
  171. * Specifies the time to delay before reshowing the tooltip.
  172. *
  173. * @param milliseconds the time in milliseconds
  174. * to delay before reshowing the tooltip if the cursor stops again
  175. * @see #getReshowDelay
  176. */
  177. public void setReshowDelay(int milliseconds) {
  178. exitTimer.setInitialDelay(milliseconds);
  179. }
  180. /**
  181. * Returns the reshow delay value.
  182. *
  183. * @return an int representing the reshow delay value
  184. * @see #setReshowDelay
  185. */
  186. public int getReshowDelay() {
  187. return exitTimer.getInitialDelay();
  188. }
  189. void showTipWindow() {
  190. if(insideComponent == null || !insideComponent.isShowing())
  191. return;
  192. if (enabled) {
  193. Dimension size;
  194. Point screenLocation = insideComponent.getLocationOnScreen();
  195. Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize();
  196. Point location = new Point();
  197. boolean leftToRight
  198. = SwingUtilities.isLeftToRight(insideComponent);
  199. // Just to be paranoid
  200. hideTipWindow();
  201. tip = insideComponent.createToolTip();
  202. tip.setTipText(toolTipText);
  203. size = tip.getPreferredSize();
  204. if(preferredLocation != null) {
  205. location.x = screenLocation.x + preferredLocation.x;
  206. location.y = screenLocation.y + preferredLocation.y;
  207. if (!leftToRight) {
  208. location.x -= size.width;
  209. }
  210. } else {
  211. location.x = screenLocation.x + mouseEvent.getX();
  212. location.y = screenLocation.y + mouseEvent.getY() + 20;
  213. if (!leftToRight) {
  214. if(location.x - size.width>=0) {
  215. location.x -= size.width;
  216. }
  217. }
  218. if (location.x + size.width > screenSize.width) {
  219. location.x -= size.width;
  220. }
  221. if (location.y + size.height > screenSize.height) {
  222. location.y -= (size.height + 20);
  223. }
  224. }
  225. // we do not adjust x/y when using awt.Window tips
  226. if (!heavyWeightPopupEnabled){
  227. if (popupRect == null){
  228. popupRect = new Rectangle();
  229. }
  230. popupRect.setBounds(location.x,location.y,
  231. size.width,size.height);
  232. int y = getPopupFitHeight(popupRect, insideComponent);
  233. int x = getPopupFitWidth(popupRect,insideComponent);
  234. if (y > 0){
  235. location.y -= y;
  236. }
  237. if (x > 0){
  238. // adjust
  239. location.x -= x;
  240. }
  241. }
  242. tipWindow = popupFactory.getPopup(tip,
  243. insideComponent,
  244. location.x,
  245. location.y);
  246. if (tipWindow instanceof Window)
  247. ((Window)tipWindow).addMouseListener(this);
  248. tipWindow.show(insideComponent);
  249. insideTimer.start();
  250. timerEnter = System.currentTimeMillis();
  251. tipShowing = true;
  252. }
  253. }
  254. void hideTipWindow() {
  255. if (tipWindow != null) {
  256. if (tipWindow instanceof Window)
  257. ((Window)tipWindow).removeMouseListener(this);
  258. tipWindow.hide();
  259. tipWindow = null;
  260. tipShowing = false;
  261. timerEnter = 0;
  262. (tip.getUI()).uninstallUI(tip);
  263. tip = null;
  264. insideTimer.stop();
  265. }
  266. }
  267. /**
  268. * Returns a shared ToolTipManager instance.
  269. *
  270. * @return a shared ToolTipManager object
  271. */
  272. public static ToolTipManager sharedInstance() {
  273. return sharedInstance;
  274. }
  275. // add keylistener here to trigger tip for access
  276. /**
  277. * Register a component for tooltip management.
  278. * <p>This will register key bindings to show and hide the tooltip text
  279. * only if <code>component</code> has focus bindings. This is done
  280. * so that components that are not normally focus traversable, such
  281. * as JLabel, are not made focus traversable as a result of invoking
  282. * this method.
  283. *
  284. * @param component a JComponent object
  285. * @see JComponent#isFocusTraversable
  286. */
  287. public void registerComponent(JComponent component) {
  288. component.removeMouseListener(this);
  289. component.addMouseListener(this);
  290. if (shouldRegisterBindings(component)) {
  291. // register our accessibility keybindings for this component
  292. // this will apply globally across L&F
  293. // Post Tip: Ctrl+F1
  294. // Unpost Tip: Esc and Ctrl+F1
  295. InputMap inputMap = component.getInputMap(JComponent.WHEN_FOCUSED);
  296. ActionMap actionMap = component.getActionMap();
  297. if (inputMap != null && actionMap != null) {
  298. inputMap.put(postTip, "postTip");
  299. inputMap.put(hideTip, "hideTip");
  300. actionMap.put("postTip", postTipAction);
  301. actionMap.put("hideTip", hideTipAction);
  302. }
  303. }
  304. }
  305. /**
  306. * Remove a component from tooltip control.
  307. *
  308. * @param component a JComponent object
  309. */
  310. public void unregisterComponent(JComponent component) {
  311. component.removeMouseListener(this);
  312. if (shouldRegisterBindings(component)) {
  313. InputMap inputMap = component.getInputMap(JComponent.WHEN_FOCUSED);
  314. ActionMap actionMap = component.getActionMap();
  315. if (inputMap != null && actionMap != null) {
  316. inputMap.remove(postTip);
  317. inputMap.remove(hideTip);
  318. actionMap.remove("postTip");
  319. actionMap.remove("hideTip");
  320. }
  321. }
  322. }
  323. /**
  324. * Returns whether or not bindings should be registered on the given
  325. * Component. This is implemented to return true if the receiver has
  326. * a binding in any one of the InputMaps registered under the condition
  327. * <code>WHEN_FOCUSED</code>.
  328. * <p>
  329. * This does not use <code>isFocusTraversable</code> as
  330. * some components may override <code>isFocusTraversable</code> and
  331. * base the return value on something other than bindings. For example,
  332. * JButton bases its return value on its enabled state.
  333. */
  334. private boolean shouldRegisterBindings(JComponent component) {
  335. InputMap inputMap = component.getInputMap(JComponent.WHEN_FOCUSED,
  336. false);
  337. while (inputMap != null && inputMap.size() == 0) {
  338. inputMap = inputMap.getParent();
  339. }
  340. return (inputMap != null);
  341. }
  342. // implements java.awt.event.MouseListener
  343. public void mouseEntered(MouseEvent event) {
  344. // this is here for a workaround for a Solaris *application* only bug
  345. // in which an extra MouseExit/Enter events are generated when a Panel
  346. // initially is shown
  347. if ((tipShowing) && !lightWeightPopupEnabled)
  348. {
  349. if (System.currentTimeMillis() - timerEnter < 200){
  350. return;
  351. }
  352. }
  353. if(event.getSource() == tipWindow)
  354. return;
  355. JComponent component = (JComponent)event.getSource();
  356. toolTipText = component.getToolTipText(event);
  357. preferredLocation = component.getToolTipLocation(event);
  358. exitTimer.stop();
  359. Point location = event.getPoint();
  360. // ensure tooltip shows only in proper place
  361. if (location.x < 0 ||
  362. location.x >=component.getWidth() ||
  363. location.y < 0 ||
  364. location.y >= component.getHeight())
  365. {
  366. return;
  367. }
  368. if (insideComponent != null) {
  369. enterTimer.stop();
  370. insideComponent = null;
  371. }
  372. component.addMouseMotionListener(this);
  373. insideComponent = component;
  374. // if (toolTipText != null) {
  375. // fix for 4133318
  376. if (tipWindow != null){
  377. // fix for 4139679
  378. // without this - the tip flashes
  379. // since we get extra enter from the
  380. // tip window when being displayed over top
  381. // of the component - the behaviour is
  382. // the same whether or not we are over the
  383. // component - so additional location checks unneeded
  384. if (heavyWeightPopupEnabled){
  385. return;
  386. }
  387. else {
  388. mouseEvent = event;
  389. if (showImmediately) {
  390. showTipWindow();
  391. } else {
  392. enterTimer.start();
  393. }
  394. }
  395. }
  396. }
  397. // implements java.awt.event.MouseListener
  398. public void mouseExited(MouseEvent event) {
  399. // this is here for a workaround for a Solaris *application* only bug
  400. // when Panels are used
  401. if ((tipShowing) && !lightWeightPopupEnabled)
  402. {
  403. if (System.currentTimeMillis() - timerEnter < 200)
  404. {
  405. return;
  406. }
  407. }
  408. boolean shouldHide = true;
  409. if (insideComponent == null) {
  410. // Drag exit
  411. }
  412. if(event.getSource() == tipWindow) {
  413. // if we get an exit and have a heavy window
  414. // we need to check if it if overlapping the inside component
  415. Container insideComponentWindow = insideComponent.getTopLevelAncestor();
  416. Rectangle b = tipWindow.getBoundsOnScreen();
  417. Point location = event.getPoint();
  418. location.x += b.x;
  419. location.y += b.y;
  420. b = insideComponentWindow.getBounds();
  421. location.x -= b.x;
  422. location.y -= b.y;
  423. location = SwingUtilities.convertPoint(null,location,insideComponent);
  424. if(location.x >= 0 && location.x < insideComponent.getWidth() &&
  425. location.y >= 0 && location.y < insideComponent.getHeight()) {
  426. shouldHide = false;
  427. } else
  428. shouldHide = true;
  429. } else if(event.getSource() == insideComponent && tipWindow != null) {
  430. Point location = SwingUtilities.convertPoint(insideComponent,
  431. event.getPoint(),
  432. null);
  433. Rectangle bounds = insideComponent.getTopLevelAncestor().getBounds();
  434. location.x += bounds.x;
  435. location.y += bounds.y;
  436. bounds = tipWindow.getBoundsOnScreen();
  437. if(location.x >= bounds.x && location.x < (bounds.x + bounds.width) &&
  438. location.y >= bounds.y && location.y < (bounds.y + bounds.height)) {
  439. shouldHide = false;
  440. } else
  441. shouldHide = true;
  442. }
  443. if(shouldHide) {
  444. enterTimer.stop();
  445. if (insideComponent != null) {
  446. insideComponent.removeMouseMotionListener(this);
  447. }
  448. insideComponent = null;
  449. toolTipText = null;
  450. mouseEvent = null;
  451. hideTipWindow();
  452. exitTimer.start();
  453. }
  454. }
  455. // implements java.awt.event.MouseListener
  456. public void mousePressed(MouseEvent event) {
  457. hideTipWindow();
  458. enterTimer.stop();
  459. showImmediately = false;
  460. }
  461. // implements java.awt.event.MouseMotionListener
  462. public void mouseDragged(MouseEvent event) {
  463. }
  464. // implements java.awt.event.MouseMotionListener
  465. public void mouseMoved(MouseEvent event) {
  466. JComponent component = (JComponent)event.getSource();
  467. String newText = component.getToolTipText(event);
  468. Point newPreferredLocation = component.getToolTipLocation(event);
  469. if (newText != null || newPreferredLocation != null) {
  470. mouseEvent = event;
  471. if (((newText != null && newText.equals(toolTipText)) || newText == null) &&
  472. ((newPreferredLocation != null && newPreferredLocation.equals(preferredLocation))
  473. || newPreferredLocation == null)) {
  474. if (tipWindow != null) {
  475. insideTimer.restart();
  476. } else {
  477. enterTimer.restart();
  478. }
  479. } else {
  480. toolTipText = newText;
  481. preferredLocation = newPreferredLocation;
  482. if (showImmediately) {
  483. hideTipWindow();
  484. showTipWindow();
  485. } else {
  486. enterTimer.restart();
  487. }
  488. }
  489. } else {
  490. toolTipText = null;
  491. preferredLocation = null;
  492. mouseEvent = null;
  493. hideTipWindow();
  494. enterTimer.stop();
  495. exitTimer.start();
  496. }
  497. }
  498. protected class insideTimerAction implements ActionListener {
  499. public void actionPerformed(ActionEvent e) {
  500. if(insideComponent != null && insideComponent.isShowing()) {
  501. showImmediately = true;
  502. showTipWindow();
  503. }
  504. }
  505. }
  506. protected class outsideTimerAction implements ActionListener {
  507. public void actionPerformed(ActionEvent e) {
  508. showImmediately = false;
  509. }
  510. }
  511. protected class stillInsideTimerAction implements ActionListener {
  512. public void actionPerformed(ActionEvent e) {
  513. hideTipWindow();
  514. enterTimer.stop();
  515. showImmediately = false;
  516. }
  517. }
  518. static Frame frameForComponent(Component component) {
  519. while (!(component instanceof Frame)) {
  520. component = component.getParent();
  521. }
  522. return (Frame)component;
  523. }
  524. private FocusListener createFocusChangeListener(){
  525. return new FocusAdapter(){
  526. public void focusLost(FocusEvent evt){
  527. hideTipWindow();
  528. JComponent c = (JComponent)evt.getSource();
  529. c.removeFocusListener(focusChangeListener);
  530. }
  531. };
  532. }
  533. // Returns: 0 no adjust
  534. // -1 can't fit
  535. // >0 adjust value by amount returned
  536. private int getPopupFitWidth(Rectangle popupRectInScreen, Component invoker){
  537. if (invoker != null){
  538. Container parent;
  539. for (parent = invoker.getParent(); parent != null; parent = parent.getParent()){
  540. // fix internal frame size bug: 4139087 - 4159012
  541. if(parent instanceof JFrame || parent instanceof JDialog ||
  542. parent instanceof JWindow) { // no check for awt.Frame since we use Heavy tips
  543. return getWidthAdjust(parent.getBounds(),popupRectInScreen);
  544. } else if (parent instanceof JApplet || parent instanceof JInternalFrame) {
  545. if (popupFrameRect == null){
  546. popupFrameRect = new Rectangle();
  547. }
  548. Point p = parent.getLocationOnScreen();
  549. popupFrameRect.setBounds(p.x,p.y,
  550. parent.getBounds().width,
  551. parent.getBounds().height);
  552. return getWidthAdjust(popupFrameRect,popupRectInScreen);
  553. }
  554. }
  555. }
  556. return 0;
  557. }
  558. // Returns: 0 no adjust
  559. // >0 adjust by value return
  560. private int getPopupFitHeight(Rectangle popupRectInScreen, Component invoker){
  561. if (invoker != null){
  562. Container parent;
  563. for (parent = invoker.getParent(); parent != null; parent = parent.getParent()){
  564. if(parent instanceof JFrame || parent instanceof JDialog ||
  565. parent instanceof JWindow) {
  566. return getHeightAdjust(parent.getBounds(),popupRectInScreen);
  567. } else if (parent instanceof JApplet || parent instanceof JInternalFrame) {
  568. if (popupFrameRect == null){
  569. popupFrameRect = new Rectangle();
  570. }
  571. Point p = parent.getLocationOnScreen();
  572. popupFrameRect.setBounds(p.x,p.y,
  573. parent.getBounds().width,
  574. parent.getBounds().height);
  575. return getHeightAdjust(popupFrameRect,popupRectInScreen);
  576. }
  577. }
  578. }
  579. return 0;
  580. }
  581. private int getHeightAdjust(Rectangle a, Rectangle b){
  582. if (b.y >= a.y && (b.y + b.height) <= (a.y + a.height))
  583. return 0;
  584. else
  585. return (((b.y + b.height) - (a.y + a.height)) + 5);
  586. }
  587. // Return the number of pixels over the edge we are extending.
  588. // If we are over the edge the ToolTipManager can adjust.
  589. // REMIND: what if the Tooltip is just too big to fit at all - we currently will just clip
  590. private int getWidthAdjust(Rectangle a, Rectangle b){
  591. // System.out.println("width b.x/b.width: " + b.x + "/" + b.width +
  592. // "a.x/a.width: " + a.x + "/" + a.width);
  593. if (b.x >= a.x && (b.x + b.width) <= (a.x + a.width)){
  594. return 0;
  595. }
  596. else {
  597. return (((b.x + b.width) - (a.x +a.width)) + 5);
  598. }
  599. }
  600. }