Display popup over RangeHighlighter on mouseover

Answered

I have added some RangeHighlighters to the editor and would like display some information in a popup when the user mouse overs the highlighter.

Something like this or similiar:

 

Is there a way to achieve this?  Thanks in advance!

 

26 comments
Comment actions Permalink

If your highlighter is generated from HighlightInfo, you can simply specify tooltip/description for it.
There's also com.intellij.codeInsight.preview.PreviewHintProvider that works on "Shift + mouse move" (ex: to show color by its defiinition).

Otherwise, you might need to register EditorMouseMotionListener and EditorMouseListener and show popup when EditorMouseEvent.getLogicalPosition points at your highlighter.
<editorFactoryMouseListener implementation="com.MyPluginListener"/>
<editorFactoryMouseMotionListener implementation="com.MyPluginListener"/>

You can use ImageOrColorPreviewManager and EditorMouseHoverPopupManager as an example.

0
Comment actions Permalink

Thanks Aleksey Pivovarov!

I'm currently adding my highlighter the following way:

RangeHighlighter highlighter = editor.getMarkupModel().addRangeHighlighter(startoffset, endoffset, 0, new TextAttributes(JBColor.black, JBColor.WHITE, JBColor.PINK, EffectType.ROUNDED_BOX, 13), HighlighterTargetArea.EXACT_RANGE);
highlighter.setErrorStripeMarkColor(JBColor.RED);
highlighter.setErrorStripeTooltip("some string");

Is it possible to use both RangeHighlighter for ErrorStripeTooltip and HighlightInfo for tooltip/description in the editor? Basically I want to display the same content set in the ErrorStripeTooltip when I mouse hover over highlighted range in the editor. So I want to have 2 tooltips (the error stripe tooltip, which is already there, and a tooltip over the range itself).

Hope I could describe this understandable.

0
Comment actions Permalink

HighlightInfo is created from 'com.intellij.lang.annotation.Annotation' that are created by Annotators. It can't be used together with 'addRangeHighlighter'.

0
Comment actions Permalink

So what would be most convenient way be to display a popup over the highlighted range in the editor and also keep the ErrorStripeTooltip added with the RangeHighlighter?
Should I use an Annotator?

0
Comment actions Permalink

https://jetbrains.org/intellij/sdk/docs/tutorials/custom_language_support/annotator.html

If annotator fits your purpose, yes, you're most likely should use an annotator.

0
Comment actions Permalink
 @Override
  public void annotate(@NotNull final PsiElement element, @NotNull AnnotationHolder holder) {
   
}

Is there a way to check whether a PsiElement is highlighted? I'm asking because I just want to anotate the PsiElements which are already highlighted by RangeHighlighter.

0
Comment actions Permalink

Could you explain what you're trying to highlight in more details? It's hard to suggest anything without knowing how/why highlighting should be created and when it should be updated (file changes, referenced file changes, etc).

>Is there a way to check whether a PsiElement is highlighted?
No, annotators should not rely on other highlighters. Only on the Psi code structure.

0
Comment actions Permalink

I have integrated a tool in my plugin which checks for cyclic dependencies in the source code of a project. This tool gives me the start- and endoffset of elements in a document. I use RangeHighlighter to highlight them and show them to the user. In the update function of my plugin action I'm removing the highlights.

Currently it looks like this:

What I want to achieve additionally is to show a tooltip/popup when the user mouse hover over the rounded box with basically the same content in the errorstripe tooltip.

 

0
Comment actions Permalink

Is this an external tool or your own plugin? If it's external tool, consider using com.intellij.lang.annotation.ExternalAnnotator

0
Comment actions Permalink

It's a jar file which I integrated in my own plugin

0
Comment actions Permalink

It is hard to suggest a solution without seeing the whole code: who calculates what and when and how and why do updates get applied to the editor. Could you share a link to your repo?

0
Comment actions Permalink

All the calculation, highlighting, etc. is being done in those two classes:


https://pastebin.com/yqPTVXTy

https://pastebin.com/AJX47P2F

0
Comment actions Permalink

Did you consider using https://www.jetbrains.com/help/idea/2020.2/dsm-tool-window.html? AFAIU it should do the same as your plugin

0
Comment actions Permalink

No because it's a university project and I have to do this using the tool they provided me (Bachelor thesis)

0
Comment actions Permalink

Aleksey Pivovarov

I would like to use the approach with EditorMouseMotionListener and EditorMouseListener and show popup when EditorMouseEvent.getLogicalPosition points at my highlighters.

