/* * 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: Apr 19, 2002 * Time: 2:56:43 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.codeHighlighting.HighlightDisplayLevel; import com.intellij.codeInsight.hint.*; import com.intellij.ide.ui.UISettings; import com.intellij.ide.ui.UISettingsListener; import com.intellij.openapi.Disposable; import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.application.ex.ApplicationManagerEx; import com.intellij.openapi.application.impl.ApplicationImpl; import com.intellij.openapi.command.CommandProcessor; import com.intellij.openapi.command.UndoConfirmationPolicy; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.editor.*; import com.intellij.openapi.editor.actionSystem.DocCommandGroupId; import com.intellij.openapi.editor.colors.EditorFontType; import com.intellij.openapi.editor.ex.*; import com.intellij.openapi.editor.ex.util.EditorUIUtil; import com.intellij.openapi.editor.markup.ErrorStripeRenderer; import com.intellij.openapi.editor.markup.RangeHighlighter; import com.intellij.openapi.fileEditor.impl.EditorWindowHolder; import com.intellij.openapi.ui.MessageType; import com.intellij.openapi.ui.popup.Balloon; import com.intellij.openapi.util.Disposer; import com.intellij.openapi.util.ProperTextRange; import com.intellij.openapi.wm.IdeFocusManager; import com.intellij.openapi.wm.ToolWindowAnchor; import com.intellij.openapi.wm.ex.ToolWindowManagerEx; import com.intellij.ui.*; import com.intellij.ui.awt.RelativePoint; import com.intellij.util.Alarm; import com.intellij.util.IJSwingUtilities; import com.intellij.util.Processor; import com.intellij.util.containers.ContainerUtil; import com.intellij.util.ui.ButtonlessScrollBarUI; import com.intellij.util.ui.GraphicsUtil; import com.intellij.util.ui.JBUI; import com.intellij.util.ui.UIUtil; import consulo.annotations.RequiredDispatchThread; import gnu.trove.THashSet; import gnu.trove.TIntIntHashMap; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import javax.swing.*; import java.awt.*; import java.awt.event.*; import java.awt.geom.AffineTransform; import java.awt.geom.Area; import java.awt.geom.Ellipse2D; import java.awt.geom.RoundRectangle2D; import java.awt.image.BufferedImage; import java.util.*; import java.util.List; import java.util.Queue; import java.util.concurrent.atomic.AtomicReference; public class EditorMarkupModelImpl extends MarkupModelImpl implements EditorMarkupModel { public static final Logger LOGGER = Logger.getInstance(EditorMarkupModelImpl.class); private static final TooltipGroup ERROR_STRIPE_TOOLTIP_GROUP = new TooltipGroup("ERROR_STRIPE_TOOLTIP_GROUP", 0); private final EditorImpl myEditor; // null renderer means we should not show traffic light icon private ErrorStripeRenderer myErrorStripeRenderer; private final List<ErrorStripeListener> myErrorMarkerListeners = ContainerUtil.createLockFreeCopyOnWriteList(); private MyErrorPanel myErrorPanel; private boolean dimensionsAreValid; private int myEditorScrollbarTop = -1; private int myEditorTargetHeight = -1; private int myEditorSourceHeight = -1; private ProperTextRange myDirtyYPositions; private static final ProperTextRange WHOLE_DOCUMENT = new ProperTextRange(0, 0); @NotNull private ErrorStripTooltipRendererProvider myTooltipRendererProvider = new BasicTooltipRendererProvider(); private int myMinMarkHeight = JBUI.scale(3); private static final int myPreviewLines = 5;// Actually preview has myPreviewLines * 2 + 1 lines (above + below + current one) private static final int myCachePreviewLines = 100;// Actually cache image has myCachePreviewLines * 2 + 1 lines (above + below + current one) private LightweightHint myEditorPreviewHint = null; private final EditorFragmentRenderer myEditorFragmentRenderer; private int myRowAdjuster = 0; private int myWheelAccumulator = 0; EditorMarkupModelImpl(@NotNull EditorImpl editor) { super(editor.getDocument()); myEditor = editor; myEditorFragmentRenderer = new EditorFragmentRenderer(); } private int offsetToLine(int offset, Document document) { if (offset < 0) { return 0; } if (offset > document.getTextLength()) { return document.getLineCount(); } return myEditor.offsetToVisualLine(offset); } public void repaintVerticalScrollBar() { myEditor.getVerticalScrollBar().repaint(); } void recalcEditorDimensions() { int scrollBarHeight = myEditor.getPanel().getHeight(); myEditorScrollbarTop = myErrorStripeRenderer == null ? 0 : myErrorStripeRenderer.getSquareSize(); int editorScrollbarBottom = 0; myEditorTargetHeight = scrollBarHeight - myEditorScrollbarTop - editorScrollbarBottom; myEditorSourceHeight = myEditor.getPreferredHeight(); dimensionsAreValid = scrollBarHeight != 0; } public void repaintTrafficLightIcon() { MyErrorPanel errorPanel = getErrorPanel(); if (errorPanel != null) { errorPanel.repaint(); errorPanel.repaintTrafficTooltip(); } } private static class PositionedStripe { @NotNull private Color color; private int yEnd; private final boolean thin; private final int layer; private PositionedStripe(@NotNull Color color, int yEnd, boolean thin, int layer) { this.color = color; this.yEnd = yEnd; this.thin = thin; this.layer = layer; } } private boolean showToolTipByMouseMove(final MouseEvent e) { if (myEditor.getVisibleLineCount() == 0) return false; MouseEvent me = new MouseEvent(e.getComponent(), e.getID(), e.getWhen(), e.getModifiers(), 0, e.getY() + 1, e.getClickCount(), e.isPopupTrigger()); final int visualLine = getVisualLineByEvent(e); Rectangle area = myEditor.getScrollingModel().getVisibleArea(); int visualY = myEditor.getLineHeight() * visualLine; boolean isVisible = area.contains(area.x, visualY) && myWheelAccumulator == 0; TooltipRenderer bigRenderer; if (IJSwingUtilities.findParentByInterface(myEditor.getComponent(), EditorWindowHolder.class) == null || isVisible || !UISettings.getInstance().SHOW_EDITOR_TOOLTIP) { final Set<RangeHighlighter> highlighters = new THashSet<RangeHighlighter>(); getNearestHighlighters(this, me.getY(), highlighters); getNearestHighlighters((MarkupModelEx)DocumentMarkupModel.forDocument(myEditor.getDocument(), getEditor().getProject(), true), me.getY(), highlighters); if (highlighters.isEmpty()) return false; int y = e.getY(); RangeHighlighter nearest = getNearestRangeHighlighter(e); if (nearest != null) { ProperTextRange range = offsetsToYPositions(nearest.getStartOffset(), nearest.getEndOffset()); int eachStartY = range.getStartOffset(); int eachEndY = range.getEndOffset(); y = eachStartY + (eachEndY - eachStartY) / 2; } me = new MouseEvent(e.getComponent(), e.getID(), e.getWhen(), e.getModifiers(), me.getX(), y + 1, e.getClickCount(), e.isPopupTrigger()); bigRenderer = myTooltipRendererProvider.calcTooltipRenderer(highlighters); if (bigRenderer != null) { showTooltip(me, bigRenderer, createHint(me)); return true; } return false; } else { float rowRatio = (float)visualLine / (myEditor.getVisibleLineCount() - 1); int y = myRowAdjuster != 0 ? (int)(rowRatio * myEditor.getVerticalScrollBar().getHeight()) : me.getY(); me = new MouseEvent(me.getComponent(), me.getID(), me.getWhen(), me.getModifiers(), me.getX(), y, me.getClickCount(), me.isPopupTrigger()); final List<RangeHighlighterEx> highlighters = new ArrayList<RangeHighlighterEx>(); collectRangeHighlighters(this, visualLine, highlighters); collectRangeHighlighters((MarkupModelEx)DocumentMarkupModel.forDocument(myEditor.getDocument(), getEditor().getProject(), true), visualLine, highlighters); myEditorFragmentRenderer.update(visualLine, highlighters, me.isAltDown()); myEditorFragmentRenderer.show(myEditor, me.getPoint(), true, ERROR_STRIPE_TOOLTIP_GROUP, createHint(me)); return true; } } private static HintHint createHint(MouseEvent me) { return new HintHint(me).setAwtTooltip(true).setPreferredPosition(Balloon.Position.atLeft).setBorderInsets(new Insets(1, 1, 1, 1)).setShowImmediately(true) .setAnimationEnabled(false); } private int getVisualLineByEvent(MouseEvent e) { return fitLineToEditor(myEditor.offsetToVisualLine(yPositionToOffset(e.getY() + myWheelAccumulator, true))); } private int fitLineToEditor(int visualLine) { return Math.max(0, Math.min(myEditor.getVisibleLineCount() - 1, visualLine)); } private int getOffset(int visualLine, boolean startLine) { int logicalLine = myEditor.visualToLogicalPosition(new VisualPosition(visualLine, 0)).line; return startLine ? myEditor.getDocument().getLineStartOffset(logicalLine) : myEditor.getDocument().getLineEndOffset(logicalLine); } private void collectRangeHighlighters(MarkupModelEx markupModel, final int visualLine, final Collection<RangeHighlighterEx> highlighters) { final int startOffset = getOffset(fitLineToEditor(visualLine - myPreviewLines), true); final int endOffset = getOffset(fitLineToEditor(visualLine + myPreviewLines), false); markupModel.processRangeHighlightersOverlappingWith(startOffset, endOffset, new Processor<RangeHighlighterEx>() { @Override public boolean process(RangeHighlighterEx highlighter) { if (highlighter.getErrorStripeMarkColor() != null) { if (highlighter.getStartOffset() < endOffset && highlighter.getEndOffset() > startOffset) { highlighters.add(highlighter); } } return true; } }); } @Nullable private RangeHighlighter getNearestRangeHighlighter(final MouseEvent e) { List<RangeHighlighter> highlighters = new ArrayList<RangeHighlighter>(); getNearestHighlighters(this, e.getY(), highlighters); getNearestHighlighters((MarkupModelEx)DocumentMarkupModel.forDocument(myEditor.getDocument(), myEditor.getProject(), true), e.getY(), highlighters); RangeHighlighter nearestMarker = null; int yPos = 0; for (RangeHighlighter highlighter : highlighters) { final int newYPos = offsetsToYPositions(highlighter.getStartOffset(), highlighter.getEndOffset()).getStartOffset(); if (nearestMarker == null || Math.abs(yPos - e.getY()) > Math.abs(newYPos - e.getY())) { nearestMarker = highlighter; yPos = newYPos; } } return nearestMarker; } private void getNearestHighlighters(MarkupModelEx markupModel, final int scrollBarY, final Collection<RangeHighlighter> nearest) { int startOffset = yPositionToOffset(scrollBarY - myMinMarkHeight, true); int endOffset = yPositionToOffset(scrollBarY + myMinMarkHeight, false); markupModel.processRangeHighlightersOverlappingWith(startOffset, endOffset, new Processor<RangeHighlighterEx>() { @Override public boolean process(RangeHighlighterEx highlighter) { if (highlighter.getErrorStripeMarkColor() != null) { ProperTextRange range = offsetsToYPositions(highlighter.getStartOffset(), highlighter.getEndOffset()); if (scrollBarY >= range.getStartOffset() - myMinMarkHeight * 2 && scrollBarY <= range.getEndOffset() + myMinMarkHeight * 2) { nearest.add(highlighter); } } return true; } }); } @RequiredDispatchThread private void doClick(final MouseEvent e) { RangeHighlighter marker = getNearestRangeHighlighter(e); int offset; LogicalPosition logicalPositionToScroll = null; if (marker == null) { if (myEditorPreviewHint != null) { logicalPositionToScroll = myEditor.visualToLogicalPosition(new VisualPosition(myEditorFragmentRenderer.myStartVisualLine, 0)); offset = myEditor.getDocument().getLineStartOffset(logicalPositionToScroll.line); } else { return; } } else { offset = marker.getStartOffset(); } final Document doc = myEditor.getDocument(); if (doc.getLineCount() > 0 && myEditorPreviewHint == null) { // Necessary to expand folded block even if navigating just before one // Very useful when navigating to first unused import statement. int lineEnd = doc.getLineEndOffset(doc.getLineNumber(offset)); myEditor.getCaretModel().moveToOffset(lineEnd); } myEditor.getCaretModel().removeSecondaryCarets(); myEditor.getCaretModel().moveToOffset(offset); myEditor.getSelectionModel().removeSelection(); ScrollingModel scrollingModel = myEditor.getScrollingModel(); scrollingModel.disableAnimation(); if (logicalPositionToScroll != null) { int lineY = myEditor.logicalPositionToXY(logicalPositionToScroll).y; int relativePopupOffset = myEditorFragmentRenderer.myRelativeY; scrollingModel.scrollVertically(lineY - relativePopupOffset); } else { scrollingModel.scrollToCaret(ScrollType.CENTER); } scrollingModel.enableAnimation(); if (marker != null) { fireErrorMarkerClicked(marker, e); } } @Override public void setErrorStripeVisible(boolean val) { if (val) { if (myErrorPanel != null) { myErrorPanel.setPopupHandler(null); } myErrorPanel = new MyErrorPanel(); myEditor.getPanel() .add(myErrorPanel, myEditor.getVerticalScrollbarOrientation() == EditorEx.VERTICAL_SCROLLBAR_LEFT ? BorderLayout.WEST : BorderLayout.EAST); } else if (myErrorPanel != null) { myEditor.getPanel().remove(myErrorPanel); myErrorPanel = null; } } @Nullable private MyErrorPanel getErrorPanel() { return myErrorPanel; } @RequiredDispatchThread @Override public void setErrorPanelPopupHandler(@NotNull PopupHandler handler) { ApplicationManager.getApplication().assertIsDispatchThread(); MyErrorPanel errorPanel = getErrorPanel(); if (errorPanel != null) { errorPanel.setPopupHandler(handler); } } @Override public void setErrorStripTooltipRendererProvider(@NotNull final ErrorStripTooltipRendererProvider provider) { myTooltipRendererProvider = provider; } @Override @NotNull public ErrorStripTooltipRendererProvider getErrorStripTooltipRendererProvider() { return myTooltipRendererProvider; } @Override @NotNull public Editor getEditor() { return myEditor; } @RequiredDispatchThread @Override public void setErrorStripeRenderer(ErrorStripeRenderer renderer) { assertIsDispatchThread(); if (myErrorStripeRenderer instanceof Disposable) { Disposer.dispose((Disposable)myErrorStripeRenderer); } myErrorStripeRenderer = renderer; //try to not cancel tooltips here, since it is being called after every writeAction, even to the console //HintManager.getInstance().getTooltipController().cancelTooltips(); repaintVerticalScrollBar(); } @RequiredDispatchThread private static void assertIsDispatchThread() { ApplicationManagerEx.getApplicationEx().assertIsDispatchThread(); } @Override public ErrorStripeRenderer getErrorStripeRenderer() { return myErrorStripeRenderer; } @Override public void dispose() { final MyErrorPanel panel = getErrorPanel(); if (panel != null) { panel.setPopupHandler(null); } if (myErrorStripeRenderer instanceof Disposable) { Disposer.dispose((Disposable)myErrorStripeRenderer); } myErrorStripeRenderer = null; super.dispose(); } // startOffset == -1 || endOffset == -1 means whole document void repaint(int startOffset, int endOffset) { ProperTextRange range = offsetsToYPositions(startOffset, endOffset); markDirtied(range); if (startOffset == -1 || endOffset == -1) { myDirtyYPositions = WHOLE_DOCUMENT; } if (myErrorPanel != null) { myErrorPanel.repaint(0, range.getStartOffset(), myErrorPanel.getWidth(), range.getLength() + myMinMarkHeight); } } private boolean isMirrored() { return myEditor.isMirrored(); } private class MyErrorPanel extends JPanel implements MouseMotionListener, MouseListener, MouseWheelListener, UISettingsListener { private PopupHandler myHandler; @Nullable private BufferedImage myCachedTrack; private int myCachedHeight = -1; private MyErrorPanel() { setOpaque(true); addMouseListener(this); addMouseMotionListener(this); } @Override public void updateUI() { super.updateUI(); myCachedTrack = null; } @Override public void uiSettingsChanged(UISettings source) { if (!UISettings.getInstance().SHOW_EDITOR_TOOLTIP) { hideMyEditorPreviewHint(); } } @Override public Dimension getPreferredSize() { // icon scale are different than simple scale return new Dimension(JBUI.scale(1) + HighlightDisplayLevel.getEmptyIconDim(), 0); } @Override protected void paintComponent(Graphics g) { if (UISettings.getInstance().PRESENTATION_MODE) { g.setColor(getEditor().getColorsScheme().getDefaultBackground()); g.fillRect(0, 0, getWidth(), getHeight()); return; } Rectangle componentBounds = getBounds(); ProperTextRange docRange = ProperTextRange.create(0, componentBounds.height); if (myCachedTrack == null || myCachedHeight != componentBounds.height) { myCachedTrack = UIUtil.createImage(componentBounds.width, componentBounds.height, BufferedImage.TYPE_INT_ARGB); myCachedHeight = componentBounds.height; myDirtyYPositions = docRange; paintBackground(myCachedTrack.getGraphics(), new Rectangle(0, 0, componentBounds.width, componentBounds.height)); } if (myDirtyYPositions == WHOLE_DOCUMENT) { myDirtyYPositions = docRange; } if (myDirtyYPositions != null) { final Graphics2D imageGraphics = myCachedTrack.createGraphics(); ((ApplicationImpl)ApplicationManager.getApplication()).editorPaintStart(); try { myDirtyYPositions = myDirtyYPositions.intersection(docRange); if (myDirtyYPositions == null) myDirtyYPositions = docRange; repaint(imageGraphics, componentBounds.width, myDirtyYPositions); myDirtyYPositions = null; } finally { ((ApplicationImpl)ApplicationManager.getApplication()).editorPaintFinish(); } } UIUtil.drawImage(g, myCachedTrack, null, 0, 0); if (myErrorStripeRenderer != null) { myErrorStripeRenderer.paint(this, g, new Point(JBUI.scale(1), 0)); } } private void paintBackground(Graphics g, Rectangle bounds) { if (UISettings.getInstance().PRESENTATION_MODE) { return; } g.setColor(ButtonlessScrollBarUI.getTrackBackground()); g.fillRect(bounds.x, bounds.y, bounds.width, bounds.height); g.setColor(UIUtil.getBorderColor()); int border = isMirrored() ? bounds.x + bounds.width - getBorderWidth() : bounds.x; g.drawLine(border, bounds.y, border, bounds.y + bounds.height + getBorderWidth()); } private int getBorderWidth() { return JBUI.scale(1); } private void repaint(@NotNull final Graphics g, int gutterWidth, @NotNull ProperTextRange yrange) { final Rectangle clip = new Rectangle(0, yrange.getStartOffset(), gutterWidth, yrange.getLength() + myMinMarkHeight); paintBackground(g, clip); Document document = myEditor.getDocument(); int startOffset = yPositionToOffset(clip.y - myMinMarkHeight, true); int endOffset = yPositionToOffset(clip.y + clip.height, false); Shape oldClip = g.getClip(); g.clipRect(clip.x, clip.y, clip.width, clip.height); drawMarkup(g, startOffset, endOffset, (MarkupModelEx)DocumentMarkupModel.forDocument(document, myEditor.getProject(), true), EditorMarkupModelImpl.this); g.setClip(oldClip); } private void drawMarkup(@NotNull final Graphics g, int startOffset, int endOffset, @NotNull MarkupModelEx markup1, @NotNull MarkupModelEx markup2) { final Queue<PositionedStripe> thinEnds = new PriorityQueue<PositionedStripe>(5, new Comparator<PositionedStripe>() { @Override public int compare(@NotNull PositionedStripe o1, @NotNull PositionedStripe o2) { return o1.yEnd - o2.yEnd; } }); final Queue<PositionedStripe> wideEnds = new PriorityQueue<PositionedStripe>(5, new Comparator<PositionedStripe>() { @Override public int compare(@NotNull PositionedStripe o1, @NotNull PositionedStripe o2) { return o1.yEnd - o2.yEnd; } }); // sorted by layer final List<PositionedStripe> thinStripes = new ArrayList<PositionedStripe>(); // layer desc final List<PositionedStripe> wideStripes = new ArrayList<PositionedStripe>(); // layer desc final int[] thinYStart = new int[1]; // in range 0..yStart all spots are drawn final int[] wideYStart = new int[1]; // in range 0..yStart all spots are drawn MarkupIterator<RangeHighlighterEx> iterator1 = markup1.overlappingIterator(startOffset, endOffset); MarkupIterator<RangeHighlighterEx> iterator2 = markup2.overlappingIterator(startOffset, endOffset); MarkupIterator<RangeHighlighterEx> iterator = IntervalTreeImpl.mergeIterators(iterator1, iterator2, RangeHighlighterEx.BY_AFFECTED_START_OFFSET); try { ContainerUtil.process(iterator, new Processor<RangeHighlighterEx>() { @Override public boolean process(@NotNull RangeHighlighterEx highlighter) { Color color = highlighter.getErrorStripeMarkColor(); if (color == null) return true; boolean isThin = highlighter.isThinErrorStripeMark(); int[] yStart = isThin ? thinYStart : wideYStart; List<PositionedStripe> stripes = isThin ? thinStripes : wideStripes; Queue<PositionedStripe> ends = isThin ? thinEnds : wideEnds; ProperTextRange range = offsetsToYPositions(highlighter.getStartOffset(), highlighter.getEndOffset()); final int ys = range.getStartOffset(); int ye = range.getEndOffset(); if (ye - ys < myMinMarkHeight) ye = ys + myMinMarkHeight; yStart[0] = drawStripesEndingBefore(ys, ends, stripes, g, yStart[0]); final int layer = highlighter.getLayer(); PositionedStripe stripe = null; int i; for (i = 0; i < stripes.size(); i++) { PositionedStripe s = stripes.get(i); if (s.layer == layer) { stripe = s; break; } if (s.layer < layer) { break; } } if (stripe == null) { // started new stripe, draw previous above if (i == 0 && yStart[0] != ys) { if (!stripes.isEmpty()) { PositionedStripe top = stripes.get(0); drawSpot(g, top.thin, yStart[0], ys, top.color); } yStart[0] = ys; } stripe = new PositionedStripe(color, ye, isThin, layer); stripes.add(i, stripe); ends.offer(stripe); } else { if (stripe.yEnd < ye) { if (!color.equals(stripe.color)) { // paint previous stripe on this layer if (i == 0 && yStart[0] != ys) { drawSpot(g, stripe.thin, yStart[0], ys, stripe.color); yStart[0] = ys; } stripe.color = color; } // key changed, reinsert into queue ends.remove(stripe); stripe.yEnd = ye; ends.offer(stripe); } } return true; } }); } finally { iterator.dispose(); } drawStripesEndingBefore(Integer.MAX_VALUE, thinEnds, thinStripes, g, thinYStart[0]); drawStripesEndingBefore(Integer.MAX_VALUE, wideEnds, wideStripes, g, wideYStart[0]); } private int drawStripesEndingBefore(int ys, @NotNull Queue<PositionedStripe> ends, @NotNull List<PositionedStripe> stripes, @NotNull Graphics g, int yStart) { while (!ends.isEmpty()) { PositionedStripe endingStripe = ends.peek(); if (endingStripe.yEnd > ys) break; ends.remove(); // check whether endingStripe got obscured in the range yStart..endingStripe.yEnd int i = stripes.indexOf(endingStripe); stripes.remove(i); if (i == 0) { // visible drawSpot(g, endingStripe.thin, yStart, endingStripe.yEnd, endingStripe.color); yStart = endingStripe.yEnd; } } return yStart; } private void drawSpot(@NotNull Graphics g, boolean thinErrorStripeMark, int yStart, int yEnd, @NotNull Color color) { int paintWidth; int x; if (thinErrorStripeMark) { //noinspection SuspiciousNameCombination paintWidth = myMinMarkHeight; x = isMirrored() ? getWidth() - paintWidth - getBorderWidth() : getBorderWidth(); if (yEnd - yStart < 6) { yStart -= JBUI.scale(1); yEnd += yEnd - yStart - JBUI.scale(1); } } else { x = isMirrored() ? getBorderWidth() : myMinMarkHeight + getBorderWidth(); paintWidth = getWidth() - myMinMarkHeight; } g.setColor(color); g.fillRect(x, yStart, paintWidth, yEnd - yStart); } // mouse events @Override @RequiredDispatchThread public void mouseClicked(final MouseEvent e) { CommandProcessor.getInstance().executeCommand(myEditor.getProject(), new Runnable() { @Override @RequiredDispatchThread public void run() { doMouseClicked(e); } }, EditorBundle.message("move.caret.command.name"), DocCommandGroupId.noneGroupId(getDocument()), UndoConfirmationPolicy.DEFAULT, getDocument()); } @Override public void mousePressed(MouseEvent e) { } @Override public void mouseReleased(MouseEvent e) { } @RequiredDispatchThread private void doMouseClicked(MouseEvent e) { IdeFocusManager.getGlobalInstance().doWhenFocusSettlesDown(() -> { IdeFocusManager.getGlobalInstance().requestFocus(myEditor.getContentComponent(), true); }); int lineCount = getDocument().getLineCount() + myEditor.getSettings().getAdditionalLinesCount(); if (lineCount == 0) { return; } if (e.getX() > 0 && e.getX() <= getWidth()) { doClick(e); } } @Override public void mouseMoved(MouseEvent e) { int lineCount = getDocument().getLineCount() + myEditor.getSettings().getAdditionalLinesCount(); if (lineCount == 0) { return; } if (myErrorStripeRenderer != null && e.getY() < myErrorStripeRenderer.getSquareSize()) { showTrafficLightTooltip(e); return; } if (e.getX() > 0 && e.getX() <= getWidth() && showToolTipByMouseMove(e)) { setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)); return; } cancelMyToolTips(e, false); if (getCursor().equals(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR))) { setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR)); } } @Override public void mouseWheelMoved(MouseWheelEvent e) { if (myEditorPreviewHint == null) return; myWheelAccumulator += (e.getScrollType() == MouseWheelEvent.WHEEL_UNIT_SCROLL ? e.getUnitsToScroll() * e.getScrollAmount() : e.getWheelRotation() < 0 ? -e.getScrollAmount() : e.getScrollAmount()); myRowAdjuster = myWheelAccumulator / myEditor.getLineHeight(); showToolTipByMouseMove(e); } private TrafficTooltipRenderer myTrafficTooltipRenderer; private void showTrafficLightTooltip(MouseEvent e) { if (myTrafficTooltipRenderer == null) { myTrafficTooltipRenderer = myTooltipRendererProvider.createTrafficTooltipRenderer(new Runnable() { @Override public void run() { myTrafficTooltipRenderer = null; } }, myEditor); } showTooltip(e, myTrafficTooltipRenderer, new HintHint(e).setAwtTooltip(true).setMayCenterPosition(true).setContentActive(false).setPreferredPosition(Balloon.Position.atLeft)); } private void repaintTrafficTooltip() { if (myTrafficTooltipRenderer != null) { myTrafficTooltipRenderer.repaintTooltipWindow(); } } private void cancelMyToolTips(final MouseEvent e, boolean checkIfShouldSurvive) { hideMyEditorPreviewHint(); final TooltipController tooltipController = TooltipController.getInstance(); if (!checkIfShouldSurvive || !tooltipController.shouldSurvive(e)) { tooltipController.cancelTooltip(ERROR_STRIPE_TOOLTIP_GROUP, e, true); } } private void hideMyEditorPreviewHint() { if (myEditorPreviewHint != null) { myEditorPreviewHint.hide(); myEditorPreviewHint = null; myRowAdjuster = 0; myWheelAccumulator = 0; } } @Override public void mouseEntered(MouseEvent e) { } @Override public void mouseExited(MouseEvent e) { cancelMyToolTips(e, true); } @Override public void mouseDragged(MouseEvent e) { cancelMyToolTips(e, true); } private void setPopupHandler(@Nullable PopupHandler handler) { if (myHandler != null) { removeMouseListener(myHandler); } if (handler != null) { myHandler = handler; addMouseListener(handler); } } } private void showTooltip(MouseEvent e, final TooltipRenderer tooltipObject, @NotNull HintHint hintHint) { TooltipController tooltipController = TooltipController.getInstance(); tooltipController.showTooltipByMouseMove(myEditor, new RelativePoint(e), tooltipObject, myEditor.getVerticalScrollbarOrientation() == EditorEx.VERTICAL_SCROLLBAR_RIGHT, ERROR_STRIPE_TOOLTIP_GROUP, hintHint); } @RequiredDispatchThread private void fireErrorMarkerClicked(RangeHighlighter marker, MouseEvent e) { ApplicationManager.getApplication().assertIsDispatchThread(); ErrorStripeEvent event = new ErrorStripeEvent(getEditor(), e, marker); for (ErrorStripeListener listener : myErrorMarkerListeners) { listener.errorMarkerClicked(event); } } @Override public void addErrorMarkerListener(@NotNull final ErrorStripeListener listener, @NotNull Disposable parent) { ContainerUtil.add(listener, myErrorMarkerListeners, parent); } public void markDirtied(@NotNull ProperTextRange yPositions) { if (myDirtyYPositions != WHOLE_DOCUMENT) { int start = Math.max(0, yPositions.getStartOffset() - myEditor.getLineHeight()); int end = myEditorScrollbarTop + myEditorTargetHeight == 0 ? yPositions.getEndOffset() + myEditor.getLineHeight() : Math.min(myEditorScrollbarTop + myEditorTargetHeight, yPositions.getEndOffset() + myEditor.getLineHeight()); ProperTextRange adj = new ProperTextRange(start, Math.max(end, start)); myDirtyYPositions = myDirtyYPositions == null ? adj : myDirtyYPositions.union(adj); } myEditorScrollbarTop = 0; myEditorSourceHeight = 0; myEditorTargetHeight = 0; dimensionsAreValid = false; } @Override public void setMinMarkHeight(final int minMarkHeight) { myMinMarkHeight = JBUI.scale(minMarkHeight); } @Override public boolean isErrorStripeVisible() { return getErrorPanel() != null; } private static class BasicTooltipRendererProvider implements ErrorStripTooltipRendererProvider { @Override public TooltipRenderer calcTooltipRenderer(@NotNull final Collection<RangeHighlighter> highlighters) { LineTooltipRenderer bigRenderer = null; //do not show same tooltip twice Set<String> tooltips = null; for (RangeHighlighter highlighter : highlighters) { final Object tooltipObject = highlighter.getErrorStripeTooltip(); if (tooltipObject == null) continue; final String text = tooltipObject.toString(); if (tooltips == null) { tooltips = new THashSet<String>(); } if (tooltips.add(text)) { if (bigRenderer == null) { bigRenderer = new LineTooltipRenderer(text, new Object[]{highlighters}); } else { bigRenderer.addBelow(text); } } } return bigRenderer; } @NotNull @Override public TooltipRenderer calcTooltipRenderer(@NotNull final String text) { return new LineTooltipRenderer(text, new Object[]{text}); } @NotNull @Override public TooltipRenderer calcTooltipRenderer(@NotNull final String text, final int width) { return new LineTooltipRenderer(text, width, new Object[]{text}); } @NotNull @Override public TrafficTooltipRenderer createTrafficTooltipRenderer(@NotNull final Runnable onHide, @NotNull Editor editor) { return new TrafficTooltipRenderer() { @Override public void repaintTooltipWindow() { } @Override public LightweightHint show(@NotNull Editor editor, @NotNull Point p, boolean alignToRight, @NotNull TooltipGroup group, @NotNull HintHint hintHint) { JLabel label = new JLabel("WTF"); return new LightweightHint(label) { @Override public void hide() { super.hide(); onHide.run(); } }; } }; } } @NotNull private ProperTextRange offsetsToYPositions(int start, int end) { if (!dimensionsAreValid) { recalcEditorDimensions(); } Document document = myEditor.getDocument(); int startLineNumber = end == -1 ? 0 : offsetToLine(start, document); int startY; int lineCount; if (myEditorSourceHeight < myEditorTargetHeight) { lineCount = 0; startY = myEditorScrollbarTop + startLineNumber * myEditor.getLineHeight(); } else { lineCount = myEditorSourceHeight / myEditor.getLineHeight(); startY = myEditorScrollbarTop + (int)((float)startLineNumber / lineCount * myEditorTargetHeight); } int endY; int endLineNumber = offsetToLine(end, document); if (end == -1 || start == -1) { endY = Math.min(myEditorSourceHeight, myEditorTargetHeight); } else if (start == end || offsetToLine(start, document) == endLineNumber) { endY = startY; // both offsets are on the same line, no need to recalc Y position } else { if (myEditorSourceHeight < myEditorTargetHeight) { endY = myEditorScrollbarTop + endLineNumber * myEditor.getLineHeight(); } else { endY = myEditorScrollbarTop + (int)((float)endLineNumber / lineCount * myEditorTargetHeight); } } if (endY < startY) endY = startY; if (startY < 0 || endY < 0) { //LOGGER.error("Bad text range startY=" + startY + // ", endY=" + endY + // ", myEditorSourceHeight=" + myEditorSourceHeight + // ", myEditorTargetHeight=" + myEditorTargetHeight + // ", myEditorScrollbarTop=" + myEditorScrollbarTop); return new ProperTextRange(0, 0); } return new ProperTextRange(startY, endY); } private int yPositionToOffset(int y, boolean beginLine) { if (!dimensionsAreValid) { recalcEditorDimensions(); } final int safeY = Math.max(0, y - myEditorScrollbarTop); VisualPosition visual; if (myEditorSourceHeight < myEditorTargetHeight) { visual = myEditor.xyToVisualPosition(new Point(0, safeY)); } else { float fraction = Math.max(0, Math.min(1, safeY / (float)myEditorTargetHeight)); final int lineCount = myEditorSourceHeight / myEditor.getLineHeight(); visual = new VisualPosition((int)(fraction * lineCount), 0); } int line = myEditor.visualToLogicalPosition(visual).line; Document document = myEditor.getDocument(); if (line < 0) return 0; if (line >= document.getLineCount()) return document.getTextLength(); final FoldingModelEx foldingModel = myEditor.getFoldingModel(); if (beginLine) { final int offset = document.getLineStartOffset(line); final FoldRegion startCollapsed = foldingModel.getCollapsedRegionAtOffset(offset); return startCollapsed != null ? Math.min(offset, startCollapsed.getStartOffset()) : offset; } else { final int offset = document.getLineEndOffset(line); final FoldRegion startCollapsed = foldingModel.getCollapsedRegionAtOffset(offset); return startCollapsed != null ? Math.max(offset, startCollapsed.getEndOffset()) : offset; } } private class EditorFragmentRenderer implements TooltipRenderer { private int myVisualLine; private boolean myShowInstantly; private final List<RangeHighlighterEx> myHighlighters = new ArrayList<RangeHighlighterEx>(); private BufferedImage myCacheLevel1; private BufferedImage myCacheLevel2; private int myCacheStartLine; private int myCacheEndLine; private int myStartVisualLine; private int myEndVisualLine; private int myRelativeY; private boolean myDelayed = false; private boolean isDirty = false; private final AtomicReference<Point> myPointHolder = new AtomicReference<Point>(); private final AtomicReference<HintHint> myHintHolder = new AtomicReference<HintHint>(); private EditorFragmentRenderer() { update(-1, Collections.<RangeHighlighterEx>emptyList(), false); } void update(int visualLine, Collection<RangeHighlighterEx> rangeHighlighters, boolean showInstantly) { myVisualLine = visualLine; myShowInstantly = showInstantly; myHighlighters.clear(); if (myVisualLine == -1) return; int oldStartLine = myStartVisualLine; int oldEndLine = myEndVisualLine; myStartVisualLine = fitLineToEditor(myVisualLine - myPreviewLines); myEndVisualLine = fitLineToEditor(myVisualLine + myPreviewLines); isDirty |= oldStartLine != myStartVisualLine || oldEndLine != myEndVisualLine; for (RangeHighlighterEx rangeHighlighter : rangeHighlighters) { myHighlighters.add(rangeHighlighter); } Collections.sort(myHighlighters, new Comparator<RangeHighlighterEx>() { @Override public int compare(RangeHighlighterEx ex1, RangeHighlighterEx ex2) { LogicalPosition startPos1 = myEditor.offsetToLogicalPosition(ex1.getAffectedAreaStartOffset()); LogicalPosition startPos2 = myEditor.offsetToLogicalPosition(ex2.getAffectedAreaStartOffset()); if (startPos1.line != startPos2.line) return 0; return startPos1.column - startPos2.column; } }); } @Override public LightweightHint show(@NotNull final Editor editor, @NotNull Point p, boolean alignToRight, @NotNull TooltipGroup group, @NotNull final HintHint hintInfo) { final HintManagerImpl hintManager = HintManagerImpl.getInstanceImpl(); boolean needDelay = false; if (myEditorPreviewHint == null) { needDelay = true; final JPanel editorFragmentPreviewPanel = new JPanel() { private static final int R = 6; @Override public Dimension getPreferredSize() { int width = myEditor.getGutterComponentEx().getWidth() + myEditor.getScrollingModel().getVisibleArea().width; if (!ToolWindowManagerEx.getInstanceEx(myEditor.getProject()).getIdsOn(ToolWindowAnchor.LEFT).isEmpty()) width--; return new Dimension(width - BalloonImpl.POINTER_WIDTH, myEditor.getLineHeight() * (myEndVisualLine - myStartVisualLine)); } @Override protected void paintComponent(Graphics g) { if (myVisualLine == -1) return; Dimension size = getPreferredSize(); EditorGutterComponentEx gutterComponentEx = myEditor.getGutterComponentEx(); int gutterWidth = gutterComponentEx.getWidth(); if (myCacheLevel2 == null || myCacheStartLine > myStartVisualLine || myCacheEndLine < myEndVisualLine) { myCacheStartLine = fitLineToEditor(myVisualLine - myCachePreviewLines); myCacheEndLine = fitLineToEditor(myCacheStartLine + 2 * myCachePreviewLines + JBUI.scale(1)); if (myCacheLevel2 == null) { myCacheLevel2 = UIUtil.createImage(size.width, myEditor.getLineHeight() * (2 * myCachePreviewLines + JBUI.scale(1)), BufferedImage.TYPE_INT_RGB); } Graphics2D cg = myCacheLevel2.createGraphics(); final AffineTransform t = cg.getTransform(); EditorUIUtil.setupAntialiasing(cg); int lineShift = -myEditor.getLineHeight() * myCacheStartLine; AffineTransform translateInstance = AffineTransform.getTranslateInstance(-JBUI.scale(3), lineShift); translateInstance.preConcatenate(t); cg.setTransform(translateInstance); cg.setClip(0, -lineShift, gutterWidth, myCacheLevel2.getHeight()); gutterComponentEx.paint(cg); translateInstance = AffineTransform.getTranslateInstance(gutterWidth - JBUI.scale(3), lineShift); translateInstance.preConcatenate(t); cg.setTransform(translateInstance); EditorComponentImpl contentComponent = myEditor.getContentComponent(); cg.setClip(0, -lineShift, contentComponent.getWidth(), myCacheLevel2.getHeight()); contentComponent.paint(cg); } if (myCacheLevel1 == null) { myCacheLevel1 = UIUtil.createImage(size.width, myEditor.getLineHeight() * (2 * myPreviewLines + JBUI.scale(1)), BufferedImage.TYPE_INT_RGB); isDirty = true; } if (isDirty) { myRelativeY = SwingUtilities.convertPoint(this, 0, 0, myEditor.getScrollPane()).y; Graphics2D g2d = myCacheLevel1.createGraphics(); final AffineTransform transform = g2d.getTransform(); EditorUIUtil.setupAntialiasing(g2d); GraphicsUtil.setupAAPainting(g2d); g2d.setColor(myEditor.getBackgroundColor()); g2d.fillRect(0, 0, getWidth(), getHeight()); AffineTransform translateInstance = AffineTransform.getTranslateInstance(gutterWidth, myEditor.getLineHeight() * (myCacheStartLine - myStartVisualLine)); translateInstance.preConcatenate(transform); g2d.setTransform(translateInstance); UIUtil.drawImage(g2d, myCacheLevel2, -gutterWidth, 0, null); TIntIntHashMap rightEdges = new TIntIntHashMap(); int h = myEditor.getLineHeight() - 2; for (RangeHighlighterEx ex : myHighlighters) { int hEndOffset = ex.getAffectedAreaEndOffset(); Object tooltip = ex.getErrorStripeTooltip(); if (tooltip == null) continue; String s = String.valueOf(tooltip); if (s.isEmpty()) continue; s = s.replaceAll(" ", " ").replaceAll("\\s+", " "); LogicalPosition logicalPosition = myEditor.offsetToLogicalPosition(hEndOffset); int endOfLineOffset = myEditor.getDocument().getLineEndOffset(logicalPosition.line); logicalPosition = myEditor.offsetToLogicalPosition(endOfLineOffset); Point placeToShow = myEditor.logicalPositionToXY(logicalPosition); logicalPosition = myEditor.xyToLogicalPosition(placeToShow);//wraps&foldings workaround placeToShow.x += R * 3 / 2; placeToShow.y -= myCacheStartLine * myEditor.getLineHeight() - 1; Font font = myEditor.getColorsScheme().getFont(EditorFontType.PLAIN); g2d.setFont(font.deriveFont(font.getSize() * .8F)); int w = g2d.getFontMetrics().stringWidth(s); int rightEdge = rightEdges.get(logicalPosition.line); placeToShow.x = Math.max(placeToShow.x, rightEdge); rightEdge = Math.max(rightEdge, placeToShow.x + w + 3 * R); rightEdges.put(logicalPosition.line, rightEdge); g2d.setColor(MessageType.WARNING.getPopupBackground()); g2d.fillRoundRect(placeToShow.x, placeToShow.y, w + JBUI.scale(2) * R, h, R, R); g2d.setColor(new JBColor(JBColor.GRAY, Gray._200)); g2d.drawRoundRect(placeToShow.x, placeToShow.y, w + JBUI.scale(2) * R, h, R, R); g2d.setColor(JBColor.foreground()); g2d.drawString(s, placeToShow.x + R, placeToShow.y + h - g2d.getFontMetrics(g2d.getFont()).getDescent() / 2 - 2); } isDirty = false; } Graphics2D g2 = (Graphics2D)g.create(); try { GraphicsUtil.setupAAPainting(g2); g2.setClip(new RoundRectangle2D.Double(0, 0, size.width - .5, size.height - .5, 2, 2)); UIUtil.drawImage(g2, myCacheLevel1, 0, 0, this); if (UIUtil.isUnderDarkBuildInLaf()) { //Add glass effect Shape s = new Rectangle(0, 0, size.width, size.height); double cx = size.width / 2; double cy = 0; double rx = size.width / 10; int ry = myEditor.getLineHeight() * 3 / 2; g2.setPaint(new GradientPaint(0, 0, Gray._255.withAlpha(75), 0, ry, Gray._255.withAlpha(10))); double pseudoMajorAxis = size.width - rx * 9 / 5; Shape topShape1 = new Ellipse2D.Double(cx - rx - pseudoMajorAxis / 2, cy - ry, 2 * rx, 2 * ry); Shape topShape2 = new Ellipse2D.Double(cx - rx + pseudoMajorAxis / 2, cy - ry, 2 * rx, 2 * ry); Area topArea = new Area(topShape1); topArea.add(new Area(topShape2)); topArea.add(new Area(new Rectangle.Double(cx - pseudoMajorAxis / 2, cy, pseudoMajorAxis, ry))); g2.fill(topArea); Area bottomArea = new Area(s); bottomArea.subtract(topArea); g2.setPaint(new GradientPaint(0, size.height - ry, Gray._0.withAlpha(10), 0, size.height, Gray._255.withAlpha(30))); g2.fill(bottomArea); } } finally { g2.dispose(); } } }; myEditorPreviewHint = new LightweightHint(editorFragmentPreviewPanel) { @Override public void hide(boolean ok) { super.hide(ok); myCacheLevel1 = null; if (myCacheLevel2 != null) { myCacheLevel2 = null; myCacheStartLine = -1; myCacheEndLine = -1; } myDelayed = false; } }; myEditorPreviewHint.setForceLightweightPopup(true); } Point point = new Point(hintInfo.getOriginalPoint()); hintInfo.setTextBg(myEditor.getColorsScheme().getDefaultBackground()); hintInfo.setBorderColor(myEditor.getColorsScheme().getDefaultForeground()); point = SwingUtilities.convertPoint(((EditorImpl)editor).getVerticalScrollBar(), point, myEditor.getComponent().getRootPane()); myPointHolder.set(point); myHintHolder.set(hintInfo); if (needDelay && !myShowInstantly) { myDelayed = true; Alarm alarm = new Alarm(); alarm.addRequest(new Runnable() { @Override public void run() { if (myEditorPreviewHint == null || !myDelayed) return; showEditorHint(hintManager, myPointHolder.get(), myHintHolder.get()); myDelayed = false; } }, /*Registry.intValue("ide.tooltip.initialDelay")*/300); } else if (!myDelayed) { showEditorHint(hintManager, point, hintInfo); } return myEditorPreviewHint; } private void showEditorHint(HintManagerImpl hintManager, Point point, HintHint hintInfo) { int flags = HintManager.HIDE_BY_ANY_KEY | HintManager.HIDE_BY_TEXT_CHANGE | HintManager.HIDE_BY_MOUSEOVER | HintManager.HIDE_BY_ESCAPE | HintManager.HIDE_BY_SCROLLING; hintManager.showEditorHint(myEditorPreviewHint, myEditor, point, flags, 0, false, hintInfo); } } }