001: /*
002: * Sun Public License Notice
003: *
004: * The contents of this file are subject to the Sun Public License
005: * Version 1.0 (the "License"). You may not use this file except in
006: * compliance with the License. A copy of the License is available at
007: * http://www.sun.com/
008: *
009: * The Original Code is NetBeans. The Initial Developer of the Original
010: * Code is Sun Microsystems, Inc. Portions Copyright 1997-2000 Sun
011: * Microsystems, Inc. All Rights Reserved.
012: */
013:
014: package org.netbeans.editor.ext;
015:
016: import java.awt.Color;
017: import java.awt.Dimension;
018: import java.awt.Font;
019: import java.awt.Rectangle;
020: import java.awt.event.ActionEvent;
021: import java.awt.event.ActionListener;
022: import java.awt.event.ComponentAdapter;
023: import java.awt.event.ComponentEvent;
024: import java.awt.event.FocusEvent;
025: import java.awt.event.FocusListener;
026: import java.awt.event.MouseAdapter;
027: import java.awt.event.MouseEvent;
028: import java.awt.event.MouseMotionListener;
029: import java.beans.PropertyChangeEvent;
030: import java.beans.PropertyChangeListener;
031: import java.beans.PropertyChangeSupport;
032:
033: import javax.swing.Action;
034: import javax.swing.JComponent;
035: import javax.swing.JLabel;
036: import javax.swing.JLayeredPane;
037: import javax.swing.JRootPane;
038: import javax.swing.Timer;
039: import javax.swing.UIManager;
040: import javax.swing.text.BadLocationException;
041: import javax.swing.text.JTextComponent;
042:
043: import org.netbeans.editor.BaseDocument;
044: import org.netbeans.editor.BaseKit;
045: import org.netbeans.editor.BaseTextUI;
046: import org.netbeans.editor.SettingsChangeEvent;
047: import org.netbeans.editor.SettingsChangeListener;
048: import org.netbeans.editor.Utilities;
049:
050: /**
051: * Support for editor tooltips. Once the user stops moving the mouse for the
052: * {@link #INITIAL_DELAY} milliseconds the enterTimer fires and the
053: * {@link #updateToolTip()} method is called which searches for the action named
054: * {@link ExtKit#buildToolTipAction} and if found it executes it. The tooltips
055: * can be displayed by either calling {@link #setToolTipText(java.lang.String)}
056: * or {@link #setToolTip(javax.swing.JComponent)}.<BR>
057: * However only one of the above ways should be used not a combination of both
058: * because in such case the text could be propagated in the previously set
059: * custom tooltip component.
060: *
061: * @author Miloslav Metelka
062: * @version 1.00
063: */
064:
065: public class ToolTipSupport extends MouseAdapter implements
066: MouseMotionListener, ActionListener, PropertyChangeListener,
067: SettingsChangeListener, FocusListener {
068:
069: /** Property for the tooltip component change */
070: public static final String PROP_TOOL_TIP = "toolTip";
071:
072: /** Property for the tooltip text change */
073: public static final String PROP_TOOL_TIP_TEXT = "toolTipText";
074:
075: /** Property for the visibility status change. */
076: public static final String PROP_STATUS = "status";
077:
078: /** Property for the enabled flag change */
079: public static final String PROP_ENABLED = "enabled";
080:
081: /** Property for the initial delay change */
082: public static final String PROP_INITIAL_DELAY = "initialDelay";
083:
084: /** Property for the dismiss delay change */
085: public static final String PROP_DISMISS_DELAY = "dismissDelay";
086:
087: private static final String UI_PREFIX = "ToolTip"; // NOI18N
088:
089: /** Initial delay before the tooltip is shown in milliseconds. */
090: public static final int INITIAL_DELAY = 1000;
091:
092: /**
093: * Delay after which the tooltip will be hidden automatically in
094: * milliseconds.
095: */
096: public static final int DISMISS_DELAY = 60000;
097:
098: /** Status indicating that the tooltip is not showing on the screen. */
099: public static final int STATUS_HIDDEN = 0;
100: /**
101: * Status indicating that the tooltip is not showing on the screen but once
102: * either the {@link #setToolTipText(java.lang.String)} or
103: * {@link #setToolTip(javax.swing.JComponent)} gets called the tooltip will
104: * become visible.
105: */
106: public static final int STATUS_VISIBILITY_ENABLED = 1;
107: /**
108: * Status indicating that the tooltip is visible because
109: * {@link #setToolTipText(java.lang.String)} was called.
110: */
111: public static final int STATUS_TEXT_VISIBLE = 2;
112: /**
113: * Status indicating that the tooltip is visible because
114: * {@link #setToolTip(javax.swing.JComponent)} was called.
115: */
116: public static final int STATUS_COMPONENT_VISIBLE = 3;
117:
118: private ExtEditorUI extEditorUI;
119:
120: private JComponent toolTip;
121:
122: private String toolTipText;
123:
124: private Timer enterTimer;
125:
126: private Timer exitTimer;
127:
128: private boolean enabled;
129:
130: /** Status of the tooltip visibility. */
131: private int status;
132:
133: private MouseEvent lastMouseEvent;
134:
135: private ComponentAdapter componentL;
136:
137: private PropertyChangeSupport pcs;
138:
139: /**
140: * Construct new support for tooltips.
141: */
142: public ToolTipSupport(ExtEditorUI extEditorUI) {
143: this .extEditorUI = extEditorUI;
144:
145: componentL = new ComponentAdapter() {
146: public void componentHidden(ComponentEvent evt) {
147: checkRemoveFromPane((JComponent) evt.getSource());
148: }
149: };
150:
151: // enterTimer = new Timer(INITIAL_DELAY, new WeakTimerListener(this));
152: // enterTimer.setRepeats(false);
153: // exitTimer = new Timer(DISMISS_DELAY, new WeakTimerListener(this));
154: // exitTimer.setRepeats(false);
155:
156: // Settings.addSettingsChangeListener(this);
157: // extEditorUI.addPropertyChangeListener(this);
158:
159: // setEnabled(true);
160: }
161:
162: /**
163: * @return the component that either contains the tooltip or is responsible
164: * for displaying of text tooltips.
165: */
166: public final JComponent getToolTip() {
167: if (toolTip == null) {
168: setToolTip(createDefaultToolTip());
169: }
170:
171: return toolTip;
172: }
173:
174: /**
175: * Set the tooltip component. It can be called either to set the custom
176: * component that will display the text tooltips or to display the generic
177: * component with the tooltip after the tooltip timer has fired.
178: *
179: * @param toolTip
180: * component that either contains the tooltip or that will
181: * display a text tooltip.
182: */
183: public void setToolTip(JComponent toolTip) {
184: if (this .toolTip != toolTip) {
185: JComponent oldToolTip = this .toolTip;
186: checkRemoveFromPane(extEditorUI.getComponent());
187: this .toolTip = toolTip;
188: checkAddToPane();
189: if (status >= STATUS_VISIBILITY_ENABLED) {
190: ensureVisibility();
191: }
192:
193: firePropertyChange(PROP_TOOL_TIP, oldToolTip, this .toolTip);
194: }
195: }
196:
197: /**
198: * Create the default tooltip component.
199: */
200: protected JComponent createDefaultToolTip() {
201: JLabel tt = new JLabel();
202:
203: Font font = UIManager.getFont(UI_PREFIX + ".font"); // NOI18N
204: Color backColor = UIManager.getColor(UI_PREFIX + ".background"); // NOI18N
205: Color foreColor = UIManager.getColor(UI_PREFIX + ".foreground"); // NOI18N
206:
207: if (font != null) {
208: tt.setFont(font);
209: }
210: if (foreColor != null) {
211: tt.setForeground(foreColor);
212: }
213: if (backColor != null) {
214: tt.setBackground(backColor);
215: }
216:
217: tt.setOpaque(true);
218: // tt.setBorder(BorderFactory.createCompoundBorder(
219: // BorderFactory.createLineBorder(tt.getForeground()),
220: // BorderFactory.createEmptyBorder(0, 3, 0, 3)
221: // ));
222:
223: return tt;
224: }
225:
226: public void settingsChange(SettingsChangeEvent evt) {
227: }
228:
229: public void propertyChange(PropertyChangeEvent evt) {
230: String propName = evt.getPropertyName();
231:
232: if (extEditorUI.COMPONENT_PROPERTY.equals(propName)) {
233: JTextComponent component = (JTextComponent) evt
234: .getNewValue();
235: if (component != null) { // just installed
236:
237: component.addPropertyChangeListener(this );
238: component.addComponentListener(componentL);
239:
240: checkAddToPane();
241:
242: disableSwingToolTip(component);
243:
244: component.addFocusListener(this );
245: if (component.hasFocus()) {
246: focusGained(new FocusEvent(component,
247: FocusEvent.FOCUS_GAINED));
248: }
249:
250: } else { // just deinstalled
251: component = (JTextComponent) evt.getOldValue();
252:
253: component.removeFocusListener(this );
254: component.removePropertyChangeListener(this );
255: component.removeComponentListener(componentL);
256:
257: checkRemoveFromPane(component);
258: }
259: }
260:
261: if (JComponent.TOOL_TIP_TEXT_KEY.equals(propName)) {
262: JComponent component = (JComponent) evt.getSource();
263: disableSwingToolTip(component);
264:
265: componentToolTipTextChanged(evt);
266: }
267:
268: }
269:
270: private void disableSwingToolTip(final JComponent component) {
271: javax.swing.SwingUtilities.invokeLater(new Runnable() {
272: public void run() {
273: // Prevent default swing tooltip manager
274:
275: javax.swing.ToolTipManager.sharedInstance()
276: .unregisterComponent(component);
277: }
278: });
279: }
280:
281: private void checkRemoveFromPane(JComponent component) {
282: if (toolTip != null) {
283: JRootPane rp = component.getRootPane();
284: if (rp != null) {
285: rp.remove(toolTip);
286: }
287: }
288: }
289:
290: private void checkAddToPane() {
291: if (toolTip != null) {
292: JTextComponent component = extEditorUI.getComponent();
293: if (component != null) {
294: JRootPane rp = component.getRootPane();
295:
296: // Possibly deinstall the old component from layered pane
297: JRootPane ttrp = toolTip.getRootPane();
298: if (ttrp != rp) {
299: if (ttrp != null) {
300: ttrp.getLayeredPane().remove(toolTip);
301: }
302: rp.getLayeredPane().add(toolTip,
303: JLayeredPane.POPUP_LAYER, 0);
304: }
305: }
306: }
307: }
308:
309: /**
310: * Update the tooltip by running corresponding action
311: * {@link ExtKit#buildToolTipAction}. This method gets called once the
312: * enterTimer fires and it can be overriden by children.
313: */
314: protected void updateToolTip() {
315: ExtEditorUI ui = extEditorUI;
316: if (ui == null)
317: return;
318: JTextComponent comp = ui.getComponent();
319: if (comp == null)
320: return;
321: BaseKit kit = Utilities.getKit(comp);
322: if (kit != null) {
323: Action a = kit.getActionByName(ExtKit.buildToolTipAction);
324: if (a != null) {
325: a.actionPerformed(new ActionEvent(comp, 0, "")); // NOI18N
326: }
327: }
328: }
329:
330: /**
331: * Update bounds of the tooltip component. It gets called when the tooltip
332: * component is about to be shown or when it was updated by
333: * {@link #setToolTip(javax.swing.JComponent)}.
334: */
335: protected void updateToolTipBounds() {
336: Dimension prefSize = toolTip.getPreferredSize();
337: Rectangle extBounds = extEditorUI.getExtentBounds();
338: int x = Math.min(lastMouseEvent.getX() - prefSize.width / 2,
339: extBounds.x + extBounds.width - prefSize.width);
340: x = Math.max(x, extBounds.x);
341: int lineHeight = extEditorUI.getLineHeight();
342: int y = lastMouseEvent.getY() - 2 * lineHeight;
343: if (y - extBounds.y < lineHeight) {
344: y = lastMouseEvent.getY() + lineHeight;
345: }
346:
347: toolTip.setBounds(x - extBounds.x, y - extBounds.y,
348: prefSize.width, prefSize.height);
349: }
350:
351: /**
352: * Set the visibility of the tooltip.
353: *
354: * @param visible
355: * whether tooltip should become visible or not. If true the
356: * status is changed to {@link { #STATUS_VISIBILITY_ENABLED} and
357: * @link #updateToolTip()} is called.<BR>
358: * It is still possible that the tooltip will not be showing on the
359: * screen in case the tooltip or tooltip text are left unchanged.
360: */
361: protected void setToolTipVisible(boolean visible) {
362: if (!visible) { // ensure the timers are stopped
363: enterTimer.stop();
364: exitTimer.stop();
365: }
366:
367: if (visible && status < STATUS_VISIBILITY_ENABLED || !visible
368: && status >= STATUS_VISIBILITY_ENABLED) {
369: if (visible) { // try to show the tooltip
370: if (enabled) {
371: setStatus(STATUS_VISIBILITY_ENABLED);
372:
373: updateToolTip();
374: }
375:
376: } else { // hide tip
377: if (toolTip != null) {
378: toolTip.setVisible(false);
379: }
380:
381: setStatus(STATUS_HIDDEN);
382: }
383: }
384: }
385:
386: /**
387: * @return Whether the tooltip is showing on the screen.
388: * {@link #getStatus() } gives the exact visibility state.
389: */
390: public boolean isToolTipVisible() {
391: return status > STATUS_VISIBILITY_ENABLED;
392: }
393:
394: /**
395: * @return status of the tooltip visibility. It can be
396: * {@link #STATUS_HIDDEN} or {@link #STATUS_VISIBILITY_ENABLED} or
397: * {@link #STATUS_TEXT_VISIBLE} or {@link #STATUS_COMPONENT_VISIBLE}.
398: */
399: public final int getStatus() {
400: return status;
401: }
402:
403: private void setStatus(int status) {
404: if (this .status != status) {
405: int oldStatus = this .status;
406: this .status = status;
407: firePropertyChange(PROP_STATUS, new Integer(oldStatus),
408: new Integer(this .status));
409: }
410: }
411:
412: /**
413: * @return the current tooltip text.
414: */
415: public String getToolTipText() {
416: return toolTipText;
417: }
418:
419: /**
420: * Set the tooltip text to make the tooltip to be shown on the screen.
421: *
422: * @param text
423: * tooltip text to be displayed.
424: */
425: public void setToolTipText(String text) {
426: String oldText = toolTipText;
427:
428: toolTipText = text;
429:
430: firePropertyChange(PROP_TOOL_TIP_TEXT, oldText, toolTipText);
431:
432: applyToolTipText();
433:
434: if (toolTipText != null) {
435: if (status >= STATUS_VISIBILITY_ENABLED) {
436: ensureVisibility();
437: }
438:
439: } else { // null text
440: if (status == STATUS_TEXT_VISIBLE) {
441: setToolTipVisible(false);
442: }
443: }
444:
445: }
446:
447: private void applyToolTipText() {
448: JComponent tt = getToolTip();
449: if (tt != null) {
450: if (tt instanceof JLabel) {
451: ((JLabel) tt).setText(toolTipText);
452:
453: } else if (tt instanceof JTextComponent) {
454: ((JTextComponent) tt).setText(toolTipText);
455:
456: } else if (tt instanceof javax.swing.JToolTip) {
457: ((javax.swing.JToolTip) tt).setTipText(toolTipText);
458:
459: } else {
460: try {
461: java.lang.reflect.Method m = tt.getClass()
462: .getMethod("setText",
463: new Class[] { String.class });
464: if (m != null) {
465: m.invoke(toolTip, new Object[] { toolTipText });
466: }
467: } catch (NoSuchMethodException e) {
468: } catch (IllegalAccessException e) {
469: } catch (java.lang.reflect.InvocationTargetException e) {
470: }
471: }
472: }
473: }
474:
475: private void ensureVisibility() {
476: updateToolTipBounds();
477: toolTip.setVisible(true);
478: exitTimer.restart();
479: }
480:
481: /**
482: * Helper method to get the identifier under the mouse cursor.
483: *
484: * @return string containing identifier under mouse cursor.
485: */
486: public String getIdentifierUnderCursor() {
487: String word = null;
488: try {
489: JTextComponent component = extEditorUI.getComponent();
490: BaseTextUI ui = (BaseTextUI) component.getUI();
491: int pos = ui.viewToModel(component, lastMouseEvent
492: .getPoint());
493: if (pos >= 0) {
494: BaseDocument doc = (BaseDocument) component
495: .getDocument();
496: int eolPos = Utilities.getRowEnd(doc, pos);
497: Rectangle eolRect = ui.modelToView(component, eolPos);
498: int lineHeight = extEditorUI.getLineHeight();
499: if (lastMouseEvent.getX() <= eolRect.x
500: && lastMouseEvent.getY() <= eolRect.y
501: + lineHeight) {
502: word = Utilities.getIdentifier(doc, pos);
503: }
504: }
505: } catch (BadLocationException e) {
506: // word will be null
507: }
508:
509: return word;
510: }
511:
512: /**
513: * @return whether the tooltip support is enabled. If it's disabled the
514: * tooltip does not become visible.
515: */
516: public boolean isEnabled() {
517: return enabled;
518: }
519:
520: /**
521: * Set whether the tooltip support is enabled. If it's disabled the tooltip
522: * does not become visible.
523: *
524: * @param enabled
525: * whether the tooltip will be enabled or not.
526: */
527: public void setEnabled(boolean enabled) {
528: if (enabled != this .enabled) {
529: this .enabled = enabled;
530:
531: firePropertyChange(PROP_ENABLED, enabled ? Boolean.FALSE
532: : Boolean.TRUE, enabled ? Boolean.TRUE
533: : Boolean.FALSE);
534:
535: if (!enabled) {
536: setToolTipVisible(false);
537: }
538: }
539: }
540:
541: /**
542: * @return the delay between stopping mouse movement and displaying of the
543: * tooltip in milliseconds.
544: */
545: public int getInitialDelay() {
546: return enterTimer.getDelay();
547: }
548:
549: /**
550: * Set the delay between stopping mouse movement and displaying of the
551: * tooltip in milliseconds.
552: */
553: public void setInitialDelay(int delay) {
554: if (enterTimer.getDelay() != delay) {
555: int oldDelay = enterTimer.getDelay();
556: enterTimer.setDelay(delay);
557:
558: firePropertyChange(PROP_INITIAL_DELAY,
559: new Integer(oldDelay), new Integer(enterTimer
560: .getDelay()));
561: }
562: }
563:
564: /**
565: * @return the delay between displaying of the tooltip and its automatic
566: * hiding in milliseconds.
567: */
568: public int getDismissDelay() {
569: return exitTimer.getDelay();
570: }
571:
572: /**
573: * Set the delay between displaying of the tooltip and its automatic hiding
574: * in milliseconds.
575: */
576: public void setDismissDelay(int delay) {
577: if (exitTimer.getDelay() != delay) {
578: int oldDelay = exitTimer.getDelay();
579: exitTimer.setDelay(delay);
580:
581: firePropertyChange(PROP_DISMISS_DELAY,
582: new Integer(oldDelay), new Integer(exitTimer
583: .getDelay()));
584: }
585: }
586:
587: public void actionPerformed(ActionEvent evt) {
588: if (evt.getSource() == enterTimer) {
589: setToolTipVisible(true);
590:
591: } else if (evt.getSource() == exitTimer) {
592: setToolTipVisible(false);
593: }
594: }
595:
596: public void mouseClicked(MouseEvent evt) {
597: lastMouseEvent = evt;
598: setToolTipVisible(false);
599: }
600:
601: public void mousePressed(MouseEvent evt) {
602: lastMouseEvent = evt;
603: setToolTipVisible(false);
604: }
605:
606: public void mouseReleased(MouseEvent evt) {
607: lastMouseEvent = evt;
608: setToolTipVisible(false);
609: }
610:
611: public void mouseEntered(MouseEvent evt) {
612: lastMouseEvent = evt;
613: }
614:
615: public void mouseExited(MouseEvent evt) {
616: lastMouseEvent = evt;
617: setToolTipVisible(false);
618: }
619:
620: public void mouseDragged(MouseEvent evt) {
621: lastMouseEvent = evt;
622: setToolTipVisible(false);
623: }
624:
625: public void mouseMoved(MouseEvent evt) {
626: setToolTipVisible(false);
627: if (enabled) {
628: enterTimer.restart();
629: }
630: lastMouseEvent = evt;
631: }
632:
633: /**
634: * @return last mouse event captured by this support. This method can be
635: * used by the action that evaluates the tooltip.
636: */
637: public final MouseEvent getLastMouseEvent() {
638: return lastMouseEvent;
639: }
640:
641: /**
642: * Called automatically when the
643: * {@link javax.swing.JComponent#TOOL_TIP_TEXT_KEY} property of the
644: * corresponding editor component gets changed.<BR>
645: * By default it calls {@link #setToolTipText(java.lang.String)} with the
646: * new tooltip text of the component.
647: */
648: protected void componentToolTipTextChanged(PropertyChangeEvent evt) {
649: JComponent component = (JComponent) evt.getSource();
650: setToolTipText(component.getToolTipText());
651: }
652:
653: private PropertyChangeSupport getPCS() {
654: if (pcs == null) {
655: pcs = new PropertyChangeSupport(this );
656: }
657: return pcs;
658: }
659:
660: /**
661: * Add the listener for the property changes. The names of the supported
662: * properties are defined as "PROP_" public static string constants.
663: *
664: * @param listener
665: * listener to be added.
666: */
667: public void addPropertyChangeListener(
668: PropertyChangeListener listener) {
669: getPCS().addPropertyChangeListener(listener);
670: }
671:
672: public void removePropertyChangeListener(
673: PropertyChangeListener listener) {
674: getPCS().removePropertyChangeListener(listener);
675: }
676:
677: /**
678: * Fire the change of the given property.
679: *
680: * @param propertyName
681: * name of the fired property
682: * @param oldValue
683: * old value of the property
684: * @param newValue
685: * new value of the property.
686: */
687: protected void firePropertyChange(String propertyName,
688: Object oldValue, Object newValue) {
689: getPCS().firePropertyChange(propertyName, oldValue, newValue);
690: }
691:
692: public void focusGained(FocusEvent e) {
693: JComponent component = (JComponent) e.getSource();
694: component.addMouseListener(this );
695: component.addMouseMotionListener(this );
696: }
697:
698: public void focusLost(FocusEvent e) {
699: JComponent component = (JComponent) e.getSource();
700: component.removeMouseListener(this );
701: component.removeMouseMotionListener(this );
702: setToolTipVisible(false);
703: }
704:
705: }
|