Which class, method, interface etc. should I use to create the popup and display it?

0
Comment actions Permalink

>Which class, method, interface etc. should I use to create the popup and display it?

Probably, JBPopupFactory.getInstance().createComponentPopupBuilder(...) ... .createPopup().showInBestPositionFor(editor).
https://jetbrains.org/intellij/sdk/docs/user_interface_components/popups.html

0
Comment actions Permalink

Ok, thanks!

I can't find the method EditorMouseEvent.getLogicalPosition. Are you referring to EditorMouseEvent.getMouseEvent().getLocationOnScreen()? Or am I missing something?

0
Comment actions Permalink

This method is new (since 2020.2), it's a cached version of "Editor.xyToLogicalPosition(e.getMouseEvent().getPoint())".
You can use the latter for compatibility with older IDEs.

0
Comment actions Permalink

Thanks!

Sorry for asking so many questions but now I have the LogicalPosition. How can I check wheter this position is between the start- and endoffset of RangeHighlighter.
Something like this maybe?

LogicalPosition pos = e.getEditor().xyToLogicalPosition(e.getMouseEvent().getPoint());
for(RangeHighlighter highlighter : myHighLighters){
int startOffsetOfHighlighter = highlighter.getStartOffset();
int endOffsetOfHighlighter = highlighter.getEndOffset();
if(startOffsetOfHighlighter <= pos && pos < endOffsetOfHighlighter) //something like this?
}
0
Comment actions Permalink

RangeHighlighter start/end offsets are "number of symbols since the start of file".
You can convert LogicalPosition to offset using Editor.logicalPositionToOffset.

See also https://jetbrains.org/intellij/sdk/docs/tutorials/editor_basics/coordinates_system.html

0
Comment actions Permalink
public void mouseMoved(@NotNull EditorMouseEvent e) {
if (ignoreEvent(e)) return;

for(RangeHighlighter highlighter : editorHighlighters){
JBPopupFactory.getInstance().createComponentPopupBuilder(e.getEditor().getComponent(),e.getEditor().getComponent())
.setTitle(highlighter.getErrorStripeTooltip().toString()).createPopup().showInBestPositionFor(e.getEditor());
}
}

}

JBPopupFactory.getInstance().createComponentPopupBuilder(...)... throws following exception:

