All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.intellij.ide.IdeTooltipManager Maven / Gradle / Ivy

Go to download

A packaging of the IntelliJ Community Edition platform-impl library. This is release number 1 of trunk branch 142.

The newest version!
/*
 * 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.JBInsets;
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 prefSize = new Ref(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) {
          JBInsets.addTo(s, b.getBorderInsets(this));
        }
        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);
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy