/* * 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. */ /* * Created by IntelliJ IDEA. * User: max * Date: Jun 6, 2002 * Time: 8:37:03 PM * To change template for new class use * Code Style | Class Templates options (Tools | IDE Options). */ package com.intellij.openapi.editor.impl; import com.intellij.codeInsight.daemon.GutterMark; import com.intellij.codeInsight.hint.TooltipController; import com.intellij.codeInsight.hint.TooltipGroup; import com.intellij.ide.IdeEventQueue; import com.intellij.ide.dnd.*; import com.intellij.ide.ui.UISettings; import com.intellij.ide.ui.customization.CustomActionsSchema; import com.intellij.openapi.actionSystem.*; import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.application.impl.ApplicationImpl; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.editor.*; import com.intellij.openapi.editor.colors.ColorKey; import com.intellij.openapi.editor.colors.EditorColors; import com.intellij.openapi.editor.colors.EditorColorsScheme; import com.intellij.openapi.editor.colors.EditorFontType; import com.intellij.openapi.editor.event.EditorMouseEventArea; import com.intellij.openapi.editor.ex.*; import com.intellij.openapi.editor.ex.util.EditorUtil; import com.intellij.openapi.editor.markup.*; import com.intellij.openapi.project.DumbAwareAction; import com.intellij.openapi.ui.popup.Balloon; import com.intellij.openapi.util.Comparing; import com.intellij.openapi.util.Ref; import com.intellij.openapi.util.SystemInfo; import com.intellij.ui.HintHint; import com.intellij.ui.JBColor; import com.intellij.ui.awt.RelativePoint; import com.intellij.util.Function; import com.intellij.util.IconUtil; import com.intellij.util.NullableFunction; import com.intellij.util.SmartList; import com.intellij.util.containers.HashMap; import com.intellij.util.ui.UIUtil; import gnu.trove.*; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import javax.swing.*; import javax.swing.plaf.ComponentUI; import java.awt.*; import java.awt.event.*; import java.awt.geom.AffineTransform; import java.util.*; import java.util.List; class EditorGutterComponentImpl extends EditorGutterComponentEx implements MouseListener, MouseMotionListener { private static final Logger LOG = Logger.getInstance("#com.intellij.openapi.editor.impl.EditorGutterComponentImpl"); private static final int START_ICON_AREA_WIDTH = 15; private static final int FREE_PAINTERS_AREA_WIDTH = 5; private static final int GAP_BETWEEN_ICONS = 3; private static final TooltipGroup GUTTER_TOOLTIP_GROUP = new TooltipGroup("GUTTER_TOOLTIP_GROUP", 0); private static final Color COLOR_F0F0 = new Color(0xF0F0F0); public static final TIntFunction ID = new TIntFunction() { @Override public int execute(int value) { return value; } }; private final EditorImpl myEditor; private final FoldingAnchorsOverlayStrategy myAnchorsDisplayStrategy; private int myLineMarkerAreaWidth = START_ICON_AREA_WIDTH + FREE_PAINTERS_AREA_WIDTH; private int myIconsAreaWidth = START_ICON_AREA_WIDTH; private int myLineNumberAreaWidth = 0; private FoldRegion myActiveFoldRegion; private boolean myPopupInvokedOnPressed; private int myTextAnnotationGuttersSize = 0; private int myTextAnnotationExtraSize = 0; private TIntArrayList myTextAnnotationGutterSizes = new TIntArrayList(); private ArrayList myTextAnnotationGutters = new ArrayList(); private final Map myProviderToListener = new HashMap(); private static final int GAP_BETWEEN_ANNOTATIONS = 6; private Color myBackgroundColor = null; private String myLastGutterToolTip = null; private int myLastPreferredHeight = -1; @NotNull private TIntFunction myLineNumberConvertor; private boolean myShowDefaultGutterPopup = true; @SuppressWarnings("unchecked") public EditorGutterComponentImpl(EditorImpl editor) { myEditor = editor; myLineNumberConvertor = ID; if (!ApplicationManager.getApplication().isHeadlessEnvironment()) { installDnD(); } setOpaque(true); myAnchorsDisplayStrategy = new FoldingAnchorsOverlayStrategy(editor); } @SuppressWarnings({"ConstantConditions"}) private void installDnD() { DnDSupport.createBuilder(this) .setBeanProvider(new Function() { @Override public DnDDragStartBean fun(DnDActionInfo info) { final GutterMark renderer = getGutterRenderer(info.getPoint()); return renderer != null && (info.isCopy() || info.isMove()) ? new DnDDragStartBean(renderer) : null; } }) .setDropHandler(new DnDDropHandler() { @Override public void drop(DnDEvent e) { final Object attachedObject = e.getAttachedObject(); if (attachedObject instanceof GutterIconRenderer) { final GutterDraggableObject draggableObject = ((GutterIconRenderer)attachedObject).getDraggableObject(); if (draggableObject != null) { final int line = convertPointToLineNumber(e.getPoint()); if (line != -1) { draggableObject.copy(line, myEditor.getVirtualFile()); } } } } }) .setImageProvider(new NullableFunction() { @Override public DnDImage fun(DnDActionInfo info) { return new DnDImage(IconUtil.toImage(getGutterRenderer(info.getPoint()).getIcon())); } }) .install(); } private void fireResized() { processComponentEvent(new ComponentEvent(this, ComponentEvent.COMPONENT_RESIZED)); } @Override public Dimension getPreferredSize() { int w = getLineNumberAreaWidth() + getAnnotationsAreaWidthEx() + getLineMarkerAreaWidth() + getFoldingAreaWidth(); return new Dimension(w, myLastPreferredHeight); } @Override protected void setUI(ComponentUI newUI) { super.setUI(newUI); reinitSettings(); } @Override public void updateUI() { super.updateUI(); reinitSettings(); } public void reinitSettings() { myBackgroundColor = null; revalidateMarkup(); repaint(); } @Override public void paint(Graphics g) { ((ApplicationImpl)ApplicationManager.getApplication()).editorPaintStart(); try { Rectangle clip = g.getClipBounds(); if (clip.height < 0) return; final Graphics2D g2 = (Graphics2D)g; final AffineTransform old = g2.getTransform(); if (isMirrored()) { final AffineTransform transform = new AffineTransform(old); transform.scale(-1, 1); transform.translate(-getWidth(), 0); g2.setTransform(transform); } UISettings.setupAntialiasing(g); paintLineNumbersBackground(g, clip); paintAnnotations(g, clip); Object hint = g2.getRenderingHint(RenderingHints.KEY_ANTIALIASING); if (!UIUtil.isRetina()) g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_OFF); try { int firstVisibleOffset = myEditor.logicalPositionToOffset(myEditor.xyToLogicalPosition(new Point(0, clip.y - myEditor.getLineHeight()))); int lastVisibleOffset = myEditor.logicalPositionToOffset(myEditor.xyToLogicalPosition(new Point(0, clip.y + clip.height + myEditor.getLineHeight()))); paintFoldingBackground(g, clip); paintFoldingLines((Graphics2D)g, clip); paintLineMarkers(g, clip, firstVisibleOffset, lastVisibleOffset); paintFoldingTree(g, clip, firstVisibleOffset, lastVisibleOffset); paintLineNumbers(g, clip); } finally { g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, hint); } g2.setTransform(old); } finally { ((ApplicationImpl)ApplicationManager.getApplication()).editorPaintFinish(); } } private void processClose(final MouseEvent e) { final IdeEventQueue queue = IdeEventQueue.getInstance(); // See IDEA-59553 for rationale on why this feature is disabled //if (isLineNumbersShown()) { // if (e.getX() >= getLineNumberAreaOffset() && getLineNumberAreaOffset() + getLineNumberAreaWidth() >= e.getX()) { // queue.blockNextEvents(e); // myEditor.getSettings().setLineNumbersShown(false); // e.consume(); // return; // } //} if (getGutterRenderer(e) != null) return; int x = getAnnotationsAreaOffset(); for (int i = 0; i < myTextAnnotationGutters.size(); i++) { final int size = myTextAnnotationGutterSizes.get(i); if (x <= e.getX() && e.getX() <= x + size + GAP_BETWEEN_ANNOTATIONS) { queue.blockNextEvents(e); closeAllAnnotations(); e.consume(); break; } x += size + GAP_BETWEEN_ANNOTATIONS; } } private void paintAnnotations(Graphics g, Rectangle clip) { int x = getAnnotationsAreaOffset(); int w = getAnnotationsAreaWidthEx(); if (w == 0) return; final Color background = myEditor.isInDistractionFreeMode() ? myEditor.getBackgroundColor() : getBackground(); paintBackground(g, clip, getAnnotationsAreaOffset(), w, background); Color color = myEditor.getColorsScheme().getColor(EditorColors.ANNOTATIONS_COLOR); g.setColor(color != null ? color : JBColor.blue); g.setFont(myEditor.getColorsScheme().getFont(EditorFontType.PLAIN)); for (int i = 0; i < myTextAnnotationGutters.size(); i++) { TextAnnotationGutterProvider gutterProvider = myTextAnnotationGutters.get(i); int lineHeight = myEditor.getLineHeight(); int startLineNumber = clip.y / lineHeight; int endLineNumber = (clip.y + clip.height) / lineHeight + 1; int lastLine = myEditor.logicalToVisualPosition( new LogicalPosition(endLineNumber(), 0)) .line; endLineNumber = Math.min(endLineNumber, lastLine + 1); if (startLineNumber >= endLineNumber) { break; } for (int j = startLineNumber; j < endLineNumber; j++) { int logLine = myEditor.visualToLogicalPosition(new VisualPosition(j, 0)).line; String s = gutterProvider.getLineText(logLine, myEditor); final EditorFontType style = gutterProvider.getStyle(logLine, myEditor); final Color bg = gutterProvider.getBgColor(logLine, myEditor); if (bg != null) { g.setColor(bg); g.fillRect(x, j * lineHeight, w, lineHeight); } g.setColor(myEditor.getColorsScheme().getColor(gutterProvider.getColor(logLine, myEditor))); g.setFont(myEditor.getColorsScheme().getFont(style)); if (s != null) { g.drawString(s, x, (j+1) * lineHeight - myEditor.getDescent()); } } x += myTextAnnotationGutterSizes.get(i); } if (!myEditor.isInDistractionFreeMode()) { UIUtil.drawVDottedLine((Graphics2D)g, getAnnotationsAreaOffset() + w - 1, clip.y, clip.y + clip.height, null, getOutlineColor(false)); } } private void paintFoldingTree(Graphics g, Rectangle clip, int firstVisibleOffset, int lastVisibleOffset) { if (isFoldingOutlineShown()) { doPaintFoldingTree((Graphics2D)g, clip, firstVisibleOffset, lastVisibleOffset); } else { UIUtil.drawVDottedLine((Graphics2D)g, clip.x + clip.width - 1, clip.y, clip.y + clip.height, null, getOutlineColor(false)); } } private void paintLineMarkers(Graphics g, Rectangle clip, int firstVisibleOffset, int lastVisibleOffset) { if (isLineMarkersShown()) { paintBackground(g, clip, getLineMarkerAreaOffset(), getLineMarkerAreaWidth()); paintGutterRenderers(g, firstVisibleOffset, lastVisibleOffset); } } private void paintBackground(final Graphics g, final Rectangle clip, final int x, final int width) { paintBackground(g, clip, x, width, getBackground()); } private void paintBackground(final Graphics g, final Rectangle clip, final int x, final int width, Color background) { g.setColor(background); g.fillRect(x, clip.y, width, clip.height); paintCaretRowBackground(g, x, width); } private void paintCaretRowBackground(final Graphics g, final int x, final int width) { final VisualPosition visCaret = myEditor.getCaretModel().getVisualPosition(); Color caretRowColor = myEditor.getColorsScheme().getColor(EditorColors.CARET_ROW_COLOR); if (caretRowColor != null) { g.setColor(caretRowColor); final Point caretPoint = myEditor.visualPositionToXY(visCaret); g.fillRect(x, caretPoint.y, width, myEditor.getLineHeight()); } } private void paintLineNumbers(Graphics g, Rectangle clip) { if (isLineNumbersShown()) { int x = getLineNumberAreaOffset() + getLineNumberAreaWidth() - 2; UIUtil.drawVDottedLine((Graphics2D)g, x, clip.y, clip.y + clip.height, null, getOutlineColor(false)); doPaintLineNumbers(g, clip); } } private void paintLineNumbersBackground(Graphics g, Rectangle clip) { if (isLineNumbersShown()) { paintBackground(g, clip, getLineNumberAreaOffset(), getLineNumberAreaWidth()); } } @Override public Color getBackground() { if (myBackgroundColor == null) { EditorColorsScheme colorsScheme = myEditor.getColorsScheme(); boolean distractionMode = myEditor.isInDistractionFreeMode(); Color color = distractionMode ? colorsScheme.getDefaultBackground() : colorsScheme.getColor(EditorColors.GUTTER_BACKGROUND); myBackgroundColor = color == null ? COLOR_F0F0 : color; } return myBackgroundColor; } private void doPaintLineNumbers(Graphics g, Rectangle clip) { if (!isLineNumbersShown()) { return; } int lineHeight = myEditor.getLineHeight(); int startLineNumber = clip.y / lineHeight; int endLineNumber = (clip.y + clip.height) / lineHeight + 1; int lastLine = myEditor.logicalToVisualPosition( new LogicalPosition(endLineNumber(), 0)) .line; endLineNumber = Math.min(endLineNumber, lastLine + 1); if (startLineNumber >= endLineNumber) { return; } Color color = myEditor.getColorsScheme().getColor(EditorColors.LINE_NUMBERS_COLOR); g.setColor(color != null ? color : JBColor.blue); g.setFont(myEditor.getColorsScheme().getFont(EditorFontType.PLAIN)); Graphics2D g2 = (Graphics2D)g; AffineTransform old = g2.getTransform(); if (isMirrored()) { AffineTransform originalTransform = new AffineTransform(old); originalTransform.scale(-1, 1); originalTransform.translate(-getLineNumberAreaWidth() + 2, 0); g2.setTransform(originalTransform); } for (int i = startLineNumber; i < endLineNumber; i++) { LogicalPosition logicalPosition = myEditor.visualToLogicalPosition(new VisualPosition(i, 0)); if (logicalPosition.softWrapLinesOnCurrentLogicalLine > 0) { continue; } int logLine = myLineNumberConvertor.execute(logicalPosition.line); if (logLine >= 0) { String s = String.valueOf(logLine + 1); g.drawString(s, getLineNumberAreaOffset() + getLineNumberAreaWidth() - myEditor.getFontMetrics(Font.PLAIN).stringWidth(s) - 4, (i + 1) * lineHeight - myEditor.getDescent()); } } g2.setTransform(old); } private int endLineNumber() { return Math.max(0, myEditor.getDocument().getLineCount() - 1); } private interface RangeHighlighterProcessor { void process(@NotNull RangeHighlighter highlighter); } private void processRangeHighlighters(int startOffset, int endOffset, @NotNull RangeHighlighterProcessor processor) { Document document = myEditor.getDocument(); final MarkupModelEx docMarkup = (MarkupModelEx)DocumentMarkupModel.forDocument(document, myEditor.getProject(), true); // we limit highlighters to process to between line starting at startOffset and line ending at endOffset DisposableIterator docHighlighters = docMarkup.overlappingIterator(startOffset, endOffset); DisposableIterator editorHighlighters = myEditor.getMarkupModel().overlappingIterator(startOffset, endOffset); try { RangeHighlighterEx lastDocHighlighter = null; RangeHighlighterEx lastEditorHighlighter = null; while (true) { if (lastDocHighlighter == null && docHighlighters.hasNext()) { lastDocHighlighter = docHighlighters.next(); if (!lastDocHighlighter.isValid() || lastDocHighlighter.getAffectedAreaStartOffset() > endOffset) { lastDocHighlighter = null; continue; } if (lastDocHighlighter.getAffectedAreaEndOffset() < startOffset) { lastDocHighlighter = null; continue; } } if (lastEditorHighlighter == null && editorHighlighters.hasNext()) { lastEditorHighlighter = editorHighlighters.next(); if (!lastEditorHighlighter.isValid() || lastEditorHighlighter.getAffectedAreaStartOffset() > endOffset) { lastEditorHighlighter = null; continue; } if (lastEditorHighlighter.getAffectedAreaEndOffset() < startOffset) { lastEditorHighlighter = null; continue; } } if (lastDocHighlighter == null && lastEditorHighlighter == null) return; final RangeHighlighterEx lowerHighlighter; if (less(lastDocHighlighter, lastEditorHighlighter)) { lowerHighlighter = lastDocHighlighter; lastDocHighlighter = null; } else { lowerHighlighter = lastEditorHighlighter; lastEditorHighlighter = null; } assert lowerHighlighter != null; if (!lowerHighlighter.isValid()) continue; int startLineIndex = lowerHighlighter.getDocument().getLineNumber(startOffset); if (startLineIndex < 0 || startLineIndex >= document.getLineCount()) continue; int endLineIndex = lowerHighlighter.getDocument().getLineNumber(endOffset); if (endLineIndex < 0 || endLineIndex >= document.getLineCount()) continue; if (lowerHighlighter.getEditorFilter().avaliableIn(myEditor)) { processor.process(lowerHighlighter); } } } finally { docHighlighters.dispose(); editorHighlighters.dispose(); } } private static boolean less(RangeHighlighter h1, RangeHighlighter h2) { return h1 != null && (h2 == null || h1.getStartOffset() < h2.getStartOffset()); } @Override public void revalidateMarkup() { updateSize(); } public void updateSize() { int prevHash = sizeHash(); updateSizeInner(); if (prevHash != sizeHash()) { fireResized(); } repaint(); } private void updateSizeInner() { myLastPreferredHeight = myEditor.getPreferredHeight(); calcIconAreaWidth(); calcAnnotationsSize(); calcAnnotationExtraSize(); } private int sizeHash() { int result = myLastPreferredHeight; result = 31 * result + myLineMarkerAreaWidth; result = 31 * result + myTextAnnotationGuttersSize; result = 31 * result + myTextAnnotationExtraSize; return result; } private void calcAnnotationsSize() { myTextAnnotationGuttersSize = 0; final FontMetrics fontMetrics = myEditor.getFontMetrics(Font.PLAIN); final int lineCount = myEditor.getDocument().getLineCount(); for (int j = 0; j < myTextAnnotationGutters.size(); j++) { TextAnnotationGutterProvider gutterProvider = myTextAnnotationGutters.get(j); int gutterSize = 0; for (int i = 0; i < lineCount; i++) { final String lineText = gutterProvider.getLineText(i, myEditor); if (lineText != null) { gutterSize = Math.max(gutterSize, fontMetrics.stringWidth(lineText)); } } if (gutterSize > 0) gutterSize += GAP_BETWEEN_ANNOTATIONS; myTextAnnotationGutterSizes.set(j, gutterSize); myTextAnnotationGuttersSize += gutterSize; } } private void calcAnnotationExtraSize() { myTextAnnotationExtraSize = 0; if (!myEditor.isInDistractionFreeMode() || isMirrored()) return; Window frame = SwingUtilities.getWindowAncestor(myEditor.getComponent()); if (frame == null) return; EditorSettings settings = myEditor.getSettings(); int rightMargin = settings.getRightMargin(myEditor.getProject()); if (rightMargin <= 0) return; JComponent editorComponent = myEditor.getComponent(); RelativePoint point = new RelativePoint(editorComponent, new Point(0, 0)); Point editorLocationInWindow = point.getPoint(frame); int editorLocationX = (int)editorLocationInWindow.getX(); int rightMarginX = rightMargin * EditorUtil.getSpaceWidth(Font.PLAIN, myEditor) + editorLocationX; int width = editorLocationX + editorComponent.getWidth(); if (rightMarginX < width && editorLocationX < width - rightMarginX) { int centeredSize = (width - rightMarginX - editorLocationX) / 2 - (myLineMarkerAreaWidth + myLineNumberAreaWidth); myTextAnnotationExtraSize = Math.max(0, centeredSize - myTextAnnotationGuttersSize); } } private TIntObjectHashMap> myLineToGutterRenderers; private void calcIconAreaWidth() { myLineToGutterRenderers = new TIntObjectHashMap>(); processRangeHighlighters(0, myEditor.getDocument().getTextLength(), new RangeHighlighterProcessor() { @Override public void process(@NotNull RangeHighlighter highlighter) { GutterMark renderer = highlighter.getGutterIconRenderer(); if (renderer == null) { return; } if (myEditor.getFoldingModel().isOffsetCollapsed(highlighter.getStartOffset())) { return; } VisualPosition visualPosition = myEditor.offsetToVisualPosition(highlighter.getStartOffset()); int line = EditorUtil.calcSurroundingRange(myEditor, visualPosition, visualPosition).getFirst().line; List renderers = myLineToGutterRenderers.get(line); if (renderers == null) { renderers = new SmartList(); myLineToGutterRenderers.put(line, renderers); } if (renderers.size() < 5) { // Don't allow more than 5 icons per line renderers.add(renderer); } } }); myIconsAreaWidth = START_ICON_AREA_WIDTH; myLineToGutterRenderers.forEachValue(new TObjectProcedure>() { @Override public boolean execute(List renderers) { int width = 1; for (int i = 0; i < renderers.size(); i++) { GutterMark renderer = renderers.get(i); width += renderer.getIcon().getIconWidth(); if (i > 0) width += GAP_BETWEEN_ICONS; } if (myIconsAreaWidth < width) { myIconsAreaWidth = width; } return true; } }); myLineMarkerAreaWidth = myIconsAreaWidth + FREE_PAINTERS_AREA_WIDTH + // if folding outline is shown, there will be enough place for change markers, otherwise add place for it. (isFoldingOutlineShown() ? 0 : getFoldingAnchorWidth() / 2); } private void paintGutterRenderers(final Graphics g, int firstVisibleOffset, int lastVisibleOffset) { Graphics2D g2 = (Graphics2D)g; Object hint = g2.getRenderingHint(RenderingHints.KEY_ANTIALIASING); g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); try { processRangeHighlighters(firstVisibleOffset, lastVisibleOffset, new RangeHighlighterProcessor() { @Override public void process(@NotNull RangeHighlighter highlighter) { paintLineMarkerRenderer(highlighter, g); } }); } finally { g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, hint); } int firstVisibleLine = myEditor.getDocument().getLineNumber(firstVisibleOffset); int lastVisibleLine = myEditor.getDocument().getLineNumber(lastVisibleOffset); paintIcons(firstVisibleLine, lastVisibleLine, g); } private void paintIcons(final int firstVisibleLine, final int lastVisibleLine, final Graphics g) { myLineToGutterRenderers.forEachKey(new TIntProcedure() { @Override public boolean execute(int line) { if (firstVisibleLine > line || lastVisibleLine < line) return true; if (isLineCollapsed(line)) return true; List renderers = myLineToGutterRenderers.get(line); paintIconRow(line, renderers, g); return true; } }); } private boolean isLineCollapsed(final int line) { int startOffset = myEditor.getDocument().getLineStartOffset(line); final FoldRegion region = myEditor.getFoldingModel().getCollapsedRegionAtOffset(startOffset); return region != null && region.getEndOffset() >= myEditor.getDocument().getLineEndOffset(line); } private void paintIconRow(int line, List row, final Graphics g) { processIconsRow(line, row, new LineGutterIconRendererProcessor() { @Override public void process(int x, int y, GutterMark renderer) { Icon icon = renderer.getIcon(); icon.paintIcon(EditorGutterComponentImpl.this, g, x, y); } }); } private void paintLineMarkerRenderer(RangeHighlighter highlighter, Graphics g) { Rectangle rectangle = getLineRendererRectangle(highlighter); if (rectangle != null) { final LineMarkerRenderer lineMarkerRenderer = highlighter.getLineMarkerRenderer(); assert lineMarkerRenderer != null; lineMarkerRenderer.paint(myEditor, g, rectangle); } } @Nullable private Rectangle getLineRendererRectangle(RangeHighlighter highlighter) { LineMarkerRenderer renderer = highlighter.getLineMarkerRenderer(); if (renderer == null) return null; int startOffset = highlighter.getStartOffset(); int endOffset = highlighter.getEndOffset(); if (myEditor.getFoldingModel().isOffsetCollapsed(startOffset) && myEditor.getFoldingModel().isOffsetCollapsed(endOffset)) { return null; } int startY = myEditor.visualPositionToXY(myEditor.offsetToVisualPosition(startOffset)).y; // top edge of the last line of the highlighted area int endY = myEditor.visualPositionToXY(myEditor.offsetToVisualPosition(endOffset)).y; // => add one line height to make height correct (bottom edge of the highlighted area) DocumentEx document = myEditor.getDocument(); if (document.getLineStartOffset(document.getLineNumber(endOffset)) != endOffset) { // but if the highlighter ends with the end of line, its line number is the next line, but that line should not be highlighted endY += myEditor.getLineHeight(); } int height = endY - startY; int w = FREE_PAINTERS_AREA_WIDTH; int x = getLineMarkerAreaOffset() + myIconsAreaWidth; return new Rectangle(x, startY, w, height); } private interface LineGutterIconRendererProcessor { void process(int x, int y, GutterMark renderer); } private void processIconsRow(int line, List row, LineGutterIconRendererProcessor processor) { int middleCount = 0; int middleSize = 0; int x = getLineMarkerAreaOffset() + 1; final int y = myEditor.logicalPositionToXY(new LogicalPosition(line, 0)).y; for (GutterMark r : row) { final GutterIconRenderer.Alignment alignment = ((GutterIconRenderer)r).getAlignment(); final Icon icon = r.getIcon(); if (alignment == GutterIconRenderer.Alignment.LEFT) { processor.process(x, y + getTextAlignmentShift(icon), r); x += icon.getIconWidth() + GAP_BETWEEN_ICONS; } else { if (alignment == GutterIconRenderer.Alignment.CENTER) { middleCount++; middleSize += icon.getIconWidth() + GAP_BETWEEN_ICONS; } } } final int leftSize = x - getLineMarkerAreaOffset(); x = getLineMarkerAreaOffset() + myIconsAreaWidth; for (GutterMark r : row) { if (((GutterIconRenderer)r).getAlignment() == GutterIconRenderer.Alignment.RIGHT) { Icon icon = r.getIcon(); x -= icon.getIconWidth(); processor.process(x, y + getTextAlignmentShift(icon), r); x -= GAP_BETWEEN_ICONS; } } int rightSize = myIconsAreaWidth + getLineMarkerAreaOffset() - x; if (middleCount > 0) { middleSize -= GAP_BETWEEN_ICONS; x = getLineMarkerAreaOffset() + leftSize + (myIconsAreaWidth - leftSize - rightSize - middleSize) / 2; for (GutterMark r : row) { if (((GutterIconRenderer)r).getAlignment() == GutterIconRenderer.Alignment.CENTER) { Icon icon = r.getIcon(); processor.process(x, y + getTextAlignmentShift(icon), r); x += icon.getIconWidth() + GAP_BETWEEN_ICONS; } } } } private int getTextAlignmentShift(Icon icon) { return (myEditor.getLineHeight() - icon.getIconHeight()) /2; } @Override public Color getOutlineColor(boolean isActive) { ColorKey key = isActive ? EditorColors.SELECTED_TEARLINE_COLOR : EditorColors.TEARLINE_COLOR; Color color = myEditor.getColorsScheme().getColor(key); return color != null ? color : JBColor.black; } @Override public void registerTextAnnotation(@NotNull TextAnnotationGutterProvider provider) { myTextAnnotationGutters.add(provider); myTextAnnotationGutterSizes.add(0); updateSize(); } @Override public void registerTextAnnotation(@NotNull TextAnnotationGutterProvider provider, @NotNull EditorGutterAction action) { myTextAnnotationGutters.add(provider); myProviderToListener.put(provider, action); myTextAnnotationGutterSizes.add(0); updateSize(); } private void doPaintFoldingTree(final Graphics2D g, final Rectangle clip, int firstVisibleOffset, int lastVisibleOffset) { final int anchorX = getFoldingAreaOffset(); final int width = getFoldingAnchorWidth(); Collection anchorsToDisplay = myAnchorsDisplayStrategy.getAnchorsToDisplay(firstVisibleOffset, lastVisibleOffset, myActiveFoldRegion); for (DisplayedFoldingAnchor anchor : anchorsToDisplay) { drawAnchor(width, clip, g, anchorX, anchor.visualLine, anchor.type, anchor.foldRegion == myActiveFoldRegion); } } private void paintFoldingBackground(Graphics g, Rectangle clip) { int lineX = getWhitespaceSeparatorOffset(); paintBackground(g, clip, getFoldingAreaOffset(), getFoldingAreaWidth()); g.setColor(myEditor.getBackgroundColor()); g.fillRect(lineX, clip.y, getFoldingAreaWidth(), clip.height); paintCaretRowBackground(g, lineX, getFoldingAnchorWidth()); } private void paintFoldingLines(final Graphics2D g, final Rectangle clip) { if (!isFoldingOutlineShown()) return; UIUtil.drawVDottedLine(g, getWhitespaceSeparatorOffset(), clip.y, clip.y + clip.height, null, getOutlineColor(false)); final int anchorX = getFoldingAreaOffset(); final int width = getFoldingAnchorWidth(); if (myActiveFoldRegion != null && myActiveFoldRegion.isExpanded() && myActiveFoldRegion.isValid()) { int foldStart = myEditor.offsetToVisualLine(myActiveFoldRegion.getStartOffset()); int foldEnd = myEditor.offsetToVisualLine(getEndOffset(myActiveFoldRegion)); int startY = myEditor.visibleLineToY(foldStart + 1) - myEditor.getDescent(); int endY = myEditor.visibleLineToY(foldEnd) + myEditor.getLineHeight() - myEditor.getDescent(); if (startY <= clip.y + clip.height && endY + 1 + myEditor.getDescent() >= clip.y) { int lineX = anchorX + width / 2; g.setColor(getOutlineColor(true)); UIUtil.drawLine(g, lineX, startY, lineX, endY); } } } @Override public int getWhitespaceSeparatorOffset() { return getFoldingAreaOffset() + getFoldingAnchorWidth() / 2; } public void setActiveFoldRegion(FoldRegion activeFoldRegion) { if (myActiveFoldRegion != activeFoldRegion) { myActiveFoldRegion = activeFoldRegion; repaint(); } } public int getHeadCenterY(FoldRegion foldRange) { int width = getFoldingAnchorWidth(); int foldStart = myEditor.offsetToVisualLine(foldRange.getStartOffset()); return myEditor.visibleLineToY(foldStart) + myEditor.getLineHeight() - myEditor.getDescent() - width / 2; } private void drawAnchor(int width, Rectangle clip, Graphics2D g, int anchorX, int visualLine, DisplayedFoldingAnchor.Type type, boolean active) { int height = width + 2; int y; switch (type) { case COLLAPSED: y = myEditor.visibleLineToY(visualLine) + myEditor.getLineHeight() - myEditor.getDescent() - width; if (y <= clip.y + clip.height && y + height >= clip.y) { drawSquareWithPlus(g, anchorX, y, width, active); } break; case EXPANDED_TOP: y = myEditor.visibleLineToY(visualLine) + myEditor.getLineHeight() - myEditor.getDescent() - width; if (y <= clip.y + clip.height && y + height >= clip.y) { drawDirectedBox(g, anchorX, y, width, height, width - 2, active); } break; case EXPANDED_BOTTOM: y = myEditor.visibleLineToY(visualLine) + myEditor.getLineHeight() - myEditor.getDescent(); if (y - height <= clip.y + clip.height && y >= clip.y) { drawDirectedBox(g, anchorX, y, width, -height, -width + 2, active); } break; } } private int getEndOffset(FoldRegion foldRange) { LOG.assertTrue(foldRange.isValid(), foldRange); FoldingGroup group = foldRange.getGroup(); return group == null ? foldRange.getEndOffset() : myEditor.getFoldingModel().getEndOffset(group); } private void drawDirectedBox(Graphics2D g, int anchorX, int y, int width, int height, int baseHeight, boolean active) { Object antialiasing = g.getRenderingHint(RenderingHints.KEY_ANTIALIASING); if (SystemInfo.isMac && SystemInfo.JAVA_VERSION.startsWith("1.4.1") || UIUtil.isRetina()) { g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); } try { int[] xPoints = {anchorX, anchorX + width, anchorX + width, anchorX + width / 2, anchorX}; int[] yPoints = {y, y, y + baseHeight, y + height, y + baseHeight}; g.setColor(myEditor.getBackgroundColor()); g.fillPolygon(xPoints, yPoints, 5); g.setColor(getOutlineColor(active)); g.drawPolygon(xPoints, yPoints, 5); //Minus int minusHeight = y + baseHeight / 2 + (height - baseHeight) / 4; UIUtil.drawLine(g, anchorX + 2, minusHeight, anchorX + width - 2, minusHeight); } finally { g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, antialiasing); } } private void drawSquareWithPlus(Graphics2D g, int anchorX, int y, int width, boolean active) { drawSquareWithMinus(g, anchorX, y, width, active); UIUtil.drawLine(g, anchorX + width / 2, y + 2, anchorX + width / 2, y + width - 2); } @SuppressWarnings("SuspiciousNameCombination") private void drawSquareWithMinus(Graphics2D g, int anchorX, int y, int width, boolean active) { g.setColor(myEditor.getBackgroundColor()); g.fillRect(anchorX, y, width, width); g.setColor(getOutlineColor(active)); g.drawRect(anchorX, y, width, width); // Draw plus if (!active) g.setColor(getOutlineColor(true)); UIUtil.drawLine(g, anchorX + 2, y + width / 2, anchorX + width - 2, y + width / 2); } private int getFoldingAnchorWidth() { return Math.min(4, myEditor.getLineHeight() / 2 - 2) * 2; } public int getFoldingAreaOffset() { return getLineMarkerAreaOffset() + getLineMarkerAreaWidth(); } public int getFoldingAreaWidth() { return isFoldingOutlineShown() ? getFoldingAnchorWidth() + 2 : isLineNumbersShown() ? getFoldingAnchorWidth() / 2 : 0; } @Override public boolean isLineMarkersShown() { return myEditor.getSettings().isLineMarkerAreaShown(); } public boolean isLineNumbersShown() { return myEditor.getSettings().isLineNumbersShown(); } @Override public boolean isAnnotationsShown() { return !myTextAnnotationGutters.isEmpty(); } @Override public boolean isFoldingOutlineShown() { return myEditor.getSettings().isFoldingOutlineShown() && myEditor.getFoldingModel().isFoldingEnabled() && !myEditor.isInPresentationMode(); } public int getLineNumberAreaWidth() { return isLineNumbersShown() ? myLineNumberAreaWidth : 0; } public int getLineMarkerAreaWidth() { return isLineMarkersShown() ? myLineMarkerAreaWidth : 0; } public void setLineNumberAreaWidth(@NotNull TIntFunction calculator) { int maxLineNumber = 0; for (int i = endLineNumber(); i >= 0; i--) { int number = myLineNumberConvertor.execute(i); if (number >= 0) { maxLineNumber = number; break; } } final int lineNumberAreaWidth = calculator.execute(maxLineNumber); if (myLineNumberAreaWidth != lineNumberAreaWidth) { myLineNumberAreaWidth = lineNumberAreaWidth; fireResized(); } } @Nullable public EditorMouseEventArea getEditorMouseAreaByOffset(int offset) { int x = offset - getLineNumberAreaOffset(); if (x >= 0 && (x -= getLineNumberAreaWidth()) < 0) { return EditorMouseEventArea.LINE_NUMBERS_AREA; } if (x >= 0 && (x -= getAnnotationsAreaWidth()) < 0) { return EditorMouseEventArea.ANNOTATIONS_AREA; } if ((x -= myTextAnnotationExtraSize * 3 / 5) >= 0 && (x -= myTextAnnotationExtraSize * 2 / 5) < 0) { return EditorMouseEventArea.LINE_MARKERS_AREA; } if (x >= 0 && (x -= getLineMarkerAreaWidth()) < 0) { return EditorMouseEventArea.LINE_MARKERS_AREA; } if (x >= 0 && (x -= getFoldingAreaWidth()) < 0) { return EditorMouseEventArea.FOLDING_OUTLINE_AREA; } return null; } public static int getLineNumberAreaOffset() { return 0; } public int getAnnotationsAreaOffset() { return getLineNumberAreaOffset() + getLineNumberAreaWidth(); } public int getAnnotationsAreaWidth() { return myTextAnnotationGuttersSize; } public int getAnnotationsAreaWidthEx() { return myTextAnnotationGuttersSize + myTextAnnotationExtraSize; } @Override public int getLineMarkerAreaOffset() { return getAnnotationsAreaOffset() + getAnnotationsAreaWidthEx(); } @Override public int getIconsAreaWidth() { return myIconsAreaWidth; } private boolean isMirrored() { return myEditor.getVerticalScrollbarOrientation() != EditorEx.VERTICAL_SCROLLBAR_RIGHT; } @Nullable @Override public FoldRegion findFoldingAnchorAt(int x, int y) { if (!myEditor.getSettings().isFoldingOutlineShown()) return null; int anchorX = getFoldingAreaOffset(); int anchorWidth = getFoldingAnchorWidth(); int neighbourhoodStartOffset = myEditor.logicalPositionToOffset(myEditor.xyToLogicalPosition(new Point(0, y - myEditor.getLineHeight()))); int neighbourhoodEndOffset = myEditor.logicalPositionToOffset(myEditor.xyToLogicalPosition(new Point(0, y + myEditor.getLineHeight()))); Collection displayedAnchors = myAnchorsDisplayStrategy.getAnchorsToDisplay(neighbourhoodStartOffset, neighbourhoodEndOffset, null); for (DisplayedFoldingAnchor anchor : displayedAnchors) { if (rectangleByFoldOffset(anchor.visualLine, anchorWidth, anchorX).contains(x, y)) return anchor.foldRegion; } return null; } @SuppressWarnings("SuspiciousNameCombination") private Rectangle rectangleByFoldOffset(int foldStart, int anchorWidth, int anchorX) { int anchorY = myEditor.visibleLineToY(foldStart) + myEditor.getLineHeight() - myEditor.getDescent() - anchorWidth; return new Rectangle(anchorX, anchorY, anchorWidth, anchorWidth); } @Override public void mouseDragged(MouseEvent e) { TooltipController.getInstance().cancelTooltips(); } @Override public void mouseMoved(final MouseEvent e) { String toolTip = null; final GutterIconRenderer renderer = getGutterRenderer(e); TooltipController controller = TooltipController.getInstance(); if (renderer != null) { toolTip = renderer.getTooltipText(); if (renderer.isNavigateAction()) { setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)); } } else { ActiveGutterRenderer lineRenderer = getActiveRendererByMouseEvent(e); if (lineRenderer != null) { setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)); } else { TextAnnotationGutterProvider provider = getProviderAtPoint(e.getPoint()); if (provider != null) { final int line = getLineNumAtPoint(e.getPoint()); toolTip = provider.getToolTip(line, myEditor); if (!Comparing.equal(toolTip, myLastGutterToolTip)) { controller.cancelTooltip(GUTTER_TOOLTIP_GROUP, e, true); myLastGutterToolTip = toolTip; } if (myProviderToListener.containsKey(provider)) { final EditorGutterAction action = myProviderToListener.get(provider); if (action != null) { setCursor(action.getCursor(line)); } } } } } if (toolTip != null && !toolTip.isEmpty()) { final Ref t = new Ref(e.getPoint()); int line = EditorUtil.yPositionToLogicalLine(myEditor, e); List row = myLineToGutterRenderers.get(line); Balloon.Position ballPosition = Balloon.Position.atRight; if (row != null) { final TreeMap xPos = new TreeMap(); final int[] currentPos = {0}; processIconsRow(line, row, new LineGutterIconRendererProcessor() { @Override public void process(int x, int y, GutterMark r) { xPos.put(x, r); if (renderer == r && r != null) { currentPos[0] = x; Icon icon = r.getIcon(); t.set(new Point(x + icon.getIconWidth() / 2, y + icon.getIconHeight() / 2)); } } }); List xx = new ArrayList(xPos.keySet()); int posIndex = xx.indexOf(currentPos[0]); if (xPos.size() > 1 && posIndex == 0) { ballPosition = Balloon.Position.below; } } RelativePoint showPoint = new RelativePoint(this, t.get()); controller.showTooltipByMouseMove(myEditor, showPoint, ((EditorMarkupModel)myEditor.getMarkupModel()).getErrorStripTooltipRendererProvider().calcTooltipRenderer(toolTip), false, GUTTER_TOOLTIP_GROUP, new HintHint(this, t.get()).setAwtTooltip(true).setPreferredPosition(ballPosition)); } else { controller.cancelTooltip(GUTTER_TOOLTIP_GROUP, e, false); } } @Override public void mouseClicked(MouseEvent e) { if (e.isPopupTrigger()) { invokePopup(e); } } private void fireEventToTextAnnotationListeners(final MouseEvent e) { if (myEditor.getMouseEventArea(e) == EditorMouseEventArea.ANNOTATIONS_AREA) { final Point clickPoint = e.getPoint(); final TextAnnotationGutterProvider provider = getProviderAtPoint(clickPoint); if (provider == null) { return; } if (myProviderToListener.containsKey(provider)) { int line = getLineNumAtPoint(clickPoint); if (line >= 0 && line < myEditor.getDocument().getLineCount() && UIUtil.isActionClick(e, MouseEvent.MOUSE_RELEASED)) { myProviderToListener.get(provider).doAction(line); } } } } private int getLineNumAtPoint(final Point clickPoint) { return EditorUtil.yPositionToLogicalLine(myEditor, clickPoint); } @Nullable private TextAnnotationGutterProvider getProviderAtPoint(final Point clickPoint) { int current = getAnnotationsAreaOffset(); if (clickPoint.x < current) return null; for (int i = 0; i < myTextAnnotationGutterSizes.size(); i++) { current += myTextAnnotationGutterSizes.get(i); if (clickPoint.x <= current) return myTextAnnotationGutters.get(i); } return null; } @Override public void mousePressed(MouseEvent e) { if (e.isPopupTrigger()) { invokePopup(e); myPopupInvokedOnPressed = true; } else if (UIUtil.isCloseClick(e)) { processClose(e); } } @Override public void mouseReleased(final MouseEvent e) { if (e.isPopupTrigger()) { invokePopup(e); return; } if (myPopupInvokedOnPressed) { myPopupInvokedOnPressed = false; return; } GutterIconRenderer renderer = getGutterRenderer(e); AnAction clickAction = null; if (renderer != null && e.getButton() < 4) { clickAction = (InputEvent.BUTTON2_MASK & e.getModifiers()) > 0 ? renderer.getMiddleButtonClickAction() : renderer.getClickAction(); } if (clickAction != null) { clickAction.actionPerformed(new AnActionEvent(e, myEditor.getDataContext(), "ICON_NAVIGATION", clickAction.getTemplatePresentation(), ActionManager.getInstance(), e.getModifiers())); e.consume(); repaint(); } else { ActiveGutterRenderer lineRenderer = getActiveRendererByMouseEvent(e); if (lineRenderer != null) { lineRenderer.doAction(myEditor, e); } else { fireEventToTextAnnotationListeners(e); } } } @Nullable private ActiveGutterRenderer getActiveRendererByMouseEvent(final MouseEvent e) { if (findFoldingAnchorAt(e.getX(), e.getY()) != null) { return null; } if (e.isConsumed() || e.getX() > getWhitespaceSeparatorOffset()) { return null; } final ActiveGutterRenderer[] gutterRenderer = {null}; Rectangle clip = myEditor.getScrollingModel().getVisibleArea(); int firstVisibleOffset = myEditor.logicalPositionToOffset( myEditor.xyToLogicalPosition(new Point(0, clip.y - myEditor.getLineHeight()))); int lastVisibleOffset = myEditor.logicalPositionToOffset( myEditor.xyToLogicalPosition(new Point(0, clip.y + clip.height + myEditor.getLineHeight()))); processRangeHighlighters(firstVisibleOffset, lastVisibleOffset, new RangeHighlighterProcessor() { @Override public void process(@NotNull RangeHighlighter highlighter) { if (gutterRenderer[0] != null) return; Rectangle rectangle = getLineRendererRectangle(highlighter); if (rectangle == null) return; int startY = rectangle.y; int endY = startY + rectangle.height; if (startY == endY) { endY += myEditor.getLineHeight(); } if (startY < e.getY() && e.getY() <= endY) { final LineMarkerRenderer renderer = highlighter.getLineMarkerRenderer(); if (renderer instanceof ActiveGutterRenderer && ((ActiveGutterRenderer)renderer).canDoAction(e)) { gutterRenderer[0] = (ActiveGutterRenderer)renderer; } } } }); return gutterRenderer[0]; } @Override public void closeAllAnnotations() { for (TextAnnotationGutterProvider provider : myTextAnnotationGutters) { provider.gutterClosed(); } revalidateSizes(); } private void revalidateSizes() { myTextAnnotationGutters = new ArrayList(); myTextAnnotationGutterSizes = new TIntArrayList(); updateSize(); } private class CloseAnnotationsAction extends DumbAwareAction { public CloseAnnotationsAction() { super(EditorBundle.message("close.editor.annotations.action.name")); } @Override public void actionPerformed(AnActionEvent e) { closeAllAnnotations(); } } @Override @Nullable public Point getPoint(final GutterIconRenderer renderer) { final Ref result = Ref.create(); for (int line : myLineToGutterRenderers.keys()) { processIconsRow(line, myLineToGutterRenderers.get(line), new LineGutterIconRendererProcessor() { @Override public void process(int x, int y, GutterMark r) { if (result.isNull() && r.equals(renderer)) { result.set(new Point(x, y)); } } }); if (!result.isNull()) { return result.get(); } } return null; } @Override public void setLineNumberConvertor(@NotNull TIntFunction lineNumberConvertor) { myLineNumberConvertor = lineNumberConvertor; } @Override public void setShowDefaultGutterPopup(boolean show) { myShowDefaultGutterPopup = show; } private void invokePopup(MouseEvent e) { final ActionManager actionManager = ActionManager.getInstance(); if (myEditor.getMouseEventArea(e) == EditorMouseEventArea.ANNOTATIONS_AREA) { DefaultActionGroup actionGroup = new DefaultActionGroup(EditorBundle.message("editor.annotations.action.group.name"), true); actionGroup.add(new CloseAnnotationsAction()); final List addActions = new ArrayList(); final Point p = e.getPoint(); int line = EditorUtil.yPositionToLogicalLine(myEditor, p); //if (line >= myEditor.getDocument().getLineCount()) return; for (TextAnnotationGutterProvider gutterProvider : myTextAnnotationGutters) { final List list = gutterProvider.getPopupActions(line, myEditor); if (list != null) { for (AnAction action : list) { if (! addActions.contains(action)) { addActions.add(action); } } } } for (AnAction addAction : addActions) { actionGroup.add(addAction); } JPopupMenu menu = actionManager.createActionPopupMenu("", actionGroup).getComponent(); menu.show(this, e.getX(), e.getY()); } else { GutterIconRenderer renderer = getGutterRenderer(e); if (renderer != null) { ActionGroup actionGroup = renderer.getPopupMenuActions(); if (actionGroup != null) { ActionPopupMenu popupMenu = actionManager.createActionPopupMenu(ActionPlaces.UNKNOWN, actionGroup); popupMenu.getComponent().show(this, e.getX(), e.getY()); e.consume(); } else { AnAction rightButtonAction = renderer.getRightButtonClickAction(); if (rightButtonAction != null) { rightButtonAction.actionPerformed(new AnActionEvent(e, myEditor.getDataContext(), "ICON_NAVIGATION_SECONDARY_BUTTON", rightButtonAction.getTemplatePresentation(), ActionManager.getInstance(), e.getModifiers())); e.consume(); } } } else { if (myShowDefaultGutterPopup) { ActionGroup group = (ActionGroup)CustomActionsSchema.getInstance().getCorrectedAction(IdeActions.GROUP_EDITOR_GUTTER); ActionPopupMenu popupMenu = actionManager.createActionPopupMenu(ActionPlaces.UNKNOWN, group); popupMenu.getComponent().show(this, e.getX(), e.getY()); } e.consume(); } } } @Override public void mouseEntered(MouseEvent e) { } @Override public void mouseExited(MouseEvent e) { TooltipController.getInstance().cancelTooltip(GUTTER_TOOLTIP_GROUP, e, false); } private int convertPointToLineNumber(final Point p) { int line = EditorUtil.yPositionToLogicalLine(myEditor, p); if (line >= myEditor.getDocument().getLineCount()) return -1; int startOffset = myEditor.getDocument().getLineStartOffset(line); final FoldRegion region = myEditor.getFoldingModel().getCollapsedRegionAtOffset(startOffset); if (region != null) { line = myEditor.getDocument().getLineNumber(region.getEndOffset()); if (line >= myEditor.getDocument().getLineCount()) return -1; } return line; } @Nullable private GutterMark getGutterRenderer(final Point p) { int line = convertPointToLineNumber(p); if (line == -1) return null; List renderers = myLineToGutterRenderers.get(line); if (renderers == null) { return null; } final GutterMark[] result = {null}; processIconsRow(line, renderers, new LineGutterIconRendererProcessor() { @Override public void process(int x, int y, GutterMark renderer) { final int ex = convertX((int)p.getX()); Icon icon = renderer.getIcon(); // Do not check y to extend the area where users could click if (x <= ex && ex <= x + icon.getIconWidth()) { result[0] = renderer; } } }); return result[0]; } @Nullable private GutterIconRenderer getGutterRenderer(final MouseEvent e) { return (GutterIconRenderer)getGutterRenderer(e.getPoint()); } public int convertX(int x) { if (!isMirrored()) return x; return getWidth() - x; } public void dispose() { for (TextAnnotationGutterProvider gutterProvider : myTextAnnotationGutters) { gutterProvider.gutterClosed(); } myProviderToListener.clear(); } }