/* * Copyright 2000-2017 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.codeInsight.hint; import com.intellij.ide.IdeTooltip; import com.intellij.openapi.Disposable; import com.intellij.openapi.actionSystem.*; import com.intellij.openapi.actionSystem.ex.ActionManagerEx; import com.intellij.openapi.actionSystem.ex.AnActionListener; import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.components.ServiceManager; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.editor.Editor; import com.intellij.openapi.editor.LogicalPosition; import com.intellij.openapi.editor.ScrollType; import com.intellij.openapi.editor.event.*; import com.intellij.openapi.editor.ex.EditorEx; import com.intellij.openapi.editor.markup.*; import com.intellij.openapi.fileEditor.FileEditorManagerEvent; import com.intellij.openapi.fileEditor.FileEditorManagerListener; import com.intellij.openapi.project.Project; import com.intellij.openapi.project.ProjectManager; import com.intellij.openapi.project.ProjectManagerListener; import com.intellij.openapi.ui.popup.Balloon; import com.intellij.openapi.ui.popup.JBPopup; import com.intellij.openapi.ui.popup.JBPopupFactory; import com.intellij.openapi.util.Disposer; import com.intellij.openapi.util.registry.Registry; import com.intellij.ui.*; import com.intellij.ui.awt.RelativePoint; import com.intellij.util.Alarm; import com.intellij.util.BitUtil; import com.intellij.util.ui.UIUtil; import com.intellij.util.ui.accessibility.AccessibleContextUtil; import com.intellij.util.ui.accessibility.ScreenReader; import org.jetbrains.annotations.NotNull; import javax.swing.*; import java.awt.*; import java.awt.event.*; import java.util.ArrayList; import java.util.EventObject; import java.util.List; public class HintManagerImpl extends HintManager implements Disposable { private static final Logger LOG = Logger.getInstance("#com.intellij.codeInsight.hint.HintManager"); private final AnActionListener myAnActionListener; private final MyEditorManagerListener myEditorManagerListener; private final EditorMouseAdapter myEditorMouseListener; private final FocusListener myEditorFocusListener; private final DocumentListener myEditorDocumentListener; private final VisibleAreaListener myVisibleAreaListener; private final CaretListener myCaretMoveListener; private LightweightHint myQuestionHint; private QuestionAction myQuestionAction; private final List<HintInfo> myHintsStack = new ArrayList<>(); private Editor myLastEditor; private final Alarm myHideAlarm = new Alarm(); private static int getPriority(QuestionAction action) { return action instanceof PriorityQuestionAction ? ((PriorityQuestionAction)action).getPriority() : 0; } public boolean canShowQuestionAction(QuestionAction action) { ApplicationManager.getApplication().assertIsDispatchThread(); return myQuestionAction == null || getPriority(myQuestionAction) <= getPriority(action); } public interface ActionToIgnore { } private static class HintInfo { final LightweightHint hint; @HideFlags final int flags; private final boolean reviveOnEditorChange; private HintInfo(LightweightHint hint, @HideFlags int flags, boolean reviveOnEditorChange) { this.hint = hint; this.flags = flags; this.reviveOnEditorChange = reviveOnEditorChange; } } public static HintManagerImpl getInstanceImpl() { return (HintManagerImpl)ServiceManager.getService(HintManager.class); } public HintManagerImpl(ActionManagerEx actionManagerEx, ProjectManager projectManager) { myEditorManagerListener = new MyEditorManagerListener(); myAnActionListener = new MyAnActionListener(); actionManagerEx.addAnActionListener(myAnActionListener); myCaretMoveListener = new CaretListener() { @Override public void caretPositionChanged(CaretEvent e) { hideHints(HIDE_BY_ANY_KEY, false, false); } }; final MyProjectManagerListener projectManagerListener = new MyProjectManagerListener(); for (Project project : projectManager.getOpenProjects()) { projectManagerListener.projectOpened(project); } ApplicationManager.getApplication().getMessageBus().connect().subscribe(ProjectManager.TOPIC, projectManagerListener); myEditorMouseListener = new EditorMouseAdapter() { @Override public void mousePressed(EditorMouseEvent event) { hideAllHints(); } }; myVisibleAreaListener = e -> { updateScrollableHints(e); if (e.getOldRectangle() == null || e.getOldRectangle().x != e.getNewRectangle().x || e.getOldRectangle().y != e.getNewRectangle().y) { hideHints(HIDE_BY_SCROLLING, false, false); } }; myEditorFocusListener = new FocusAdapter() { @Override public void focusLost(final FocusEvent e) { //if (UIUtil.isFocusProxy(e.getOppositeComponent())) return; myHideAlarm.addRequest(new Runnable() { private boolean myNotFocused; // previous focus state @Override public void run() { // see implementation here: com.intellij.ui.popup.AbstractPopup.isFocused(java.awt.Component[]) // the following method may return null while switching focus between popups: // KeyboardFocusManager.getCurrentKeyboardFocusManager().getFocusOwner() // http://docs.oracle.com/javase/7/docs/webnotes/tsg/TSG-Desktop/html/awt.html#gdabn boolean notFocused = !JBPopupFactory.getInstance().isChildPopupFocused(e.getComponent()); if (myNotFocused && notFocused) { // hide all hints if a child popup is not focused now // and if it was not focused 200 milliseconds ago hideAllHints(); } // TODO: find a way to replace this hack with com.intellij.openapi.wm.IdeFocusManager myNotFocused = notFocused; } }, 200); } @Override public void focusGained(FocusEvent e) { myHideAlarm.cancelAllRequests(); } }; myEditorDocumentListener = new DocumentListener() { @Override public void documentChanged(DocumentEvent event) { LOG.assertTrue(SwingUtilities.isEventDispatchThread()); if (event.getOldLength() == 0 && event.getNewLength() == 0) return; HintInfo[] infos = getHintsStackArray(); for (HintInfo info : infos) { if (BitUtil.isSet(info.flags, HIDE_BY_TEXT_CHANGE)) { if (info.hint.isVisible()) { info.hint.hide(); } myHintsStack.remove(info); } } if (myHintsStack.isEmpty()) { updateLastEditor(null); } } }; } @Override public boolean isHint(Window component) { return myHintsStack.contains(component); } @NotNull private HintInfo[] getHintsStackArray() { return myHintsStack.toArray(new HintInfo[myHintsStack.size()]); } public boolean performCurrentQuestionAction() { ApplicationManager.getApplication().assertIsDispatchThread(); if (myQuestionAction != null && myQuestionHint != null) { if (myQuestionHint.isVisible()) { if (LOG.isDebugEnabled()) { LOG.debug("performing an action:" + myQuestionAction); } if (myQuestionAction.execute()) { if (myQuestionHint != null) { myQuestionHint.hide(); } } return true; } myQuestionAction = null; myQuestionHint = null; } return false; } private void updateScrollableHints(VisibleAreaEvent e) { LOG.assertTrue(SwingUtilities.isEventDispatchThread()); for (HintInfo info : getHintsStackArray()) { if (info.hint != null && BitUtil.isSet(info.flags, UPDATE_BY_SCROLLING)) { updateScrollableHintPosition(e, info.hint, BitUtil.isSet(info.flags, HIDE_IF_OUT_OF_EDITOR)); } } } @Override public boolean hasShownHintsThatWillHideByOtherHint(boolean willShowTooltip) { LOG.assertTrue(SwingUtilities.isEventDispatchThread()); for (HintInfo hintInfo : getHintsStackArray()) { if (hintInfo.hint.isVisible() && BitUtil.isSet(hintInfo.flags, HIDE_BY_OTHER_HINT)) return true; if (willShowTooltip && hintInfo.hint.isAwtTooltip()) { // only one AWT tooltip can be visible, so this hint will hide even though it's not marked with HIDE_BY_OTHER_HINT return true; } } return false; } @Override public void dispose() { ActionManagerEx.getInstanceEx().removeAnActionListener(myAnActionListener); } private static void updateScrollableHintPosition(VisibleAreaEvent e, LightweightHint hint, boolean hideIfOutOfEditor) { if (hint.getComponent() instanceof ScrollAwareHint) { ((ScrollAwareHint)hint.getComponent()).editorScrolled(); } if (!hint.isVisible()) return; Editor editor = e.getEditor(); if (!editor.getComponent().isShowing() || editor.isOneLineMode()) return; Rectangle newRectangle = e.getOldRectangle(); Rectangle oldRectangle = e.getNewRectangle(); Point location = hint.getLocationOn(editor.getContentComponent()); Dimension size = hint.getSize(); int xOffset = location.x - oldRectangle.x; int yOffset = location.y - oldRectangle.y; location = new Point(newRectangle.x + xOffset, newRectangle.y + yOffset); Rectangle newBounds = new Rectangle(location.x, location.y, size.width, size.height); final boolean okToUpdateBounds = hideIfOutOfEditor ? oldRectangle.contains(newBounds) : oldRectangle.intersects(newBounds); if (okToUpdateBounds || hint.vetoesHiding()) { hint.setLocation(new RelativePoint(editor.getContentComponent(), location)); } else { hint.hide(); } } /** * In this method the point to show hint depends on current caret position. * So, first of all, editor will be scrolled to make the caret position visible. */ public void showEditorHint(final LightweightHint hint, final Editor editor, @PositionFlags final short constraint, @HideFlags final int flags, final int timeout, final boolean reviveOnEditorChange) { editor.getScrollingModel().scrollToCaret(ScrollType.MAKE_VISIBLE); editor.getScrollingModel().runActionOnScrollingFinished(() -> { LogicalPosition pos = editor.getCaretModel().getLogicalPosition(); Point p = getHintPosition(hint, editor, pos, constraint); showEditorHint(hint, editor, p, flags, timeout, reviveOnEditorChange, createHintHint(editor, p, hint, constraint)); }); } /** * @param p point in layered pane coordinate system. */ public void showEditorHint(@NotNull final LightweightHint hint, @NotNull Editor editor, @NotNull Point p, @HideFlags int flags, int timeout, boolean reviveOnEditorChange) { showEditorHint(hint, editor, p, flags, timeout, reviveOnEditorChange, HintManager.ABOVE); } public void showEditorHint(@NotNull final LightweightHint hint, @NotNull Editor editor, @NotNull Point p, @HideFlags int flags, int timeout, boolean reviveOnEditorChange, @PositionFlags short position) { showEditorHint(hint, editor, p, flags, timeout, reviveOnEditorChange, createHintHint(editor, p, hint, position)); } public void showEditorHint(@NotNull final LightweightHint hint, @NotNull Editor editor, @NotNull Point p, @HideFlags int flags, int timeout, boolean reviveOnEditorChange, HintHint hintInfo) { LOG.assertTrue(SwingUtilities.isEventDispatchThread()); myHideAlarm.cancelAllRequests(); hideHints(HIDE_BY_OTHER_HINT, false, false); if (editor != myLastEditor) { hideAllHints(); } if (!ApplicationManager.getApplication().isUnitTestMode() && !editor.getContentComponent().isShowing()) return; if (!ApplicationManager.getApplication().isActive()) return; updateLastEditor(editor); getPublisher().hintShown(editor.getProject(), hint, flags); Component component = hint.getComponent(); // Set focus to control so that screen readers will announce the tooltip contents. // Users can press "ESC" to return to the editor. if (ScreenReader.isActive()) { hintInfo.setRequestFocus(true); } doShowInGivenLocation(hint, editor, p, hintInfo, true); ListenerUtil.addMouseListener(component, new MouseAdapter() { @Override public void mousePressed(MouseEvent e) { myHideAlarm.cancelAllRequests(); } }); ListenerUtil.addFocusListener(component, new FocusAdapter() { @Override public void focusGained(FocusEvent e) { myHideAlarm.cancelAllRequests(); } }); if (BitUtil.isSet(flags, HIDE_BY_MOUSEOVER)) { ListenerUtil.addMouseMotionListener(component, new MouseMotionAdapter() { @Override public void mouseMoved(MouseEvent e) { hideHints(HIDE_BY_MOUSEOVER, true, false); } }); } myHintsStack.add(new HintInfo(hint, flags, reviveOnEditorChange)); if (timeout > 0) { Timer timer = UIUtil.createNamedTimer("Hint timeout", timeout, event -> hint.hide()); timer.setRepeats(false); timer.start(); } } @Override public void showHint(@NotNull final JComponent component, @NotNull RelativePoint p, int flags, int timeout) { LOG.assertTrue(SwingUtilities.isEventDispatchThread()); myHideAlarm.cancelAllRequests(); hideHints(HIDE_BY_OTHER_HINT, false, false); final JBPopup popup = JBPopupFactory.getInstance().createComponentPopupBuilder(component, null).setRequestFocus(false).setResizable(false).setMovable(false) .createPopup(); popup.show(p); ListenerUtil.addMouseListener(component, new MouseAdapter() { @Override public void mousePressed(MouseEvent e) { myHideAlarm.cancelAllRequests(); } }); ListenerUtil.addFocusListener(component, new FocusAdapter() { @Override public void focusGained(FocusEvent e) { myHideAlarm.cancelAllRequests(); } }); final HintInfo info = new HintInfo(new LightweightHint(component) { @Override public void hide() { popup.cancel(); } }, flags, false); myHintsStack.add(info); if (timeout > 0) { Timer timer = UIUtil.createNamedTimer("Popup timeout", timeout, event -> Disposer.dispose(popup)); timer.setRepeats(false); timer.start(); } } private static void doShowInGivenLocation(final LightweightHint hint, final Editor editor, Point p, HintHint hintInfo, boolean updateSize) { if (ApplicationManager.getApplication().isUnitTestMode()) return; JComponent externalComponent = getExternalComponent(editor); Dimension size = updateSize ? hint.getComponent().getPreferredSize() : hint.getComponent().getSize(); if (hint.isRealPopup()) { final Point editorCorner = editor.getComponent().getLocation(); SwingUtilities.convertPointToScreen(editorCorner, externalComponent); final Point point = new Point(p); SwingUtilities.convertPointToScreen(point, externalComponent); final Rectangle editorScreen = ScreenUtil.getScreenRectangle(point.x, point.y); SwingUtilities.convertPointToScreen(p, externalComponent); final Rectangle rectangle = new Rectangle(p, size); ScreenUtil.moveToFit(rectangle, editorScreen, null); p = rectangle.getLocation(); SwingUtilities.convertPointFromScreen(p, externalComponent); } else if (externalComponent.getWidth() < p.x + size.width && !hintInfo.isAwtTooltip()) { p.x = Math.max(0, externalComponent.getWidth() - size.width); } if (hint.isVisible()) { if (updateSize) { hint.pack(); } hint.updatePosition(hintInfo.getPreferredPosition()); hint.updateLocation(p.x, p.y); } else { hint.show(externalComponent, p.x, p.y, editor.getContentComponent(), hintInfo); } } public static void updateLocation(final LightweightHint hint, final Editor editor, Point p) { doShowInGivenLocation(hint, editor, p, createHintHint(editor, p, hint, UNDER), false); } public static void adjustEditorHintPosition(final LightweightHint hint, final Editor editor, final Point p, @PositionFlags short constraint) { doShowInGivenLocation(hint, editor, p, createHintHint(editor, p, hint, constraint), true); } @Override public void hideAllHints() { LOG.assertTrue(SwingUtilities.isEventDispatchThread()); for (HintInfo info : getHintsStackArray()) { if (!info.hint.vetoesHiding()) { info.hint.hide(); } } cleanup(); } public void cleanup() { myHintsStack.clear(); updateLastEditor(null); } /** * @return coordinates in layered pane coordinate system. */ public Point getHintPosition(@NotNull LightweightHint hint, @NotNull Editor editor, @PositionFlags short constraint) { LogicalPosition pos = editor.getCaretModel().getLogicalPosition(); final DataContext dataContext = ((EditorEx)editor).getDataContext(); final Rectangle dominantArea = PlatformDataKeys.DOMINANT_HINT_AREA_RECTANGLE.getData(dataContext); LOG.assertTrue(SwingUtilities.isEventDispatchThread()); if (dominantArea != null) { return getHintPositionRelativeTo(hint, editor, constraint, dominantArea, pos); } JRootPane rootPane = editor.getComponent().getRootPane(); if (rootPane != null) { JLayeredPane lp = rootPane.getLayeredPane(); for (HintInfo info : getHintsStackArray()) { if (!info.hint.isSelectingHint()) continue; IdeTooltip tooltip = info.hint.getCurrentIdeTooltip(); if (tooltip != null) { Point p = tooltip.getShowingPoint().getPoint(lp); if (info.hint != hint) { switch (constraint) { case ABOVE: if (tooltip.getPreferredPosition() == Balloon.Position.below) { p.y -= tooltip.getPositionChangeY(); } break; case UNDER: case RIGHT_UNDER: if (tooltip.getPreferredPosition() == Balloon.Position.above) { p.y += tooltip.getPositionChangeY(); } break; case RIGHT: if (tooltip.getPreferredPosition() == Balloon.Position.atLeft) { p.x += tooltip.getPositionChangeX(); } break; case LEFT: if (tooltip.getPreferredPosition() == Balloon.Position.atRight) { p.x -= tooltip.getPositionChangeX(); } break; } } return p; } Rectangle rectangle = info.hint.getBounds(); JComponent c = info.hint.getComponent(); rectangle = SwingUtilities.convertRectangle(c.getParent(), rectangle, lp); return getHintPositionRelativeTo(hint, editor, constraint, rectangle, pos); } } return getHintPosition(hint, editor, pos, constraint); } private static Point getHintPositionRelativeTo(@NotNull final LightweightHint hint, @NotNull final Editor editor, @PositionFlags short constraint, @NotNull final Rectangle lookupBounds, final LogicalPosition pos) { JComponent externalComponent = getExternalComponent(editor); IdeTooltip ideTooltip = hint.getCurrentIdeTooltip(); if (ideTooltip != null) { Point point = ideTooltip.getPoint(); return SwingUtilities.convertPoint(ideTooltip.getComponent(), point, externalComponent); } Dimension hintSize = hint.getComponent().getPreferredSize(); int layeredPaneHeight = externalComponent.getHeight(); switch (constraint) { case LEFT: { int y = lookupBounds.y; if (y < 0) { y = 0; } else if (y + hintSize.height >= layeredPaneHeight) { y = layeredPaneHeight - hintSize.height; } return new Point(lookupBounds.x - hintSize.width, y); } case RIGHT: int y = lookupBounds.y; if (y < 0) { y = 0; } else if (y + hintSize.height >= layeredPaneHeight) { y = layeredPaneHeight - hintSize.height; } return new Point(lookupBounds.x + lookupBounds.width, y); case ABOVE: Point posAboveCaret = getHintPosition(hint, editor, pos, ABOVE); return new Point(lookupBounds.x, Math.min(posAboveCaret.y, lookupBounds.y - hintSize.height)); case UNDER: Point posUnderCaret = getHintPosition(hint, editor, pos, UNDER); return new Point(lookupBounds.x, Math.max(posUnderCaret.y, lookupBounds.y + lookupBounds.height)); default: LOG.error(""); return null; } } /** * @return position of hint in layered pane coordinate system */ public static Point getHintPosition(@NotNull LightweightHint hint, @NotNull Editor editor, @NotNull LogicalPosition pos, @PositionFlags short constraint) { return getHintPosition(hint, editor, pos, pos, constraint); } private static Point getHintPosition(@NotNull LightweightHint hint, @NotNull Editor editor, @NotNull LogicalPosition pos1, @NotNull LogicalPosition pos2, @PositionFlags short constraint) { return getHintPosition(hint, editor, pos1, pos2, constraint, Registry.is("editor.balloonHints")); } private static Point getHintPosition(@NotNull LightweightHint hint, @NotNull Editor editor, @NotNull LogicalPosition pos1, @NotNull LogicalPosition pos2, @PositionFlags short constraint, boolean showByBalloon) { if (ApplicationManager.getApplication().isUnitTestMode()) return new Point(); Point p = _getHintPosition(hint, editor, pos1, pos2, constraint, showByBalloon); JComponent externalComponent = getExternalComponent(editor); Dimension hintSize = hint.getComponent().getPreferredSize(); if (constraint == ABOVE) { if (p.y < 0) { Point p1 = _getHintPosition(hint, editor, pos1, pos2, UNDER, showByBalloon); if (p1.y + hintSize.height <= externalComponent.getSize().height) { return p1; } } } else if (constraint == UNDER) { if (p.y + hintSize.height > externalComponent.getSize().height) { Point p1 = _getHintPosition(hint, editor, pos1, pos2, ABOVE, showByBalloon); if (p1.y >= 0) { return p1; } } } return p; } @NotNull public static JComponent getExternalComponent(@NotNull Editor editor) { JComponent externalComponent = editor.getComponent(); JRootPane rootPane = externalComponent.getRootPane(); if (rootPane == null) return externalComponent; JLayeredPane layeredPane = rootPane.getLayeredPane(); return layeredPane != null ? layeredPane : rootPane; } private static Point _getHintPosition(@NotNull LightweightHint hint, @NotNull Editor editor, @NotNull LogicalPosition pos1, @NotNull LogicalPosition pos2, @PositionFlags short constraint, boolean showByBalloon) { Dimension hintSize = hint.getComponent().getPreferredSize(); Point location; JComponent externalComponent = getExternalComponent(editor); JComponent internalComponent = editor.getContentComponent(); if (constraint == RIGHT_UNDER) { Point p = editor.logicalPositionToXY(pos2); if (!showByBalloon) { p.y += editor.getLineHeight(); } location = SwingUtilities.convertPoint(internalComponent, p, externalComponent); } else { Point p = editor.logicalPositionToXY(pos1); if (constraint == UNDER) { p.y += editor.getLineHeight(); } location = SwingUtilities.convertPoint(internalComponent, p, externalComponent); } if (constraint == ABOVE && !showByBalloon) { location.y -= hintSize.height; int diff = location.x + hintSize.width - externalComponent.getWidth(); if (diff > 0) { location.x = Math.max(location.x - diff, 0); } } if ((constraint == LEFT || constraint == RIGHT) && !showByBalloon) { location.y -= hintSize.height / 2; if (constraint == LEFT) { location.x -= hintSize.width; } } return location; } @Override public void showErrorHint(@NotNull Editor editor, @NotNull String text) { showErrorHint(editor, text, ABOVE); } @Override public void showErrorHint(@NotNull Editor editor, @NotNull String text, short position) { JComponent label = HintUtil.createErrorLabel(text); LightweightHint hint = new LightweightHint(label); Point p = getHintPosition(hint, editor, position); showEditorHint(hint, editor, p, HIDE_BY_ANY_KEY | HIDE_BY_TEXT_CHANGE | HIDE_BY_SCROLLING, 0, false, position); } @Override public void showInformationHint(@NotNull Editor editor, @NotNull String text) { JComponent label = HintUtil.createInformationLabel(text); showInformationHint(editor, label); } @Override public void showInformationHint(@NotNull Editor editor, @NotNull JComponent component) { // Set the accessible name so that screen readers announce the panel type (e.g. "Hint panel") // when the tooltip gets the focus. AccessibleContextUtil.setName(component, "Hint"); showInformationHint(editor, component, true); } public void showInformationHint(@NotNull Editor editor, @NotNull JComponent component, boolean showByBalloon) { if (ApplicationManager.getApplication().isUnitTestMode()) { return; } LightweightHint hint = new LightweightHint(component); Point p = getHintPosition(hint, editor, ABOVE); showEditorHint(hint, editor, p, HIDE_BY_ANY_KEY | HIDE_BY_TEXT_CHANGE | HIDE_BY_SCROLLING, 0, false); } @Override public void showErrorHint(@NotNull Editor editor, @NotNull String hintText, int offset1, int offset2, short constraint, int flags, int timeout) { JComponent label = HintUtil.createErrorLabel(hintText); LightweightHint hint = new LightweightHint(label); final LogicalPosition pos1 = editor.offsetToLogicalPosition(offset1); final LogicalPosition pos2 = editor.offsetToLogicalPosition(offset2); final Point p = getHintPosition(hint, editor, pos1, pos2, constraint); showEditorHint(hint, editor, p, flags, timeout, false); } @Override public void showQuestionHint(@NotNull Editor editor, @NotNull String hintText, int offset1, int offset2, @NotNull QuestionAction action) { JComponent label = HintUtil.createQuestionLabel(hintText); LightweightHint hint = new LightweightHint(label); showQuestionHint(editor, offset1, offset2, hint, action, ABOVE); } public void showQuestionHint(@NotNull final Editor editor, final int offset1, final int offset2, @NotNull final LightweightHint hint, @NotNull final QuestionAction action, @PositionFlags short constraint) { final LogicalPosition pos1 = editor.offsetToLogicalPosition(offset1); final LogicalPosition pos2 = editor.offsetToLogicalPosition(offset2); final Point p = getHintPosition(hint, editor, pos1, pos2, constraint); showQuestionHint(editor, p, offset1, offset2, hint, action, constraint); } public void showQuestionHint(@NotNull final Editor editor, @NotNull final Point p, final int offset1, final int offset2, @NotNull final LightweightHint hint, @NotNull final QuestionAction action, @PositionFlags short constraint) { ApplicationManager.getApplication().assertIsDispatchThread(); hideQuestionHint(); TextAttributes attributes = new TextAttributes(); attributes.setEffectColor(HintUtil.QUESTION_UNDERSCORE_COLOR); attributes.setEffectType(EffectType.LINE_UNDERSCORE); final RangeHighlighter highlighter = editor.getMarkupModel() .addRangeHighlighter(offset1, offset2, HighlighterLayer.ERROR + 1, attributes, HighlighterTargetArea.EXACT_RANGE); hint.addHintListener(new HintListener() { @Override public void hintHidden(EventObject event) { hint.removeHintListener(this); highlighter.dispose(); if (myQuestionHint == hint) { myQuestionAction = null; myQuestionHint = null; } } }); showEditorHint(hint, editor, p, HIDE_BY_ANY_KEY | HIDE_BY_TEXT_CHANGE | UPDATE_BY_SCROLLING | HIDE_IF_OUT_OF_EDITOR | DONT_CONSUME_ESCAPE, 0, false, createHintHint(editor, p, hint, constraint)); myQuestionAction = action; myQuestionHint = hint; } private void hideQuestionHint() { ApplicationManager.getApplication().assertIsDispatchThread(); if (myQuestionHint != null) { myQuestionHint.hide(); myQuestionHint = null; myQuestionAction = null; } } public static HintHint createHintHint(Editor editor, Point p, LightweightHint hint, @PositionFlags short constraint) { return createHintHint(editor, p, hint, constraint, false); } //todo[nik,kirillk] perhaps 'createInEditorComponent' parameter should always be 'true' //old 'createHintHint' method uses LayeredPane as original component for HintHint so IdeTooltipManager.eventDispatched() //wasn't able to correctly hide tooltip after mouse move. public static HintHint createHintHint(Editor editor, Point p, LightweightHint hint, @PositionFlags short constraint, boolean createInEditorComponent) { JRootPane rootPane = editor.getComponent().getRootPane(); if (rootPane == null) { return new HintHint(editor, p); } JLayeredPane lp = rootPane.getLayeredPane(); HintHint hintInfo = new HintHint(editor, SwingUtilities.convertPoint(lp, p, editor.getContentComponent())); boolean showByBalloon = Registry.is("editor.balloonHints"); if (showByBalloon) { if (!createInEditorComponent) { hintInfo = new HintHint(lp, p); } hintInfo.setAwtTooltip(true).setHighlighterType(true); } hintInfo.initStyleFrom(hint.getComponent()); if (showByBalloon) { hintInfo.setBorderColor(new JBColor(Color.gray, Gray._140)); hintInfo.setFont(hintInfo.getTextFont().deriveFont(Font.PLAIN)); hintInfo.setCalloutShift((int)(editor.getLineHeight() * 0.1)); } hintInfo.setPreferredPosition(Balloon.Position.above); if (constraint == UNDER || constraint == RIGHT_UNDER) { hintInfo.setPreferredPosition(Balloon.Position.below); } else if (constraint == RIGHT) { hintInfo.setPreferredPosition(Balloon.Position.atRight); } else if (constraint == LEFT) { hintInfo.setPreferredPosition(Balloon.Position.atLeft); } if (hint.isAwtTooltip()) { hintInfo.setAwtTooltip(true); } hintInfo.setPositionChangeShift(0, editor.getLineHeight()); return hintInfo; } private void updateLastEditor(final Editor editor) { if (myLastEditor != editor) { if (myLastEditor != null) { myLastEditor.removeEditorMouseListener(myEditorMouseListener); myLastEditor.getContentComponent().removeFocusListener(myEditorFocusListener); myLastEditor.getDocument().removeDocumentListener(myEditorDocumentListener); myLastEditor.getScrollingModel().removeVisibleAreaListener(myVisibleAreaListener); myLastEditor.getCaretModel().removeCaretListener(myCaretMoveListener); } myLastEditor = editor; if (myLastEditor != null) { myLastEditor.addEditorMouseListener(myEditorMouseListener); myLastEditor.getContentComponent().addFocusListener(myEditorFocusListener); myLastEditor.getDocument().addDocumentListener(myEditorDocumentListener); myLastEditor.getScrollingModel().addVisibleAreaListener(myVisibleAreaListener); myLastEditor.getCaretModel().addCaretListener(myCaretMoveListener); } } } private class MyAnActionListener implements AnActionListener { @Override public void beforeActionPerformed(AnAction action, DataContext dataContext, AnActionEvent event) { if (action instanceof ActionToIgnore) return; AnAction escapeAction = ActionManagerEx.getInstanceEx().getAction(IdeActions.ACTION_EDITOR_ESCAPE); if (action == escapeAction) return; hideHints(HIDE_BY_ANY_KEY, false, false); } } /** * Hides all hints when selected editor changes. Unfortunately user can change * selected editor by mouse. These clicks are not AnActions so they are not * fired by ActionManager. */ private final class MyEditorManagerListener implements FileEditorManagerListener { @Override public void selectionChanged(@NotNull FileEditorManagerEvent event) { hideHints(0, false, true); } } /** * We have to spy for all opened projects to register MyEditorManagerListener into * all opened projects. */ private final class MyProjectManagerListener implements ProjectManagerListener { @Override public void projectOpened(Project project) { project.getMessageBus().connect().subscribe(FileEditorManagerListener.FILE_EDITOR_MANAGER, myEditorManagerListener); } @Override public void projectClosed(Project project) { ApplicationManager.getApplication().assertIsDispatchThread(); // avoid leak through com.intellij.codeInsight.hint.TooltipController.myCurrentTooltip TooltipController.getInstance().cancelTooltips(); ApplicationManager.getApplication().invokeLater(() -> hideHints(0, false, false)); myQuestionAction = null; myQuestionHint = null; if (myLastEditor != null && project == myLastEditor.getProject()) { updateLastEditor(null); } } } boolean isEscapeHandlerEnabled() { LOG.assertTrue(SwingUtilities.isEventDispatchThread()); for (int i = myHintsStack.size() - 1; i >= 0; i--) { final HintInfo info = myHintsStack.get(i); if (!info.hint.isVisible()) { myHintsStack.remove(i); // We encountered situation when 'hint' instances use 'hide()' method as object destruction callback // (e.g. LineTooltipRenderer creates hint that overrides keystroke of particular action that produces hint and // de-registers it inside 'hide()'. That means that the hint can 'stuck' to old editor location if we just remove // it but don't call hide()) info.hint.hide(); continue; } if ((info.flags & (HIDE_BY_ESCAPE | HIDE_BY_ANY_KEY)) != 0) { return true; } } return false; } @Override public boolean hideHints(int mask, boolean onlyOne, boolean editorChanged) { LOG.assertTrue(SwingUtilities.isEventDispatchThread()); try { boolean done = false; for (int i = myHintsStack.size() - 1; i >= 0; i--) { final HintInfo info = myHintsStack.get(i); if (!info.hint.isVisible() && !info.hint.vetoesHiding()) { myHintsStack.remove(i); // We encountered situation when 'hint' instances use 'hide()' method as object destruction callback // (e.g. LineTooltipRenderer creates hint that overrides keystroke of particular action that produces hint and // de-registers it inside 'hide()'. That means that the hint can 'stuck' to old editor location if we just remove // it but don't call hide()) info.hint.hide(); continue; } if ((info.flags & mask) != 0 || editorChanged && !info.reviveOnEditorChange) { info.hint.hide(); myHintsStack.remove(info); if ((mask & HIDE_BY_ESCAPE) == 0 || (info.flags & DONT_CONSUME_ESCAPE) == 0) { if (onlyOne) { return true; } done = true; } } } return done; } finally { if (myHintsStack.isEmpty()) { updateLastEditor(null); } } } private static class EditorHintListenerHolder { private static final EditorHintListener ourEditorHintPublisher = ApplicationManager.getApplication().getMessageBus().syncPublisher(EditorHintListener.TOPIC); private EditorHintListenerHolder() { } } private static EditorHintListener getPublisher() { return EditorHintListenerHolder.ourEditorHintPublisher; } }