/*
* Sun Public License Notice
*
* The contents of this file are subject to the Sun Public License
* Version 1.0 (the "License"). You may not use this file except in
* compliance with the License. A copy of the License is available at
* http://www.sun.com/
*
* The Original Code is NetBeans. The Initial Developer of the Original
* Code is Sun Microsystems, Inc. Portions Copyright 1997-2000 Sun
* Microsystems, Inc. All Rights Reserved.
*/
package org.netbeans.editor.ext;
import org.netbeans.editor.*;
import javax.swing.*;
import javax.swing.text.BadLocationException;
import javax.swing.text.JTextComponent;
import java.awt.*;
import java.awt.event.*;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeSupport;
/**
* Support for editor tooltips. Once the user stops moving the mouse
* for the {@link #INITIAL_DELAY} milliseconds the enterTimer fires
* and the {@link #updateToolTip()} method is called which searches
* for the action named {@link ExtKit#buildToolTipAction} and if found
* it executes it. The tooltips can be displayed by either calling
* {@link #setToolTipText(java.lang.String)}
* or {@link #setToolTip(javax.swing.JComponent)}.<BR>
* However only one of the above ways should be used
* not a combination of both because in such case
* the text could be propagated in the previously set
* custom tooltip component.
*
* @author Miloslav Metelka
* @version 1.00
*/
public class ToolTipSupport extends MouseAdapter
implements MouseMotionListener, ActionListener, PropertyChangeListener,
SettingsChangeListener, FocusListener {
/** Property for the tooltip component change */
public static final String PROP_TOOL_TIP = "toolTip";
/** Property for the tooltip text change */
public static final String PROP_TOOL_TIP_TEXT = "toolTipText";
/** Property for the visibility status change. */
public static final String PROP_STATUS = "status";
/** Property for the enabled flag change */
public static final String PROP_ENABLED = "enabled";
/** Property for the initial delay change */
public static final String PROP_INITIAL_DELAY = "initialDelay";
/** Property for the dismiss delay change */
public static final String PROP_DISMISS_DELAY = "dismissDelay";
private static final String UI_PREFIX = "ToolTip"; // NOI18N
/** Initial delay before the tooltip is shown in milliseconds. */
public static final int INITIAL_DELAY = 1000;
/** Delay after which the tooltip will be hidden automatically
* in milliseconds.
*/
public static final int DISMISS_DELAY = 60000;
/** Status indicating that the tooltip is not showing on the screen. */
public static final int STATUS_HIDDEN = 0;
/** Status indicating that the tooltip is not showing on the screen
* but once either the {@link #setToolTipText(java.lang.String)}
* or {@link #setToolTip(javax.swing.JComponent)} gets called
* the tooltip will become visible.
*/
public static final int STATUS_VISIBILITY_ENABLED = 1;
/** Status indicating that the tooltip is visible
* because {@link #setToolTipText(java.lang.String)}
* was called.
*/
public static final int STATUS_TEXT_VISIBLE = 2;
/** Status indicating that the tooltip is visible
* because {@link #setToolTip(javax.swing.JComponent)}
* was called.
*/
public static final int STATUS_COMPONENT_VISIBLE = 3;
/** Extra height added to the rectangle of modelToView() for mouse
* cursor coordinates.
*/
private static final int MOUSE_EXTRA_HEIGHT = 5;
private ExtEditorUI extEditorUI;
private JComponent toolTip;
private String toolTipText;
private Timer enterTimer;
private Timer exitTimer;
private boolean enabled;
/** Status of the tooltip visibility. */
private int status;
private MouseEvent lastMouseEvent;
private PropertyChangeSupport pcs;
/** Construct new support for tooltips.
*/
public ToolTipSupport(ExtEditorUI extEditorUI) {
this.extEditorUI = extEditorUI;
enterTimer = new Timer(INITIAL_DELAY, new WeakTimerListener(this));
enterTimer.setRepeats(false);
exitTimer = new Timer(DISMISS_DELAY, new WeakTimerListener(this));
exitTimer.setRepeats(false);
Settings.addSettingsChangeListener(this);
extEditorUI.addPropertyChangeListener(this);
setEnabled(true);
}
/** @return the component that either contains the tooltip
* or is responsible for displaying of text tooltips.
*/
public final JComponent getToolTip() {
if (toolTip == null) {
setToolTip(createDefaultToolTip());
}
return toolTip;
}
/** Set the tooltip component.
* It can be called either to set the custom component
* that will display the text tooltips or to display
* the generic component with the tooltip after
* the tooltip timer has fired.
* @param toolTip component that either contains the tooltip
* or that will display a text tooltip.
*/
public void setToolTip(JComponent toolTip) {
JComponent oldToolTip = this.toolTip;
this.toolTip = toolTip;
if (status >= STATUS_VISIBILITY_ENABLED) {
ensureVisibility();
}
firePropertyChange(PROP_TOOL_TIP, oldToolTip, this.toolTip);
}
/** Create the default tooltip component.
*/
protected JComponent createDefaultToolTip() {
return createTextToolTip();
}
private JTextArea createTextToolTip() {
JTextArea tt = new JTextArea() {
public void setSize(int width, int height) {
int docLen = getDocument().getLength();
if (docLen > 0) { // nonzero length
setLineWrap(false);
Dimension prefSize = getPreferredSize();
if (width > prefSize.width) { // given width unnecessarily big
width = prefSize.width; // shrink the width to preferred
if (height >= prefSize.height) {
height = prefSize.height;
} else { // height not big enough
height = -1;
}
} else { // available width not enough - wrap lines
setLineWrap(true);
super.setSize(width, 100000);
try {
Rectangle r = modelToView(docLen - 1);
int prefHeight = r.y + r.height;
if (prefHeight < height) {
height = prefHeight;
} else { // the given height is too small
height = -1;
}
} catch (BadLocationException e) {
}
}
}
if (height >= 0) { // only for valid height
super.setSize(width, height);
} else { // signal that the height is too small to display tooltip
putClientProperty(PopupManager.Placement.class, null);
}
}
};
Font font = UIManager.getFont(UI_PREFIX + ".font"); // NOI18N
Color backColor = UIManager.getColor(UI_PREFIX + ".background"); // NOI18N
Color foreColor = UIManager.getColor(UI_PREFIX + ".foreground"); // NOI18N
if (font != null) {
tt.setFont(font);
}
if (foreColor != null) {
tt.setForeground(foreColor);
}
if (backColor != null) {
tt.setBackground(backColor);
}
tt.setOpaque(true);
tt.setBorder(BorderFactory.createCompoundBorder(
BorderFactory.createLineBorder(tt.getForeground()),
BorderFactory.createEmptyBorder(0, 3, 0, 3)
));
return tt;
}
public void settingsChange(SettingsChangeEvent evt) {
}
public void propertyChange(PropertyChangeEvent evt) {
String propName = evt.getPropertyName();
if (extEditorUI.COMPONENT_PROPERTY.equals(propName)) {
JTextComponent component = (JTextComponent)evt.getNewValue();
if (component != null) { // just installed
component.addPropertyChangeListener(this);
disableSwingToolTip(component);
component.addFocusListener(this);
if (component.hasFocus()) {
focusGained(new FocusEvent(component, FocusEvent.FOCUS_GAINED));
}
} else { // just deinstalled
component = (JTextComponent)evt.getOldValue();
component.removeFocusListener(this);
component.removePropertyChangeListener(this);
}
}
if (JComponent.TOOL_TIP_TEXT_KEY.equals(propName)) {
JComponent component = (JComponent)evt.getSource();
disableSwingToolTip(component);
componentToolTipTextChanged(evt);
}
}
private void disableSwingToolTip(final JComponent component) {
javax.swing.SwingUtilities.invokeLater(
new Runnable() {
public void run() {
// Prevent default swing tooltip manager
javax.swing.ToolTipManager.sharedInstance().unregisterComponent(component);
// Also disable the swing tooltip manager on gutter component
GlyphGutter gg = extEditorUI.getGlyphGutter();
if (gg != null) {
javax.swing.ToolTipManager.sharedInstance().unregisterComponent(gg);
}
}
}
);
}
/** Update the tooltip by running corresponding action
* {@link ExtKit#buildToolTipAction}. This method gets
* called once the enterTimer fires and it can be overriden
* by children.
*/
protected void updateToolTip() {
ExtEditorUI ui = extEditorUI;
if (ui == null)
return;
JTextComponent comp = ui.getComponent();
if (comp == null)
return;
if (isGlyphGutterMouseEvent(lastMouseEvent)) {
setToolTipText(extEditorUI.getGlyphGutter().getToolTipText(lastMouseEvent));
} else { // over the text component
BaseKit kit = Utilities.getKit(comp);
if (kit != null) {
Action a = kit.getActionByName(ExtKit.buildToolTipAction);
if (a != null) {
a.actionPerformed(new ActionEvent(comp, 0, "")); // NOI18N
}
}
}
}
/** Set the visibility of the tooltip.
* @param visible whether tooltip should become visible or not.
* If true the status is changed
* to {@link { #STATUS_VISIBILITY_ENABLED}
* and @link #updateToolTip()} is called.<BR>
* It is still possible that the tooltip will not be showing
* on the screen in case the tooltip or tooltip text are left
* unchanged.
*/
protected void setToolTipVisible(boolean visible) {
if (!visible) { // ensure the timers are stopped
enterTimer.stop();
exitTimer.stop();
}
if (visible && status < STATUS_VISIBILITY_ENABLED
|| !visible && status >= STATUS_VISIBILITY_ENABLED
) {
if (visible) { // try to show the tooltip
if (enabled) {
setStatus(STATUS_VISIBILITY_ENABLED);
updateToolTip();
}
} else { // hide tip
if (toolTip != null) {
toolTip.setVisible(false);
}
setStatus(STATUS_HIDDEN);
}
}
}
/** @return Whether the tooltip is showing on the screen.
* {@link #getStatus() } gives the exact visibility state.
*/
public boolean isToolTipVisible() {
return status > STATUS_VISIBILITY_ENABLED;
}
/** @return status of the tooltip visibility. It can
* be {@link #STATUS_HIDDEN}
* or {@link #STATUS_VISIBILITY_ENABLED}
* or {@link #STATUS_TEXT_VISIBLE}
* or {@link #STATUS_COMPONENT_VISIBLE}.
*/
public final int getStatus() {
return status;
}
private void setStatus(int status) {
if (this.status != status) {
int oldStatus = this.status;
this.status = status;
firePropertyChange(PROP_STATUS,
new Integer(oldStatus), new Integer(this.status));
}
}
/** @return the current tooltip text.
*/
public String getToolTipText() {
return toolTipText;
}
/** Set the tooltip text to make the tooltip
* to be shown on the screen.
* @param text tooltip text to be displayed.
*/
public void setToolTipText(String text) {
String oldText = toolTipText;
toolTipText = text;
firePropertyChange(PROP_TOOL_TIP_TEXT, oldText, toolTipText);
if (toolTipText != null) {
JTextArea ta = createTextToolTip();
ta.setText(toolTipText);
setToolTip(ta);
} else { // null text
if (status == STATUS_TEXT_VISIBLE) {
setToolTipVisible(false);
}
}
}
private void applyToolTipText() {
JComponent tt = getToolTip();
if (tt != null) {
if (tt instanceof JLabel) {
((JLabel)tt).setText(toolTipText);
} else if (tt instanceof JTextComponent) {
((JTextComponent)tt).setText(toolTipText);
} else if (tt instanceof javax.swing.JToolTip) {
((javax.swing.JToolTip)tt).setTipText(toolTipText);
} else {
try {
java.lang.reflect.Method m = tt.getClass().getMethod("setText",
new Class[] { String.class });
if (m != null) {
m.invoke(toolTip, new Object[] { toolTipText });
}
} catch (NoSuchMethodException e) {
} catch (IllegalAccessException e) {
} catch (java.lang.reflect.InvocationTargetException e) {
}
}
}
}
private boolean isGlyphGutterMouseEvent(MouseEvent evt) {
return (evt != null && evt.getSource() == extEditorUI.getGlyphGutter());
}
private void ensureVisibility() {
// Find the visual position in the document
JTextComponent component = extEditorUI.getComponent();
if (component != null) {
// Try to display the tooltip above (or below) the line it corresponds to
int pos = component.viewToModel(getLastMouseEventPoint());
Rectangle cursorBounds = null;
if (pos >= 0) {
try {
cursorBounds = component.modelToView(pos);
// Enlarge the height slightly to not interfere with mouse cursor
cursorBounds.y -= MOUSE_EXTRA_HEIGHT;
cursorBounds.height += 2 * MOUSE_EXTRA_HEIGHT; // above and below
} catch (BadLocationException e) {
}
}
if (cursorBounds == null) { // get mose rect
cursorBounds = new Rectangle(getLastMouseEventPoint(), new Dimension(1, 1));
}
// updateToolTipBounds();
PopupManager pm = extEditorUI.getPopupManager();
pm.install(toolTip, cursorBounds, PopupManager.AbovePreferred);
}
exitTimer.restart();
}
/** Helper method to get the identifier
* under the mouse cursor.
* @return string containing identifier under
* mouse cursor.
*/
public String getIdentifierUnderCursor() {
String word = null;
if (!isGlyphGutterMouseEvent(lastMouseEvent)) {
try {
JTextComponent component = extEditorUI.getComponent();
BaseTextUI ui = (BaseTextUI)component.getUI();
Point lmePoint = getLastMouseEventPoint();
int pos = ui.viewToModel(component, lmePoint);
if (pos >= 0) {
BaseDocument doc = (BaseDocument)component.getDocument();
int eolPos = Utilities.getRowEnd(doc, pos);
Rectangle eolRect = ui.modelToView(component, eolPos);
int lineHeight = extEditorUI.getLineHeight();
if (lmePoint.x <= eolRect.x && lmePoint.y <= eolRect.y + lineHeight) {
word = Utilities.getIdentifier(doc, pos);
}
}
} catch (BadLocationException e) {
// word will be null
}
}
return word;
}
/** @return whether the tooltip support is enabled. If it's
* disabled the tooltip does not become visible.
*/
public boolean isEnabled() {
return enabled;
}
/** Set whether the tooltip support is enabled. If it's
* disabled the tooltip does not become visible.
* @param enabled whether the tooltip will be enabled or not.
*/
public void setEnabled(boolean enabled) {
if (enabled != this.enabled) {
this.enabled = enabled;
firePropertyChange(PROP_ENABLED,
enabled ? Boolean.FALSE : Boolean.TRUE,
enabled ? Boolean.TRUE : Boolean.FALSE
);
if (!enabled) {
setToolTipVisible(false);
}
}
}
/** @return the delay between stopping
* mouse movement and displaying
* of the tooltip in milliseconds.
*/
public int getInitialDelay() {
return enterTimer.getDelay();
}
/** Set the delay between stopping
* mouse movement and displaying
* of the tooltip in milliseconds.
*/
public void setInitialDelay(int delay) {
if (enterTimer.getDelay() != delay) {
int oldDelay = enterTimer.getDelay();
enterTimer.setDelay(delay);
firePropertyChange(PROP_INITIAL_DELAY,
new Integer(oldDelay), new Integer(enterTimer.getDelay()));
}
}
/** @return the delay between displaying
* of the tooltip and its automatic hiding
* in milliseconds.
*/
public int getDismissDelay() {
return exitTimer.getDelay();
}
/** Set the delay between displaying
* of the tooltip and its automatic hiding
* in milliseconds.
*/
public void setDismissDelay(int delay) {
if (exitTimer.getDelay() != delay) {
int oldDelay = exitTimer.getDelay();
exitTimer.setDelay(delay);
firePropertyChange(PROP_DISMISS_DELAY,
new Integer(oldDelay), new Integer(exitTimer.getDelay()));
}
}
public void actionPerformed(ActionEvent evt) {
if (evt.getSource() == enterTimer) {
setToolTipVisible(true);
} else if (evt.getSource() == exitTimer) {
setToolTipVisible(false);
}
}
public void mouseClicked(MouseEvent evt) {
lastMouseEvent = evt;
setToolTipVisible(false);
}
public void mousePressed(MouseEvent evt) {
lastMouseEvent = evt;
setToolTipVisible(false);
}
public void mouseReleased(MouseEvent evt) {
lastMouseEvent = evt;
setToolTipVisible(false);
}
public void mouseEntered(MouseEvent evt) {
lastMouseEvent = evt;
}
public void mouseExited(MouseEvent evt) {
lastMouseEvent = evt;
setToolTipVisible(false);
}
public void mouseDragged(MouseEvent evt) {
lastMouseEvent = evt;
setToolTipVisible(false);
}
public void mouseMoved(MouseEvent evt) {
setToolTipVisible(false);
if (enabled) {
enterTimer.restart();
}
lastMouseEvent = evt;
}
/** @return last mouse event captured by this support.
* This method can be used by the action that evaluates
* the tooltip.
*/
public final MouseEvent getLastMouseEvent() {
return lastMouseEvent;
}
/** Possibly do translation when over the gutter.
*/
private Point getLastMouseEventPoint() {
Point p = null;
MouseEvent lme = lastMouseEvent;
if (lme != null) {
p = lme.getPoint();
if (lme.getSource() == extEditorUI.getGlyphGutter()) {
// Over glyph gutter - change coords
JTextComponent c = extEditorUI.getComponent();
if (c != null) {
if (c.getParent() instanceof JViewport) {
JViewport vp = (JViewport)c.getParent();
p = new Point(vp.getViewPosition().x, p.y);
}
}
}
}
return p;
}
/** Called automatically when the
* {@link javax.swing.JComponent#TOOL_TIP_TEXT_KEY}
* property of the corresponding editor component
* gets changed.<BR>
* By default it calls {@link #setToolTipText(java.lang.String)}
* with the new tooltip text of the component.
*/
protected void componentToolTipTextChanged(PropertyChangeEvent evt) {
JComponent component = (JComponent)evt.getSource();
setToolTipText(component.getToolTipText());
}
private PropertyChangeSupport getPCS() {
if (pcs == null) {
pcs = new PropertyChangeSupport(this);
}
return pcs;
}
/** Add the listener for the property changes. The names
* of the supported properties are defined
* as "PROP_" public static string constants.
* @param listener listener to be added.
*/
public void addPropertyChangeListener(PropertyChangeListener listener) {
getPCS().addPropertyChangeListener(listener);
}
public void removePropertyChangeListener(PropertyChangeListener listener) {
getPCS().removePropertyChangeListener(listener);
}
/** Fire the change of the given property.
* @param propertyName name of the fired property
* @param oldValue old value of the property
* @param newValue new value of the property.
*/
protected void firePropertyChange(String propertyName,
Object oldValue, Object newValue) {
getPCS().firePropertyChange(propertyName, oldValue, newValue);
}
public void focusGained(FocusEvent e) {
JComponent component = (JComponent)e.getSource();
component.addMouseListener(this);
component.addMouseMotionListener(this);
GlyphGutter gg = extEditorUI.getGlyphGutter();
if (gg != null) {
gg.addMouseListener(this);
gg.addMouseMotionListener(this);
}
}
public void focusLost(FocusEvent e) {
JComponent component = (JComponent)e.getSource();
component.removeMouseListener(this);
component.removeMouseMotionListener(this);
GlyphGutter gg = extEditorUI.getGlyphGutter();
if (gg != null) {
gg.removeMouseListener(this);
gg.removeMouseMotionListener(this);
}
setToolTipVisible(false);
}
}