| /* |
| * Copyright 2000-2014 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.event.*; |
| import com.intellij.openapi.editor.event.DocumentAdapter; |
| import com.intellij.openapi.editor.ex.EditorEx; |
| import com.intellij.openapi.editor.markup.*; |
| import com.intellij.openapi.fileEditor.FileEditorManagerAdapter; |
| 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.ProjectManagerAdapter; |
| 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.registry.Registry; |
| import com.intellij.ui.*; |
| import com.intellij.ui.awt.RelativePoint; |
| import com.intellij.util.Alarm; |
| import com.intellij.util.ui.UIUtil; |
| 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 = null; |
| private QuestionAction myQuestionAction = null; |
| |
| private final List<HintInfo> myHintsStack = new ArrayList<HintInfo>(); |
| private Editor myLastEditor = null; |
| 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 CaretAdapter() { |
| @Override |
| public void caretPositionChanged(CaretEvent e) { |
| hideHints(HIDE_BY_ANY_KEY, false, false); |
| } |
| }; |
| |
| projectManager.addProjectManagerListener(new MyProjectManagerListener()); |
| |
| myEditorMouseListener = new EditorMouseAdapter() { |
| @Override |
| public void mousePressed(EditorMouseEvent event) { |
| hideAllHints(); |
| } |
| }; |
| |
| myVisibleAreaListener = new VisibleAreaListener() { |
| @Override |
| public void visibleAreaChanged(VisibleAreaEvent e) { |
| updateScrollableHints(e); |
| 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() { |
| @Override |
| public void run() { |
| if (!JBPopupFactory.getInstance().isChildPopupFocused(e.getComponent())) { |
| hideAllHints(); |
| } |
| } |
| }, 200); |
| } |
| |
| @Override |
| public void focusGained(FocusEvent e) { |
| myHideAlarm.cancelAllRequests(); |
| } |
| }; |
| |
| myEditorDocumentListener = new DocumentAdapter() { |
| @Override |
| public void documentChanged(DocumentEvent event) { |
| LOG.assertTrue(SwingUtilities.isEventDispatchThread()); |
| HintInfo[] infos = getHintsStackArray(); |
| for (HintInfo info : infos) { |
| if ((info.flags & HIDE_BY_TEXT_CHANGE) != 0) { |
| if (info.hint.isVisible()) { |
| info.hint.hide(); |
| } |
| myHintsStack.remove(info); |
| } |
| } |
| |
| if (myHintsStack.isEmpty()) { |
| updateLastEditor(null); |
| } |
| } |
| }; |
| } |
| |
| @NotNull |
| private HintInfo[] getHintsStackArray() { |
| return myHintsStack.toArray(new HintInfo[myHintsStack.size()]); |
| } |
| |
| public boolean performCurrentQuestionAction() { |
| 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 && (info.flags & UPDATE_BY_SCROLLING) != 0) { |
| updateScrollableHintPosition(e, info.hint, (info.flags & HIDE_IF_OUT_OF_EDITOR) != 0); |
| } |
| } |
| } |
| |
| @Override |
| public boolean hasShownHintsThatWillHideByOtherHint(boolean willShowTooltip) { |
| LOG.assertTrue(SwingUtilities.isEventDispatchThread()); |
| for (HintInfo hintInfo : getHintsStackArray()) { |
| if (hintInfo.hint.isVisible() && (hintInfo.flags & HIDE_BY_OTHER_HINT) != 0) 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(); |
| } |
| } |
| |
| public void showEditorHint(LightweightHint hint, Editor editor, @PositionFlags short constraint, @HideFlags int flags, int timeout, boolean reviveOnEditorChange) { |
| 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. |
| * @param reviveOnEditorChange |
| */ |
| 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(); |
| |
| 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 ((flags & HIDE_BY_MOUSEOVER) != 0) { |
| 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, new ActionListener() { |
| @Override |
| public void actionPerformed(ActionEvent 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, new ActionListener() { |
| @Override |
| public void actionPerformed(ActionEvent event) { |
| popup.dispose(); |
| } |
| }); |
| 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; |
| JLayeredPane layeredPane = editor.getComponent().getRootPane().getLayeredPane(); |
| Dimension size = updateSize ? hint.getComponent().getPreferredSize() : hint.getComponent().getSize(); |
| |
| if (hint.isRealPopup()) { |
| final Point editorCorner = editor.getComponent().getLocation(); |
| SwingUtilities.convertPointToScreen(editorCorner, layeredPane); |
| final Point point = new Point(p); |
| SwingUtilities.convertPointToScreen(point, layeredPane); |
| final Rectangle editorScreen = ScreenUtil.getScreenRectangle(point.x, point.y); |
| |
| SwingUtilities.convertPointToScreen(p, layeredPane); |
| final Rectangle rectangle = new Rectangle(p, size); |
| ScreenUtil.moveToFit(rectangle, editorScreen, null); |
| p = rectangle.getLocation(); |
| SwingUtilities.convertPointFromScreen(p, layeredPane); |
| } |
| else if (layeredPane.getWidth() < p.x + size.width && !hintInfo.isAwtTooltip()) { |
| p.x = Math.max(0, layeredPane.getWidth() - size.width); |
| } |
| |
| if (hint.isVisible()) { |
| if (updateSize) { |
| hint.updateBounds(p.x, p.y); |
| } else { |
| hint.updateLocation(p.x, p.y); |
| } |
| } |
| else { |
| hint.show(layeredPane, 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) { |
| JLayeredPane lp = editor.getComponent().getRootPane().getLayeredPane(); |
| |
| 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) { |
| 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); |
| |
| if (rectangle != null) { |
| return getHintPositionRelativeTo(hint, editor, constraint, rectangle, pos); |
| } |
| } |
| } |
| else { |
| return getHintPositionRelativeTo(hint, editor, constraint, dominantArea, pos); |
| } |
| |
| return getHintPosition(hint, editor, pos, constraint); |
| } |
| |
| private static Point getHintPositionRelativeTo(final LightweightHint hint, |
| final Editor editor, |
| @PositionFlags short constraint, |
| final Rectangle lookupBounds, |
| final LogicalPosition pos) { |
| |
| JComponent editorComponent = editor.getComponent(); |
| JLayeredPane layeredPane = editorComponent.getRootPane().getLayeredPane(); |
| |
| IdeTooltip ideTooltip = hint.getCurrentIdeTooltip(); |
| if (ideTooltip != null) { |
| Point point = ideTooltip.getPoint(); |
| return SwingUtilities.convertPoint(ideTooltip.getComponent(), point, layeredPane); |
| } |
| |
| Dimension hintSize = hint.getComponent().getPreferredSize(); |
| int layeredPaneHeight = layeredPane.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.assertTrue(false); |
| 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); |
| JLayeredPane layeredPane = editor.getComponent().getRootPane().getLayeredPane(); |
| 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 <= layeredPane.getSize().height) { |
| return p1; |
| } |
| } |
| } |
| else if (constraint == UNDER) { |
| if (p.y + hintSize.height > layeredPane.getSize().height) { |
| Point p1 = _getHintPosition(hint, editor, pos1, pos2, ABOVE, showByBalloon); |
| if (p1.y >= 0) { |
| return p1; |
| } |
| } |
| } |
| |
| return p; |
| } |
| |
| 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(); |
| int line1 = pos1.line; |
| int col1 = pos1.column; |
| int line2 = pos2.line; |
| int col2 = pos2.column; |
| |
| Point location; |
| JLayeredPane layeredPane = editor.getComponent().getRootPane().getLayeredPane(); |
| JComponent internalComponent = editor.getContentComponent(); |
| if (constraint == RIGHT_UNDER) { |
| Point p = editor.logicalPositionToXY(new LogicalPosition(line2, col2)); |
| if (!showByBalloon) { |
| p.y += editor.getLineHeight(); |
| } |
| location = SwingUtilities.convertPoint(internalComponent, p, layeredPane); |
| } |
| else { |
| Point p = editor.logicalPositionToXY(new LogicalPosition(line1, col1)); |
| if (constraint == UNDER) { |
| p.y += editor.getLineHeight(); |
| } |
| location = SwingUtilities.convertPoint(internalComponent, p, layeredPane); |
| } |
| |
| if (constraint == ABOVE && !showByBalloon) { |
| location.y -= hintSize.height; |
| int diff = location.x + hintSize.width - layeredPane.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) { |
| 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) { |
| 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); |
| if (myQuestionHint != null) { |
| myQuestionHint.hide(); |
| myQuestionHint = null; |
| myQuestionAction = null; |
| } |
| |
| hint.addHintListener(new HintListener() { |
| @Override |
| public void hintHidden(EventObject event) { |
| highlighter.dispose(); |
| |
| if (myQuestionHint == hint) { |
| myQuestionAction = null; |
| myQuestionHint = null; |
| } |
| hint.removeHintListener(this); |
| } |
| }); |
| |
| showEditorHint(hint, editor, p, HIDE_BY_ANY_KEY | HIDE_BY_TEXT_CHANGE | UPDATE_BY_SCROLLING | HIDE_IF_OUT_OF_EDITOR, 0, false, |
| createHintHint(editor, p, hint, constraint)); |
| myQuestionAction = action; |
| myQuestionHint = hint; |
| } |
| |
| 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); |
| } |
| |
| |
| @Override |
| public void afterActionPerformed(final AnAction action, final DataContext dataContext, AnActionEvent event) { |
| } |
| |
| @Override |
| public void beforeEditorTyping(char c, DataContext dataContext) { |
| } |
| } |
| |
| /** |
| * 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 extends FileEditorManagerAdapter { |
| @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 extends ProjectManagerAdapter { |
| @Override |
| public void projectOpened(Project project) { |
| project.getMessageBus().connect(project).subscribe(FileEditorManagerListener.FILE_EDITOR_MANAGER, myEditorManagerListener); |
| } |
| |
| @Override |
| public void projectClosed(Project project) { |
| // avoid leak through com.intellij.codeInsight.hint.TooltipController.myCurrentTooltip |
| TooltipController.getInstance().cancelTooltips(); |
| |
| myQuestionAction = null; |
| myQuestionHint = 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 (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; |
| } |
| } |