Problem with GutterIconRenderer

Hello Jetbrain Community !

I am currently developing a plugin for SonarJ based on the Idea 9.0 snapshot and have problems with GutterIconRenderer and MarkupModel.

One of the features of the plugin allows the user to switch on/off error markers in the gutter for certain problems like architecture violations or metric warnings. This is controlled by a toggle action.

My class LineMarker implements GutterIconRenderer. Every LineMarker can represent several issues. The issue with the highest priority determines the icon to be shown and the error stripe color.

Here is an extract from the code. Most of the action happens in updateHighlighterAttributes(). If the most important issue changes, the current highlighter is removed and replaced by a new one. If the last issue for the line disappears, the highlighter is removed from the markup model.

Now unfortunately Idea calls the getIcon method even after the highlighter has been removed from the model and even after having called setGutterIconRenderer with null. So obviously the gutter implementation caches instances of GutterIconRenderer and the cache is not updated when the markup model is changed by removing highlighters. I worked around the problem by returning an empty icon, but this certainly looks like buggy behavior to me. After I removed a highlighter the associated GutterIconRenderer should not be called by Idea anymore.

I also noticed that the gutter width is only recalculated when new markers are added. Removing a marker does not cause a recalculation of the width although I think it should. (Because removing a marker could make the width shrink)

Or am I using the API in the wrong way. Any help is highly appreciated.

class LineMarker extends GutterIconRenderer
{
    private final MarkerVisibilityManager visibilityManager;
    private MarkupModel markupModel;
    private RangeHighlighter highlighter, oldHighlighter;
    private int line;
    private List<ProblemManager.Issue> issues = new ArrayList<ProblemManager.Issue>();
    private ProblemManager.Issue currentIssue = null;


    // ...
    @NotNull
    @Override
    public Icon getIcon()
    {
        if (currentIssue == null)
        {
            System.out.println("bad access: "+this.toString());
            return IconLoader.getIcon("/icons/empty.gif");
        }
        return currentIssue.getIssueClass().getIcon();
    }

    private void updateHighlighterAttributes()
    {
        if (markupModel != null)
        {
            if (currentIssue != null)
            {
                if (highlighter != null)
                {
                    markupModel.removeHighlighter(highlighter);
                }
                highlighter = markupModel.addLineHighlighter(line-1, HighlighterLayer.WARNING+1, null);
                highlighter.setGutterIconRenderer(this);
                highlighter.setErrorStripeMarkColor(getErrorStripeColor());
                highlighter.setErrorStripeTooltip(new ToolTipFetcher());
            }
            else if (highlighter != null)
            {
                System.out.println("Highlighter of "+this.toString()+" is removed !");
                highlighter.setGutterIconRenderer(null);
                highlighter.setErrorStripeMarkColor(null);
                highlighter.setErrorStripeTooltip(null);
                markupModel.removeHighlighter(highlighter);
                oldHighlighter = highlighter;
                highlighter = null;
            }
        }
    }




    void showMarkerClass(ProblemManager.IssueClass cls)
    {
        ProblemManager.Issue toBeHighlighted = getHighlightedIssue();


        if (toBeHighlighted != currentIssue)
        {
            currentIssue = toBeHighlighted;
            updateHighlighterAttributes();
        }
    }


    void hideMarkerClass(ProblemManager.IssueClass cls)
    {
        if (currentIssue != null && currentIssue.getIssueClass() == cls)
        {
            currentIssue = getHighlightedIssue();
            updateHighlighterAttributes();
        }
    }
}
 

Message was edited by: Alexander von Zitzewitz to add Idea version

6 comments
Comment actions Permalink

Working with gutter renderers can be a bit tricky. The safest way in my opinion is to update markers in batch on a file level, e.g. first remove all (your) markers and then add the ones you want to be shown. As long as you build the markers from an in-memory model you should be good.

I did something similar not so long ago in the IntelliGuard plugin. The code can be found here: http://code.google.com/p/intelliguard/source/browse/
Perhaps com.googlecode.intelliguard.action.GutterAction and the classes in the com.googlecode.intelliguard.gutter package can give some inspiration.

0
Comment actions Permalink

Thank you - I will try that approach. I have an in memory model of all the markers, so it should be easy.

Still hoping for some official comment from JetBrains. Either there is a bug or I a missing something in the API.

0
Comment actions Permalink

Hello Alexander,

Our own code is built with the assumption that GutterIconRenderers are added
by inspection passes (for example, by means of LineMarkerProvider class).
The behavior you describe does look like a bug, and I think it's worth reporting
as a YouTrack issue.