2020-08-19 15:04:27,520 [  14808]  ERROR - llij.ide.plugins.PluginManager - Editor must be showing on the screen 
java.lang.AssertionError: Editor must be showing on the screen
at com.intellij.ui.popup.AbstractPopup.showInBestPositionFor(AbstractPopup.java:544)
at HoverPopupManager.mouseMoved(HoverPopupManager.java:36)
at com.intellij.openapi.editor.impl.event.EditorEventMulticasterImpl$3.lambda$mouseMoved$0(EditorEventMulticasterImpl.java:96)
at com.intellij.openapi.extensions.impl.ExtensionProcessingHelper.forEachExtensionSafe(ExtensionProcessingHelper.java:21)
at com.intellij.openapi.extensions.ExtensionPointName.forEachExtensionSafe(ExtensionPointName.java:50)
at com.intellij.openapi.editor.impl.event.EditorEventMulticasterImpl$3.mouseMoved(EditorEventMulticasterImpl.java:96)
at com.intellij.openapi.editor.impl.EditorImpl$MyMouseMotionListener.mouseMoved(EditorImpl.java:4188)
at java.desktop/java.awt.Component.processMouseMotionEvent(Component.java:6696)
at java.desktop/javax.swing.JComponent.processMouseMotionEvent(JComponent.java:3360)
at java.desktop/java.awt.Component.processEvent(Component.java:6420)
at java.desktop/java.awt.Container.processEvent(Container.java:2263)
at java.desktop/java.awt.Component.dispatchEventImpl(Component.java:5026)
at java.desktop/java.awt.Container.dispatchEventImpl(Container.java:2321)
at java.desktop/java.awt.Component.dispatchEvent(Component.java:4858)
at java.desktop/java.awt.LightweightDispatcher.retargetMouseEvent(Container.java:4918)
at java.desktop/java.awt.LightweightDispatcher.processMouseEvent(Container.java:4560)
at java.desktop/java.awt.LightweightDispatcher.dispatchEvent(Container.java:4488)
at java.desktop/java.awt.Container.dispatchEventImpl(Container.java:2307)
at java.desktop/java.awt.Window.dispatchEventImpl(Window.java:2773)
at java.desktop/java.awt.Component.dispatchEvent(Component.java:4858)
at java.desktop/java.awt.EventQueue.dispatchEventImpl(EventQueue.java:778)
at java.desktop/java.awt.EventQueue$4.run(EventQueue.java:727)
at java.desktop/java.awt.EventQueue$4.run(EventQueue.java:721)
at java.base/java.security.AccessController.doPrivileged(Native Method)
at java.base/java.security.ProtectionDomain$JavaSecurityAccessImpl.doIntersectionPrivilege(ProtectionDomain.java:85)
at java.base/java.security.ProtectionDomain$JavaSecurityAccessImpl.doIntersectionPrivilege(ProtectionDomain.java:95)
at java.desktop/java.awt.EventQueue$5.run(EventQueue.java:751)
at java.desktop/java.awt.EventQueue$5.run(EventQueue.java:749)
at java.base/java.security.AccessController.doPrivileged(Native Method)
at java.base/java.security.ProtectionDomain$JavaSecurityAccessImpl.doIntersectionPrivilege(ProtectionDomain.java:85)
at java.desktop/java.awt.EventQueue.dispatchEvent(EventQueue.java:748)
at com.intellij.ide.IdeEventQueue.defaultDispatchEvent(IdeEventQueue.java:908)
at com.intellij.ide.IdeEventQueue.dispatchMouseEvent(IdeEventQueue.java:846)
at com.intellij.ide.IdeEventQueue._dispatchEvent(IdeEventQueue.java:778)
at com.intellij.ide.IdeEventQueue.lambda$dispatchEvent$8(IdeEventQueue.java:424)
at com.intellij.openapi.progress.impl.CoreProgressManager.computePrioritized(CoreProgressManager.java:698)
at com.intellij.ide.IdeEventQueue.dispatchEvent(IdeEventQueue.java:423)
at java.desktop/java.awt.EventDispatchThread.pumpOneEventForFilters(EventDispatchThread.java:203)
at java.desktop/java.awt.EventDispatchThread.pumpEventsForFilter(EventDispatchThread.java:124)
at java.desktop/java.awt.EventDispatchThread.pumpEventsForHierarchy(EventDispatchThread.java:113)
at java.desktop/java.awt.EventDispatchThread.pumpEvents(EventDispatchThread.java:109)
at java.desktop/java.awt.EventDispatchThread.pumpEvents(EventDispatchThread.java:101)
at java.desktop/java.awt.EventDispatchThread.run(EventDispatchThread.java:90)

The content in the editor suddenly disappears.

0
Comment actions Permalink
createComponentPopupBuilder(e.getEditor().getComponent(),e.getEditor().getComponent())


This line takes Editor component and tries to show in in a popup. Swing does not allow to show same component twice, so editor is removed from original place (and it breaks popup showing logic later - see trace).

You can use createComponentPopupBuilder(new JLabel("text"), null) instead (and replace label with w/e should be shown in the popup).

0
Comment actions Permalink

Note that "showInBestPositionFor" is likely to show popup near caret, rather than at current mouse position.
You can use "showInScreenCoordinates" or "show(RelativePoint)" and "new RelativePoint(editor.getComponent(), mousePosition);" to show it at mouse position.
Or use Editor.logicalPositionToXY/offsetToXY to show it near start/end of the RangeHighlighter.

0
Comment actions Permalink

Thank you very much! I will hava a look at it!

0
Comment actions Permalink

>Editor.logicalPositionToXY/offsetToXY
These are absolute numbers. So you might need to adjust for scrollbar position by substracting Editor.getScrollingModel.getVerticalScrollOffset()/getHorizontalScrollOffset.

0
Comment actions Permalink

I have managed to make it run. I would like the popup to be instantly canceled/removed if I move the mouse away from my RangeHighlighter.

@Override
public void mouseEntered(@NotNull EditorMouseEvent event) {
// we receive MOUSE_MOVED event after MOUSE_ENTERED even if mouse wasn't physically moved,
// e.g. if a popup overlapping editor has been closed
skipMovement();
}

@Override
public void mouseExited(@NotNull EditorMouseEvent event) {
clearPopups(event);
}


@Override
public void mousePressed(@NotNull EditorMouseEvent event) {
clearPopups(event);
}

private void clearPopups(@NotNull EditorMouseEvent event) {
List<JBPopup> popups = JBPopupFactory.getInstance().getChildPopups(event.getEditor().getComponent());
for (JBPopup popup : popups) {
popup.cancel();
}
}

private void skipMovement(){
skipMovement = true;
}

Am I missing something? Thanks in advance!

0

Please sign in to leave a comment.