blob: bf4d92576ee5c4c0c8052de93feb7148b6994cb0 [file] [log] [blame]
/*
* Copyright 2000-2013 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.intellij.ide;
import com.intellij.codeInsight.hint.HintUtil;
import com.intellij.icons.AllIcons;
import com.intellij.openapi.actionSystem.ActionManager;
import com.intellij.openapi.actionSystem.AnAction;
import com.intellij.openapi.actionSystem.AnActionEvent;
import com.intellij.openapi.actionSystem.DataContext;
import com.intellij.openapi.actionSystem.ex.AnActionListener;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.components.ApplicationComponent;
import com.intellij.openapi.ui.popup.Balloon;
import com.intellij.openapi.ui.popup.BalloonBuilder;
import com.intellij.openapi.ui.popup.JBPopupFactory;
import com.intellij.openapi.util.Comparing;
import com.intellij.openapi.util.Ref;
import com.intellij.openapi.util.registry.Registry;
import com.intellij.openapi.util.registry.RegistryValue;
import com.intellij.openapi.util.registry.RegistryValueListener;
import com.intellij.ui.*;
import com.intellij.ui.awt.RelativePoint;
import com.intellij.ui.components.panels.Wrapper;
import com.intellij.util.Alarm;
import com.intellij.util.IJSwingUtilities;
import com.intellij.util.ui.Html;
import com.intellij.util.ui.UIUtil;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import javax.swing.*;
import javax.swing.border.Border;
import javax.swing.border.EmptyBorder;
import javax.swing.text.*;
import javax.swing.text.html.HTML;
import javax.swing.text.html.HTMLEditorKit;
import javax.swing.tree.TreePath;
import java.awt.*;
import java.awt.event.AWTEventListener;
import java.awt.event.MouseEvent;
public class IdeTooltipManager implements ApplicationComponent, AWTEventListener {
public static final String IDE_TOOLTIP_PLACE = "IdeTooltip";
public static final Color GRAPHITE_COLOR = new Color(100, 100, 100, 230);
private RegistryValue myIsEnabled;
private Component myCurrentComponent;
private Component myQueuedComponent;
private BalloonImpl myCurrentTipUi;
private MouseEvent myCurrentEvent;
private boolean myCurrentTipIsCentered;
private Runnable myHideRunnable;
private final JBPopupFactory myPopupFactory;
private boolean myShowDelay = true;
private final Alarm myAlarm = new Alarm();
private int myX;
private int myY;
private IdeTooltip myCurrentTooltip;
private Runnable myShowRequest;
private IdeTooltip myQueuedTooltip;
public IdeTooltipManager(JBPopupFactory popupFactory) {
myPopupFactory = popupFactory;
}
@Override
public void initComponent() {
myIsEnabled = Registry.get("ide.tooltip.callout");
myIsEnabled.addListener(new RegistryValueListener.Adapter() {
@Override
public void afterValueChanged(RegistryValue value) {
processEnabled();
}
}, ApplicationManager.getApplication());
Toolkit.getDefaultToolkit().addAWTEventListener(this, AWTEvent.MOUSE_EVENT_MASK | AWTEvent.MOUSE_MOTION_EVENT_MASK);
ActionManager.getInstance().addAnActionListener(new AnActionListener.Adapter() {
@Override
public void beforeActionPerformed(AnAction action, DataContext dataContext, AnActionEvent event) {
hideCurrent(null, action, event);
}
}, ApplicationManager.getApplication());
processEnabled();
}
@Override
public void eventDispatched(AWTEvent event) {
if (!myIsEnabled.asBoolean()) return;
MouseEvent me = (MouseEvent)event;
Component c = me.getComponent();
if (me.getID() == MouseEvent.MOUSE_ENTERED) {
boolean canShow = true;
if (c != myCurrentComponent) {
canShow = hideCurrent(me, null, null);
}
if (canShow) {
maybeShowFor(c, me);
}
}
else if (me.getID() == MouseEvent.MOUSE_EXITED) {
if (c == myCurrentComponent || c == myQueuedComponent) {
hideCurrent(me, null, null);
}
}
else if (me.getID() == MouseEvent.MOUSE_MOVED) {
if (c == myCurrentComponent || c == myQueuedComponent) {
if (myCurrentTipUi != null && myCurrentTipUi.wasFadedIn()) {
if (hideCurrent(me, null, null)) {
maybeShowFor(c, me);
}
}
else {
if (!myCurrentTipIsCentered) {
myX = me.getX();
myY = me.getY();
if (c instanceof JComponent && ((JComponent)c).getToolTipText(me) == null && (myQueuedTooltip == null || !myQueuedTooltip.isHint())) {
hideCurrent(me, null, null);//There is no tooltip or hint here, let's proceed it as MOUSE_EXITED
}
else {
maybeShowFor(c, me);
}
}
}
}
else if (myCurrentComponent == null && myQueuedComponent == null) {
maybeShowFor(c, me);
}
}
else if (me.getID() == MouseEvent.MOUSE_PRESSED) {
if (c == myCurrentComponent) {
hideCurrent(me, null, null);
}
}
else if (me.getID() == MouseEvent.MOUSE_DRAGGED) {
hideCurrent(me, null, null);
}
}
private void maybeShowFor(Component c, MouseEvent me) {
if (!(c instanceof JComponent)) return;
JComponent comp = (JComponent)c;
Window wnd = SwingUtilities.getWindowAncestor(comp);
if (wnd == null) return;
if (!wnd.isActive()) {
if (JBPopupFactory.getInstance().isChildPopupFocused(wnd)) return;
}
String tooltipText = comp.getToolTipText(me);
if (tooltipText == null || tooltipText.trim().isEmpty()) return;
boolean centerDefault = Boolean.TRUE.equals(comp.getClientProperty(UIUtil.CENTER_TOOLTIP_DEFAULT));
boolean centerStrict = Boolean.TRUE.equals(comp.getClientProperty(UIUtil.CENTER_TOOLTIP_STRICT));
int shift = centerStrict ? 0 : centerDefault ? 4 : 0;
// Balloon may appear exactly above useful content, such behavior is rather annoying.
if (c instanceof JTree) {
TreePath path = ((JTree)c).getClosestPathForLocation(me.getX(), me.getY());
if (path != null) {
Rectangle pathBounds = ((JTree)c).getPathBounds(path);
if (pathBounds != null && pathBounds.y + 4 < me.getY()) {
shift += me.getY() - pathBounds.y - 4;
}
}
}
queueShow(comp, me, centerStrict || centerDefault, shift, -shift, -shift);
}
private void queueShow(final JComponent c, final MouseEvent me, final boolean toCenter, int shift, int posChangeX, int posChangeY) {
final IdeTooltip tooltip = new IdeTooltip(c, me.getPoint(), null, new Object()) {
@Override
protected boolean beforeShow() {
myCurrentEvent = me;
if (!c.isShowing()) return false;
String text = c.getToolTipText(myCurrentEvent);
if (text == null || text.trim().isEmpty()) return false;
JLayeredPane layeredPane = IJSwingUtilities.findParentOfType(c, JLayeredPane.class);
final JEditorPane pane = initPane(text, new HintHint(me).setAwtTooltip(true), layeredPane);
final Wrapper wrapper = new Wrapper(pane);
setTipComponent(wrapper);
return true;
}
}.setToCenter(toCenter).setCalloutShift(shift).setPositionChangeShift(posChangeX, posChangeY).setLayer(Balloon.Layer.top);
show(tooltip, false);
}
public IdeTooltip show(final IdeTooltip tooltip, boolean now) {
return show(tooltip, now, true);
}
public IdeTooltip show(final IdeTooltip tooltip, boolean now, final boolean animationEnabled) {
myAlarm.cancelAllRequests();
hideCurrent(null, null, null);
myQueuedComponent = tooltip.getComponent();
myQueuedTooltip = tooltip;
myShowRequest = new Runnable() {
@Override
public void run() {
if (myShowRequest == null) {
return;
}
if (myQueuedComponent != tooltip.getComponent() || !tooltip.getComponent().isShowing()) {
hideCurrent(null, null, null, animationEnabled);
return;
}
if (tooltip.beforeShow()) {
show(tooltip, null, animationEnabled);
}
else {
hideCurrent(null, null, null, animationEnabled);
}
}
};
if (now) {
myShowRequest.run();
}
else {
myAlarm.addRequest(myShowRequest, myShowDelay ? tooltip.getShowDelay() : tooltip.getInitialReshowDelay());
}
return tooltip;
}
private void show(final IdeTooltip tooltip, @Nullable Runnable beforeShow, boolean animationEnabled) {
boolean toCenterX;
boolean toCenterY;
boolean toCenter = tooltip.isToCenter();
boolean small = false;
if (!toCenter && tooltip.isToCenterIfSmall()) {
Dimension size = tooltip.getComponent().getSize();
toCenterX = size.width < 64;
toCenterY = size.height < 64;
toCenter = toCenterX || toCenterY;
small = true;
}
else {
toCenterX = true;
toCenterY = true;
}
Point effectivePoint = tooltip.getPoint();
if (toCenter) {
Rectangle bounds = tooltip.getComponent().getBounds();
effectivePoint.x = toCenterX ? bounds.width / 2 : effectivePoint.x;
effectivePoint.y = toCenterY ? bounds.height / 2 : effectivePoint.y;
}
if (myCurrentComponent == tooltip.getComponent() && myCurrentTipUi != null) {
myCurrentTipUi.show(new RelativePoint(tooltip.getComponent(), effectivePoint), tooltip.getPreferredPosition());
return;
}
if (myCurrentComponent == tooltip.getComponent() && effectivePoint.equals(new Point(myX, myY))) {
return;
}
Color bg = tooltip.getTextBackground() != null ? tooltip.getTextBackground() : getTextBackground(true);
Color fg = tooltip.getTextForeground() != null ? tooltip.getTextForeground() : getTextForeground(true);
Color border = tooltip.getBorderColor() != null ? tooltip.getBorderColor() : getBorderColor(true);
BalloonBuilder builder = myPopupFactory.createBalloonBuilder(tooltip.getTipComponent())
.setFillColor(bg)
.setBorderColor(border)
.setBorderInsets(tooltip.getBorderInsets())
.setAnimationCycle(animationEnabled ? Registry.intValue("ide.tooltip.animationCycle") : 0)
.setShowCallout(true)
.setCalloutShift(small && tooltip.getCalloutShift() == 0 ? 2 : tooltip.getCalloutShift())
.setPositionChangeXShift(tooltip.getPositionChangeX())
.setPositionChangeYShift(tooltip.getPositionChangeY())
.setHideOnKeyOutside(!tooltip.isExplicitClose())
.setHideOnAction(!tooltip.isExplicitClose())
.setLayer(tooltip.getLayer());
tooltip.getTipComponent().setForeground(fg);
tooltip.getTipComponent().setBorder(new EmptyBorder(1, 3, 2, 3));
tooltip.getTipComponent().setFont(tooltip.getFont() != null ? tooltip.getFont() : getTextFont(true));
if (beforeShow != null) {
beforeShow.run();
}
myCurrentTipUi = (BalloonImpl)builder.createBalloon();
myCurrentTipUi.setAnimationEnabled(animationEnabled);
tooltip.setUi(myCurrentTipUi);
myCurrentComponent = tooltip.getComponent();
myX = effectivePoint.x;
myY = effectivePoint.y;
myCurrentTipIsCentered = toCenter;
myCurrentTooltip = tooltip;
myShowRequest = null;
myQueuedComponent = null;
myQueuedTooltip = null;
myCurrentTipUi.show(new RelativePoint(tooltip.getComponent(), effectivePoint), tooltip.getPreferredPosition());
myAlarm.addRequest(new Runnable() {
@Override
public void run() {
if (myCurrentTooltip == tooltip && tooltip.canBeDismissedOnTimeout()) {
hideCurrent(null, null, null);
}
}
}, tooltip.getDismissDelay());
}
@SuppressWarnings({"MethodMayBeStatic", "UnusedParameters"})
public Color getTextForeground(boolean awtTooltip) {
return UIUtil.getToolTipForeground();
}
@SuppressWarnings({"MethodMayBeStatic", "UnusedParameters"})
public Color getLinkForeground(boolean awtTooltip) {
return JBColor.blue;
}
@SuppressWarnings({"MethodMayBeStatic", "UnusedParameters"})
public Color getTextBackground(boolean awtTooltip) {
return UIUtil.getToolTipBackground();
}
@SuppressWarnings({"MethodMayBeStatic", "UnusedParameters"})
public String getUlImg(boolean awtTooltip) {
AllIcons.General.Mdot.getIconWidth(); // keep icon reference
return UIUtil.isUnderDarcula() ? "/general/mdot-white.png" : "/general/mdot.png";
}
@SuppressWarnings({"MethodMayBeStatic", "UnusedParameters"})
public Color getBorderColor(boolean awtTooltip) {
return new JBColor(Gray._160, new Color(154, 154, 102));
}
@SuppressWarnings({"MethodMayBeStatic", "UnusedParameters"})
public boolean isOwnBorderAllowed(boolean awtTooltip) {
return !awtTooltip;
}
@SuppressWarnings({"MethodMayBeStatic", "UnusedParameters"})
public boolean isOpaqueAllowed(boolean awtTooltip) {
return !awtTooltip;
}
@SuppressWarnings({"MethodMayBeStatic", "UnusedParameters"})
public Font getTextFont(boolean awtTooltip) {
return UIManager.getFont("ToolTip.font");
}
public boolean hasCurrent() {
return myCurrentTooltip != null;
}
public boolean hideCurrent(@Nullable MouseEvent me, @Nullable AnAction action, @Nullable AnActionEvent event) {
return hideCurrent(me, action, event, myCurrentTipUi != null && myCurrentTipUi.isAnimationEnabled());
}
public boolean hideCurrent(@Nullable MouseEvent me, @Nullable AnAction action, @Nullable AnActionEvent event, final boolean animationEnabled) {
if (myCurrentTooltip != null && me != null && myCurrentTooltip.isInside(RelativePoint.fromScreen(me.getLocationOnScreen()))) {
if (me.getButton() == MouseEvent.NOBUTTON || myCurrentTipUi == null || myCurrentTipUi.isBlockClicks()) {
return false;
}
}
myShowRequest = null;
myQueuedComponent = null;
myQueuedTooltip = null;
if (myCurrentTooltip == null) return true;
if (myCurrentTipUi != null) {
RelativePoint target = me != null ? new RelativePoint(me) : null;
boolean isInside = target != null && myCurrentTipUi.isInside(target);
boolean isMovingForward = target != null && myCurrentTipUi.isMovingForward(target);
boolean canAutoHide = myCurrentTooltip.canAutohideOn(new TooltipEvent(me, isInside || isMovingForward, action, event));
boolean implicitMouseMove = me != null &&
(me.getID() == MouseEvent.MOUSE_MOVED ||
me.getID() == MouseEvent.MOUSE_EXITED ||
me.getID() == MouseEvent.MOUSE_ENTERED);
if (!canAutoHide || myCurrentTooltip.isExplicitClose() && implicitMouseMove) {
if (myHideRunnable != null) {
myHideRunnable = null;
}
return false;
}
}
myHideRunnable = new Runnable() {
@Override
public void run() {
if (myHideRunnable != null) {
hideCurrentNow(animationEnabled);
myHideRunnable = null;
}
}
};
if (me != null && me.getButton() == MouseEvent.NOBUTTON) {
myAlarm.addRequest(myHideRunnable, Registry.intValue("ide.tooltip.autoDismissDeadZone"));
}
else {
myHideRunnable.run();
myHideRunnable = null;
}
return true;
}
public void hideCurrentNow(boolean animationEnabled) {
if (myCurrentTipUi != null) {
myCurrentTipUi.setAnimationEnabled(animationEnabled);
myCurrentTipUi.hide();
myCurrentTooltip.onHidden();
myShowDelay = false;
myAlarm.addRequest(new Runnable() {
@Override
public void run() {
myShowDelay = true;
}
}, Registry.intValue("ide.tooltip.reshowDelay"));
}
myShowRequest = null;
myCurrentTooltip = null;
myCurrentTipUi = null;
myCurrentComponent = null;
myQueuedComponent = null;
myQueuedTooltip = null;
myCurrentEvent = null;
myCurrentTipIsCentered = false;
myX = -1;
myY = -1;
}
private void processEnabled() {
if (myIsEnabled.asBoolean()) {
ToolTipManager.sharedInstance().setEnabled(false);
}
else {
ToolTipManager.sharedInstance().setEnabled(true);
}
}
@Override
public void disposeComponent() {
}
public static IdeTooltipManager getInstance() {
return ApplicationManager.getApplication().getComponent(IdeTooltipManager.class);
}
public void hide(@Nullable IdeTooltip tooltip) {
if (myCurrentTooltip == tooltip || tooltip == null || tooltip == myQueuedTooltip) {
hideCurrent(null, null, null);
}
}
public void cancelAutoHide() {
myHideRunnable = null;
}
public static JEditorPane initPane(@NonNls String text, final HintHint hintHint, @Nullable final JLayeredPane layeredPane) {
return initPane(new Html(text), hintHint, layeredPane);
}
public static JEditorPane initPane(@NonNls Html html, final HintHint hintHint, @Nullable final JLayeredPane layeredPane) {
final Ref<Dimension> prefSize = new Ref<Dimension>(null);
@NonNls String text = HintUtil.prepareHintText(html, hintHint);
final boolean[] prefSizeWasComputed = {false};
final JEditorPane pane = new JEditorPane() {
@Override
public Dimension getPreferredSize() {
if (!prefSizeWasComputed[0] && hintHint.isAwtTooltip()) {
JLayeredPane lp = layeredPane;
if (lp == null) {
JRootPane rootPane = UIUtil.getRootPane(this);
if (rootPane != null) {
lp = rootPane.getLayeredPane();
}
}
Dimension size;
if (lp != null) {
size = lp.getSize();
prefSizeWasComputed[0] = true;
}
else {
size = ScreenUtil.getScreenRectangle(0, 0).getSize();
}
int fitWidth = (int)(size.width * 0.8);
Dimension prefSizeOriginal = super.getPreferredSize();
if (prefSizeOriginal.width > fitWidth) {
setSize(new Dimension(fitWidth, Integer.MAX_VALUE));
Dimension fixedWidthSize = super.getPreferredSize();
Dimension minSize = super.getMinimumSize();
prefSize.set(new Dimension(fitWidth > minSize.width ? fitWidth : minSize.width, fixedWidthSize.height));
}
else {
prefSize.set(new Dimension(prefSizeOriginal));
}
}
Dimension s = prefSize.get() != null ? new Dimension(prefSize.get()) : super.getPreferredSize();
Border b = getBorder();
if (b != null) {
Insets insets = b.getBorderInsets(this);
if (insets != null) {
s.width += insets.left + insets.right;
s.height += insets.top + insets.bottom;
}
}
return s;
}
@Override
public void setPreferredSize(Dimension preferredSize) {
super.setPreferredSize(preferredSize);
prefSize.set(preferredSize);
}
};
final HTMLEditorKit.HTMLFactory factory = new HTMLEditorKit.HTMLFactory() {
@Override
public View create(Element elem) {
AttributeSet attrs = elem.getAttributes();
Object elementName = attrs.getAttribute(AbstractDocument.ElementNameAttribute);
Object o = elementName != null ? null : attrs.getAttribute(StyleConstants.NameAttribute);
if (o instanceof HTML.Tag) {
HTML.Tag kind = (HTML.Tag)o;
if (kind == HTML.Tag.HR) {
return new CustomHrView(elem, hintHint.getTextForeground());
}
}
return super.create(elem);
}
};
HTMLEditorKit kit = new HTMLEditorKit() {
@Override
public ViewFactory getViewFactory() {
return factory;
}
};
pane.setEditorKit(kit);
pane.setText(text);
pane.setCaretPosition(0);
pane.setEditable(false);
if (hintHint.isOwnBorderAllowed()) {
setBorder(pane);
setColors(pane);
}
else {
pane.setBorder(null);
}
if (!hintHint.isAwtTooltip()) {
prefSizeWasComputed[0] = true;
}
final boolean opaque = hintHint.isOpaqueAllowed();
pane.setOpaque(opaque);
if (UIUtil.isUnderNimbusLookAndFeel() && !opaque) {
pane.setBackground(UIUtil.TRANSPARENT_COLOR);
}
else {
pane.setBackground(hintHint.getTextBackground());
}
return pane;
}
public static void setColors(JComponent pane) {
pane.setForeground(JBColor.foreground());
pane.setBackground(HintUtil.INFORMATION_COLOR);
pane.setOpaque(true);
}
public static void setBorder(JComponent pane) {
pane.setBorder(
BorderFactory.createCompoundBorder(BorderFactory.createLineBorder(Color.black), BorderFactory.createEmptyBorder(0, 5, 0, 5)));
}
@NotNull
@Override
public String getComponentName() {
return "IDE Tooltip Manager";
}
public boolean isQueuedToShow(IdeTooltip tooltip) {
return Comparing.equal(myQueuedTooltip, tooltip);
}
}