Hello Jetbrain Community !

I am currently developing a plugin for SonarJ and have problems with
GutterIconRenderer and MarkupModel.

One of the features of the plugin allows the user to switch on/off
error markers in the gutter for certain problems like architecture
violations or metric warnings. This is controlled by a toggle action.

My class LineMarker implements GutterIconRenderer. Every LineMarker
can represent several issues. The issue with the highest priority
determines the icon to be shown and the error stripe color.

Here is an extract from the code. Most of the action happens in
updateHighlighterAttributes(). If the most important issue changes,
the current highlighter is removed and replaced by a new one. If the
last issue for the line disappears, the highlighter is removed from
the markup model.

Now unfortunately Idea calls the getIcon method even after the
highlighter has been removed from the model and even after having
called setGutterIconRenderer with null. So obviously the gutter
implementation caches instances of GutterIconRenderer and the cache is
not updated when the markup model is changed by removing highlighters.
I worked around the problem by returning an empty icon, but this
certainly looks like buggy behavior to me. After I removed a
highlighter the associated GutterIconRenderer should not be called by
Idea anymore.

I also noticed that the gutter width is only recalculated when new
markers are added. Removing a marker does not cause a recalculation of
the width although I think it should.

Or am I using the AP in the wring way. Any help is highly appreciated.

class LineMarker extends GutterIconRenderer
{
private final MarkerVisibilityManager visibilityManager;
private MarkupModel markupModel;
private RangeHighlighter highlighter, oldHighlighter;
private int line;
private List<ProblemManager.Issue> issues = new
ArrayList<ProblemManager.Issue>();
private ProblemManager.Issue currentIssue = null;
// ...
@NotNull
@Override
public Icon getIcon()
{
if (currentIssue == null)
{
System.out.println("bad access: "+this.toString());
return IconLoader.getIcon("/icons/empty.gif");
}
return currentIssue.getIssueClass().getIcon();
}
private void updateHighlighterAttributes()
{
if (markupModel != null)
{
if (currentIssue != null)
{
if (highlighter != null)
{
markupModel.removeHighlighter(highlighter);
}
highlighter = markupModel.addLineHighlighter(line-1,
HighlighterLayer.WARNING+1, null);
highlighter.setGutterIconRenderer(this);

highlighter.setErrorStripeMarkColor(getErrorStripeColor());
highlighter.setErrorStripeTooltip(new
ToolTipFetcher());
}
else if (highlighter != null)
{
System.out.println("Highlighter of "this.toString()"
is removed !");
highlighter.setGutterIconRenderer(null);
highlighter.setErrorStripeMarkColor(null);
highlighter.setErrorStripeTooltip(null);
markupModel.removeHighlighter(highlighter);
oldHighlighter = highlighter;
highlighter = null;
}
}
}
void showMarkerClass(ProblemManager.IssueClass cls)
{
ProblemManager.Issue toBeHighlighted = getHighlightedIssue();
if (toBeHighlighted != currentIssue)
{
currentIssue = toBeHighlighted;
updateHighlighterAttributes();
}
}
void hideMarkerClass(ProblemManager.IssueClass cls)
{
if (currentIssue != null && currentIssue.getIssueClass() ==
cls)
{
currentIssue = getHighlightedIssue();
updateHighlighterAttributes();
}
}
}
---
Original message URL:
http://devnet.jetbrains.net/message/5283155#5283155


--
Dmitry Jemerov
Development Lead
JetBrains, Inc.
http://www.jetbrains.com/
"Develop with Pleasure!"


0
Comment actions Permalink

Thanks, Dmitry !

The problem is that SonarJ's analysis is mainly based on the byte code, so the plugin runs as a validation compiler. Moreover I give the user the possibility to toggle marker visibility - the user can click the toggle button anytime to hide or show markers. How do I report that as a bug?

Best regards

Alexander

0
Comment actions Permalink

Hello Alexander,

To report this as a bug, please create an issue at http://youtrack.jetbrains.net/
in the IntelliJ IDEA project.

The problem is that SonarJ's analysis is mainly based on the byte
code, so the plugin runs as a validation compiler. Moreover I give the
user the possibility to toggle marker visibility - the user can click
the toggle button anytime to hide or show markers. How do I report
that as a bug?


--
Dmitry Jemerov
Development Lead
JetBrains, Inc.
http://www.jetbrains.com/
"Develop with Pleasure!"


0

Please sign in to leave a comment.