diff --git a/platform/lang-impl/src/com/intellij/codeInsight/documentation/DockablePopupManager.java b/platform/lang-impl/src/com/intellij/codeInsight/documentation/DockablePopupManager.java index ce7959ac07e8..b3bb889c8931 100644 --- a/platform/lang-impl/src/com/intellij/codeInsight/documentation/DockablePopupManager.java +++ b/platform/lang-impl/src/com/intellij/codeInsight/documentation/DockablePopupManager.java @@ -173,6 +173,10 @@ public abstract class DockablePopupManager { } } + public void resetAutoUpdateState() { + restartAutoUpdate(PropertiesComponent.getInstance().getBoolean(getAutoUpdateEnabledProperty(), getAutoUpdateDefault())); + } + public void updateComponent() { updateComponent(false); } diff --git a/platform/lang-impl/src/com/intellij/codeInsight/documentation/render/DocRenderItem.java b/platform/lang-impl/src/com/intellij/codeInsight/documentation/render/DocRenderItem.java index 5991ade0bc4a..a8971293f9bf 100644 --- a/platform/lang-impl/src/com/intellij/codeInsight/documentation/render/DocRenderItem.java +++ b/platform/lang-impl/src/com/intellij/codeInsight/documentation/render/DocRenderItem.java @@ -22,10 +22,15 @@ import com.intellij.openapi.editor.markup.HighlighterTargetArea; import com.intellij.openapi.editor.markup.RangeHighlighter; import com.intellij.openapi.project.DumbAware; import com.intellij.openapi.project.DumbAwareAction; +import com.intellij.openapi.project.Project; import com.intellij.openapi.util.Disposer; import com.intellij.openapi.util.Key; -import com.intellij.openapi.util.Segment; import com.intellij.openapi.util.TextRange; +import com.intellij.psi.PsiDocCommentBase; +import com.intellij.psi.PsiDocumentManager; +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiFile; +import com.intellij.psi.util.PsiTreeUtil; import com.intellij.util.messages.MessageBusConnection; import com.intellij.util.text.CharArrayUtil; import org.jetbrains.annotations.NotNull; @@ -47,7 +52,7 @@ class DocRenderItem { final RangeHighlighter highlighter; String textToRender; private FoldRegion foldRegion; - private Inlay inlay; + Inlay inlay; static boolean isValidRange(@NotNull Document document, @NotNull TextRange range) { CharSequence text = document.getImmutableCharSequence(); @@ -74,7 +79,7 @@ class DocRenderItem { boolean updated = false; for (Iterator it = items.iterator(); it.hasNext(); ) { DocRenderItem item = it.next(); - DocRenderPassFactory.Item matchingItem = item.isValid() ? itemsToSet.removeItem(item.getTextRange()) : null; + DocRenderPassFactory.Item matchingItem = item.isValid() ? itemsToSet.removeItem(item.highlighter) : null; if (matchingItem == null) { updated |= item.remove(foldingTasks); it.remove(); @@ -133,6 +138,11 @@ class DocRenderItem { } }, connection); editor.getCaretModel().addCaretListener(new MyCaretListener(), connection); + + DocRenderMouseEventBridge mouseEventBridge = new DocRenderMouseEventBridge(); + editor.addEditorMouseListener(mouseEventBridge, connection); + editor.addEditorMouseMotionListener(mouseEventBridge, connection); + editor.putUserData(LISTENERS_DISPOSABLE, connection); } } @@ -153,10 +163,6 @@ class DocRenderItem { highlighter.setGutterIconRenderer(new MyGutterIconRenderer(toggleAction)); } - private Segment getTextRange() { - return highlighter; - } - private int calcFoldStartOffset() { Document document = highlighter.getDocument(); int startLine = document.getLineNumber(highlighter.getStartOffset()); @@ -261,6 +267,22 @@ class DocRenderItem { return true; } + PsiElement getOwner() { + Project project = editor.getProject(); + if (project != null && highlighter.isValid()) { + PsiDocumentManager psiDocumentManager = PsiDocumentManager.getInstance(project); + PsiFile file = psiDocumentManager.getPsiFile(editor.getDocument()); + if (file != null) { + PsiDocCommentBase comment = PsiTreeUtil.getParentOfType(file.findElementAt(highlighter.getStartOffset()), PsiDocCommentBase.class, + false); + if (comment != null) { + return comment.getOwner(); + } + } + } + return null; + } + private static void updateInlays(@NotNull Editor editor) { keepScrollingPositionWhile(editor, () -> { AtomicBoolean updated = new AtomicBoolean(); diff --git a/platform/lang-impl/src/com/intellij/codeInsight/documentation/render/DocRenderMouseEventBridge.java b/platform/lang-impl/src/com/intellij/codeInsight/documentation/render/DocRenderMouseEventBridge.java new file mode 100644 index 000000000000..ea6d235c6088 --- /dev/null +++ b/platform/lang-impl/src/com/intellij/codeInsight/documentation/render/DocRenderMouseEventBridge.java @@ -0,0 +1,91 @@ +// Copyright 2000-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. +package com.intellij.codeInsight.documentation.render; + +import com.intellij.openapi.editor.Editor; +import com.intellij.openapi.editor.Inlay; +import com.intellij.openapi.editor.event.EditorMouseEvent; +import com.intellij.openapi.editor.event.EditorMouseEventArea; +import com.intellij.openapi.editor.event.EditorMouseListener; +import com.intellij.openapi.editor.event.EditorMouseMotionListener; +import com.intellij.openapi.editor.ex.EditorEx; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import sun.awt.AWTAccessor; + +import javax.swing.*; +import java.awt.*; +import java.awt.event.MouseEvent; + +class DocRenderMouseEventBridge implements EditorMouseListener, EditorMouseMotionListener { + private Inlay myCurrentInlay; + + @Override + public void mouseMoved(@NotNull EditorMouseEvent event) { + if (event.getArea() != EditorMouseEventArea.EDITING_AREA) return; + + Inlay currentInlay = redispatchEvent(event.getEditor(), event.getMouseEvent(), MouseEvent.MOUSE_MOVED); + if (currentInlay == null) { + restoreCursor(); + } + else { + ((EditorEx)event.getEditor()).setCustomCursor(DocRenderMouseEventBridge.class, currentInlay.getRenderer().myPane.getCursor()); + if (currentInlay != myCurrentInlay) { + if (myCurrentInlay != null) { + dispatchMouseExitEvent(myCurrentInlay); + } + myCurrentInlay = currentInlay; + } + } + } + + @Override + public void mouseExited(@NotNull EditorMouseEvent event) { + if (event.getArea() != EditorMouseEventArea.EDITING_AREA) return; + + restoreCursor(); + } + + @Override + public void mouseClicked(@NotNull EditorMouseEvent event) { + if (event.getArea() != EditorMouseEventArea.EDITING_AREA) return; + + redispatchEvent(event.getEditor(), event.getMouseEvent(), MouseEvent.MOUSE_CLICKED); + } + + private void restoreCursor() { + if (myCurrentInlay != null) { + dispatchMouseExitEvent(myCurrentInlay); + ((EditorEx)myCurrentInlay.getEditor()).setCustomCursor(DocRenderMouseEventBridge.class, null); + myCurrentInlay = null; + } + } + + @Nullable + private static Inlay redispatchEvent(@NotNull Editor editor, @NotNull MouseEvent mouseEvent, int eventId) { + Point mousePoint = mouseEvent.getPoint(); + Inlay inlay = editor.getInlayModel().getElementAt(mousePoint, DocRenderer.class); + if (inlay != null) { + DocRenderer renderer = inlay.getRenderer(); + Point relativeLocation = renderer.getEditorPaneLocationWithinInlay(); + Rectangle inlayBounds = inlay.getBounds(); + assert inlayBounds != null; + int x = mousePoint.x - inlayBounds.x - relativeLocation.x; + int y = mousePoint.y - inlayBounds.y - relativeLocation.y; + JEditorPane editorPane = renderer.myPane; + if (x >= 0 && x < editorPane.getWidth() && y >= 0 && y < editorPane.getHeight()) { + dispatchEvent(inlay, new MouseEvent(editorPane, eventId, 0, 0, x, y, mouseEvent.getClickCount(), false, mouseEvent.getButton())); + return inlay; + } + } + return null; + } + + private static void dispatchEvent(@NotNull Inlay inlay, @NotNull MouseEvent event) { + DocRenderer renderer = inlay.getRenderer(); + renderer.doWithRepaintTracking(() -> AWTAccessor.getComponentAccessor().processEvent(renderer.myPane, event)); + } + + private static void dispatchMouseExitEvent(@NotNull Inlay inlay) { + dispatchEvent(inlay, new MouseEvent(inlay.getRenderer().myPane, MouseEvent.MOUSE_EXITED, 0, 0, 0, 0, 0, false)); + } +} diff --git a/platform/lang-impl/src/com/intellij/codeInsight/documentation/render/DocRenderer.java b/platform/lang-impl/src/com/intellij/codeInsight/documentation/render/DocRenderer.java index 4753e7b19d9b..01cc96b534c1 100644 --- a/platform/lang-impl/src/com/intellij/codeInsight/documentation/render/DocRenderer.java +++ b/platform/lang-impl/src/com/intellij/codeInsight/documentation/render/DocRenderer.java @@ -2,7 +2,10 @@ package com.intellij.codeInsight.documentation.render; import com.intellij.codeInsight.documentation.DocumentationComponent; +import com.intellij.codeInsight.documentation.DocumentationManager; +import com.intellij.codeInsight.documentation.QuickDocUtil; import com.intellij.ide.ui.UISettings; +import com.intellij.openapi.Disposable; import com.intellij.openapi.actionSystem.ActionGroup; import com.intellij.openapi.actionSystem.DefaultActionGroup; import com.intellij.openapi.editor.Document; @@ -10,14 +13,20 @@ import com.intellij.openapi.editor.Editor; import com.intellij.openapi.editor.EditorCustomElementRenderer; import com.intellij.openapi.editor.Inlay; import com.intellij.openapi.editor.colors.EditorColorsManager; +import com.intellij.openapi.editor.event.CaretEvent; +import com.intellij.openapi.editor.event.CaretListener; import com.intellij.openapi.editor.markup.GutterIconRenderer; import com.intellij.openapi.editor.markup.TextAttributes; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.util.Disposer; import com.intellij.openapi.util.Key; import com.intellij.openapi.util.registry.Registry; import com.intellij.openapi.util.text.StringUtil; +import com.intellij.psi.PsiElement; import com.intellij.ui.AppUIUtil; import com.intellij.ui.ColorUtil; import com.intellij.ui.JBColor; +import com.intellij.ui.popup.PopupFactoryImpl; import com.intellij.ui.scale.JBUIScale; import com.intellij.util.text.CharArrayUtil; import com.intellij.util.ui.GraphicsUtil; @@ -27,6 +36,9 @@ import com.intellij.util.ui.UIUtil; import org.jetbrains.annotations.NotNull; import javax.swing.*; +import javax.swing.event.HyperlinkEvent; +import javax.swing.text.BadLocationException; +import javax.swing.text.Element; import javax.swing.text.View; import javax.swing.text.html.ImageView; import java.awt.*; @@ -47,7 +59,8 @@ class DocRenderer implements EditorCustomElementRenderer { private static final int TOP_BOTTOM_INSETS = 8; private final DocRenderItem myItem; - private JEditorPane myPane; + private boolean myRepaintRequested; + JEditorPane myPane; DocRenderer(DocRenderItem item) { myItem = item; @@ -136,11 +149,20 @@ class DocRenderer implements EditorCustomElementRenderer { return Math.max(0, indentPixels - scale(LEFT_INSET)); } + Point getEditorPaneLocationWithinInlay() { + return new Point(calcInlayStartX() + scale(LEFT_INSET), scale(getTopMargin()) + scale(ARC_WIDTH)); + } + private JComponent getRendererComponent(Inlay inlay, int width, int height) { boolean newInstance = false; if (myPane == null || Boolean.TRUE.equals(inlay.getUserData(RECREATE_COMPONENT) != null)) { newInstance = true; - myPane = new JEditorPane(UIUtil.HTML_MIME, ""); + myPane = new JEditorPane(UIUtil.HTML_MIME, "") { + @Override + public void repaint(long tm, int x, int y, int width, int height) { + myRepaintRequested = true; + } + }; myPane.setEditable(false); myPane.putClientProperty("caretWidth", 0); // do not reserve space for caret (making content one pixel narrower than component) myPane.setEditorKit(createEditorKit()); @@ -152,6 +174,11 @@ class DocRenderer implements EditorCustomElementRenderer { myPane.setFont(myPane.getFont().deriveFont(fontAttributes)); myPane.setForeground(getColorFromRegistry("editor.render.doc.comments.fg")); myPane.setText(myItem.textToRender); + myPane.addHyperlinkListener(e -> { + if (e.getEventType() == HyperlinkEvent.EventType.ACTIVATED) { + activateLink(e); + } + }); inlay.putUserData(RECREATE_COMPONENT, null); } AppUIUtil.targetToDevice(myPane, inlay.getEditor().getContentComponent()); @@ -162,6 +189,52 @@ class DocRenderer implements EditorCustomElementRenderer { return myPane; } + private void activateLink(HyperlinkEvent event) { + Editor editor = myItem.editor; + Project project = editor.getProject(); + Element element = event.getSourceElement(); + if (project != null && element != null) { + Rectangle location = null; + try { + location = myPane.modelToView(element.getStartOffset()); + } + catch (BadLocationException ignored) {} + PsiElement owner = myItem.getOwner(); + if (owner != null && location != null) { + if (QuickDocUtil.getActiveDocComponent(project) == null) { + Point inlayPosition = Objects.requireNonNull(myItem.inlay.getBounds()).getLocation(); + Point relativePosition = getEditorPaneLocationWithinInlay(); + editor.putUserData(PopupFactoryImpl.ANCHOR_POPUP_POINT, + new Point(inlayPosition.x + relativePosition.x + location.x, + inlayPosition.y + relativePosition.y + location.y + location.height)); + } + DocumentationManager documentationManager = DocumentationManager.getInstance(project); + documentationManager.showJavaDocInfo(editor, owner, owner, () -> { + editor.putUserData(PopupFactoryImpl.ANCHOR_POPUP_POINT, null); + }, "", false, true); + DocumentationComponent component = QuickDocUtil.getActiveDocComponent(project); + if (component != null) { + component.startWait(); + documentationManager.navigateByLink(component, event.getDescription()); + } + if (documentationManager.getDocInfoHint() == null) { + editor.putUserData(PopupFactoryImpl.ANCHOR_POPUP_POINT, null); + } + if (documentationManager.hasActiveDockedDocWindow()) { + documentationManager.setAllowContentUpdateFromContext(false); + Disposable disposable = Disposer.newDisposable(); + editor.getCaretModel().addCaretListener(new CaretListener() { + @Override + public void caretPositionChanged(@NotNull CaretEvent e) { + documentationManager.resetAutoUpdateState(); + Disposer.dispose(disposable); + } + }, disposable); + } + } + } + } + private void trackImageUpdates(Inlay inlay) { myPane.getPreferredSize(); // trigger internal layout ImageObserver observer = (img, infoflags, x, y, width, height) -> { @@ -190,6 +263,15 @@ class DocRenderer implements EditorCustomElementRenderer { return result; } + void doWithRepaintTracking(Runnable task) { + myRepaintRequested = false; + task.run(); + Inlay inlay = myItem.inlay; + if (myRepaintRequested && inlay != null) { + inlay.repaint(); + } + } + private static JBHtmlEditorKit createEditorKit() { JBHtmlEditorKit editorKit = new JBHtmlEditorKit(true); String editorFontName = StringUtil.escapeQuotes(EditorColorsManager.getInstance().getGlobalScheme().getEditorFontName()); diff --git a/platform/platform-impl/src/com/intellij/openapi/editor/impl/EditorImpl.java b/platform/platform-impl/src/com/intellij/openapi/editor/impl/EditorImpl.java index 68940084d09b..7437e2efbc6f 100644 --- a/platform/platform-impl/src/com/intellij/openapi/editor/impl/EditorImpl.java +++ b/platform/platform-impl/src/com/intellij/openapi/editor/impl/EditorImpl.java @@ -3873,11 +3873,9 @@ public final class EditorImpl extends UserDataHolderBase implements EditorEx, Hi } } - private boolean inlayWithCustomContextMenuExists(@NotNull Point point) { + private boolean hasBlockInlay(@NotNull Point point) { Inlay inlay = myInlayModel.getElementAt(point); - if (inlay == null) return false; - EditorCustomElementRenderer renderer = inlay.getRenderer(); - return renderer.getContextMenuGroupId(inlay) != null || renderer.getContextMenuGroup(inlay) != null; + return inlay != null && (inlay.getPlacement() == Inlay.Placement.ABOVE_LINE || inlay.getPlacement() == Inlay.Placement.BELOW_LINE); } private boolean processMousePressed(@NotNull final MouseEvent e) { @@ -3951,8 +3949,7 @@ public final class EditorImpl extends UserDataHolderBase implements EditorEx, Hi // at 'line markers' area). Also, don't move caret when context menu for an inlay is invoked. boolean moveCaret = eventArea == EditorMouseEventArea.LINE_NUMBERS_AREA || isInsideGutterWhitespaceArea(e) || - eventArea == EditorMouseEventArea.EDITING_AREA && - !(e.getButton() == MouseEvent.BUTTON3 && inlayWithCustomContextMenuExists(e.getPoint())); + eventArea == EditorMouseEventArea.EDITING_AREA && !hasBlockInlay(e.getPoint()); if (moveCaret) { VisualPosition visualPosition = getTargetPosition(x, y, true); LogicalPosition pos = visualToLogicalPosition(visualPosition); diff --git a/platform/platform-impl/src/com/intellij/ui/popup/PopupFactoryImpl.java b/platform/platform-impl/src/com/intellij/ui/popup/PopupFactoryImpl.java index 8707b7628827..7a73f8581cd5 100644 --- a/platform/platform-impl/src/com/intellij/ui/popup/PopupFactoryImpl.java +++ b/platform/platform-impl/src/com/intellij/ui/popup/PopupFactoryImpl.java @@ -60,6 +60,13 @@ public class PopupFactoryImpl extends JBPopupFactory { * Primary intention for this key is to hint popup position for the non-caret location. */ public static final Key ANCHOR_POPUP_POSITION = Key.create("popup.anchor.position"); + /** + * If corresponding value is defined for an {@link Editor}, popups shown for the editor will be located at specified point. This allows to + * show popups for non-default locations (caret location is used by default). + * + * @see JBPopupFactory#guessBestPopupLocation(Editor) + */ + public static final Key ANCHOR_POPUP_POINT = Key.create("popup.anchor.point"); private static final Logger LOG = Logger.getInstance(PopupFactoryImpl.class); @@ -563,25 +570,27 @@ public class PopupFactoryImpl extends JBPopupFactory { @Nullable private static Point getVisibleBestPopupLocation(@NotNull Editor editor) { - VisualPosition visualPosition = editor.getUserData(ANCHOR_POPUP_POSITION); + int lineHeight = editor.getLineHeight(); + Point p = editor.getUserData(ANCHOR_POPUP_POINT); + if (p == null) { + VisualPosition visualPosition = editor.getUserData(ANCHOR_POPUP_POSITION); - if (visualPosition == null) { - CaretModel caretModel = editor.getCaretModel(); - if (caretModel.isUpToDate()) { - visualPosition = caretModel.getVisualPosition(); - } - else { - visualPosition = editor.offsetToVisualPosition(caretModel.getOffset()); + if (visualPosition == null) { + CaretModel caretModel = editor.getCaretModel(); + if (caretModel.isUpToDate()) { + visualPosition = caretModel.getVisualPosition(); + } + else { + visualPosition = editor.offsetToVisualPosition(caretModel.getOffset()); + } } + + p = editor.visualPositionToXY(visualPosition); + p.y += lineHeight; } - final int lineHeight = editor.getLineHeight(); - Point p = editor.visualPositionToXY(visualPosition); - p.y += lineHeight; - final Rectangle visibleArea = editor.getScrollingModel().getVisibleArea(); - return !visibleArea.contains(p) && !visibleArea.contains(p.x, p.y - lineHeight) - ? null : p; + return !visibleArea.contains(p) && !visibleArea.contains(p.x, p.y - lineHeight) ? null : p; } @Override