/* * Copyright 2000-2017 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. */ package com.intellij.openapi.editor.impl.view; import com.intellij.diagnostic.Dumpable; import com.intellij.openapi.Disposable; import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.editor.FoldRegion; import com.intellij.openapi.editor.LogicalPosition; import com.intellij.openapi.editor.VisualPosition; import com.intellij.openapi.editor.colors.EditorFontType; import com.intellij.openapi.editor.event.VisibleAreaEvent; import com.intellij.openapi.editor.event.VisibleAreaListener; import com.intellij.openapi.editor.ex.DocumentEx; import com.intellij.openapi.editor.ex.EditorSettingsExternalizable; import com.intellij.openapi.editor.ex.util.EditorUtil; import com.intellij.openapi.editor.impl.DocumentImpl; import com.intellij.openapi.editor.impl.EditorImpl; import com.intellij.openapi.editor.impl.FontInfo; import com.intellij.openapi.editor.impl.TextDrawingCallback; import com.intellij.openapi.editor.markup.TextAttributes; import com.intellij.openapi.util.Disposer; import com.intellij.openapi.util.Key; import com.intellij.openapi.util.text.StringUtil; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.TestOnly; import java.awt.*; import java.awt.event.HierarchyEvent; import java.awt.event.HierarchyListener; import java.awt.font.FontRenderContext; import java.awt.geom.Point2D; import java.text.Bidi; /** * A facade for components responsible for drawing editor contents, managing editor size * and coordinate conversions (offset <-> logical position <-> visual position <-> x,y). * * Also contains a cache of several font-related quantities (line height, space width, etc). */ public class EditorView implements TextDrawingCallback, Disposable, Dumpable, HierarchyListener, VisibleAreaListener { private static Key<LineLayout> FOLD_REGION_TEXT_LAYOUT = Key.create("text.layout"); private final EditorImpl myEditor; private final DocumentEx myDocument; private final EditorPainter myPainter; private final EditorCoordinateMapper myMapper; private final EditorSizeManager mySizeManager; private final TextLayoutCache myTextLayoutCache; private final LogicalPositionCache myLogicalPositionCache; private final TabFragment myTabFragment; private FontRenderContext myFontRenderContext; private String myPrefixText; // accessed only in EDT private LineLayout myPrefixLayout; // guarded by myLock private TextAttributes myPrefixAttributes; // accessed only in EDT private int myBidiFlags; // accessed only in EDT private int myPlainSpaceWidth; // guarded by myLock private int myLineHeight; // guarded by myLock private int myDescent; // guarded by myLock private int myCharHeight; // guarded by myLock private int myMaxCharWidth; // guarded by myLock private int myTabSize; // guarded by myLock private int myTopOverhang; //guarded by myLock private int myBottomOverhang; //guarded by myLock private final Object myLock = new Object(); public EditorView(EditorImpl editor) { myEditor = editor; myDocument = editor.getDocument(); myPainter = new EditorPainter(this); myMapper = new EditorCoordinateMapper(this); mySizeManager = new EditorSizeManager(this); myTextLayoutCache = new TextLayoutCache(this); myLogicalPositionCache = new LogicalPositionCache(this); myTabFragment = new TabFragment(this); myEditor.getContentComponent().addHierarchyListener(this); myEditor.getScrollingModel().addVisibleAreaListener(this); Disposer.register(this, myLogicalPositionCache); Disposer.register(this, myTextLayoutCache); Disposer.register(this, mySizeManager); } EditorImpl getEditor() { return myEditor; } FontRenderContext getFontRenderContext() { return myFontRenderContext; } EditorSizeManager getSizeManager() { return mySizeManager; } TextLayoutCache getTextLayoutCache() { return myTextLayoutCache; } EditorPainter getPainter() { return myPainter; } TabFragment getTabFragment() { return myTabFragment; } LogicalPositionCache getLogicalPositionCache() { return myLogicalPositionCache; } @Override public void dispose() { myEditor.getScrollingModel().removeVisibleAreaListener(this); myEditor.getContentComponent().removeHierarchyListener(this); } @Override public void hierarchyChanged(HierarchyEvent e) { if ((e.getChangeFlags() & HierarchyEvent.SHOWING_CHANGED) != 0 && e.getComponent().isShowing()) { checkFontRenderContext(null); } } @Override public void visibleAreaChanged(VisibleAreaEvent e) { checkFontRenderContext(null); } public int yToVisualLine(int y) { return myMapper.yToVisualLine(y); } public int visualLineToY(int line) { return myMapper.visualLineToY(line); } @NotNull public LogicalPosition offsetToLogicalPosition(int offset) { assertIsReadAccess(); return myMapper.offsetToLogicalPosition(offset); } public int logicalPositionToOffset(@NotNull LogicalPosition pos) { assertIsReadAccess(); return myMapper.logicalPositionToOffset(pos); } @NotNull public VisualPosition logicalToVisualPosition(@NotNull LogicalPosition pos, boolean beforeSoftWrap) { assertIsDispatchThread(); assertNotInBulkMode(); myEditor.getSoftWrapModel().prepareToMapping(); return myMapper.logicalToVisualPosition(pos, beforeSoftWrap); } @NotNull public LogicalPosition visualToLogicalPosition(@NotNull VisualPosition pos) { assertIsDispatchThread(); assertNotInBulkMode(); myEditor.getSoftWrapModel().prepareToMapping(); return myMapper.visualToLogicalPosition(pos); } @NotNull public VisualPosition offsetToVisualPosition(int offset, boolean leanTowardsLargerOffsets, boolean beforeSoftWrap) { assertIsDispatchThread(); assertNotInBulkMode(); myEditor.getSoftWrapModel().prepareToMapping(); return myMapper.offsetToVisualPosition(offset, leanTowardsLargerOffsets, beforeSoftWrap); } public int visualPositionToOffset(VisualPosition visualPosition) { assertIsDispatchThread(); assertNotInBulkMode(); myEditor.getSoftWrapModel().prepareToMapping(); return myMapper.visualPositionToOffset(visualPosition); } public int offsetToVisualLine(int offset, boolean beforeSoftWrap) { assertIsDispatchThread(); assertNotInBulkMode(); myEditor.getSoftWrapModel().prepareToMapping(); return myMapper.offsetToVisualLine(offset, beforeSoftWrap); } public int visualLineToOffset(int visualLine) { assertIsDispatchThread(); assertNotInBulkMode(); myEditor.getSoftWrapModel().prepareToMapping(); return myMapper.visualLineToOffset(visualLine); } @NotNull public VisualPosition xyToVisualPosition(@NotNull Point2D p) { assertIsDispatchThread(); assertNotInBulkMode(); myEditor.getSoftWrapModel().prepareToMapping(); return myMapper.xyToVisualPosition(p); } @NotNull public Point2D visualPositionToXY(@NotNull VisualPosition pos) { assertIsDispatchThread(); assertNotInBulkMode(); myEditor.getSoftWrapModel().prepareToMapping(); return myMapper.visualPositionToXY(pos); } @NotNull public Point2D offsetToXY(int offset, boolean leanTowardsLargerOffsets, boolean beforeSoftWrap) { assertIsDispatchThread(); assertNotInBulkMode(); myEditor.getSoftWrapModel().prepareToMapping(); return myMapper.offsetToXY(offset, leanTowardsLargerOffsets, beforeSoftWrap); } public void setPrefix(String prefixText, TextAttributes attributes) { assertIsDispatchThread(); myPrefixText = prefixText; synchronized (myLock) { myPrefixLayout = prefixText == null || prefixText.isEmpty() ? null : LineLayout.create(this, prefixText, attributes.getFontType()); } myPrefixAttributes = attributes; mySizeManager.invalidateRange(0, 0); } public float getPrefixTextWidthInPixels() { synchronized (myLock) { return myPrefixLayout == null ? 0 : myPrefixLayout.getWidth(); } } LineLayout getPrefixLayout() { synchronized (myLock) { return myPrefixLayout; } } TextAttributes getPrefixAttributes() { return myPrefixAttributes; } public void paint(Graphics2D g) { assertIsDispatchThread(); myEditor.getSoftWrapModel().prepareToMapping(); checkFontRenderContext(g.getFontRenderContext()); myPainter.paint(g); } public void repaintCarets() { assertIsDispatchThread(); myPainter.repaintCarets(); } public Dimension getPreferredSize() { assertIsDispatchThread(); assert !myEditor.isPurePaintingMode(); myEditor.getSoftWrapModel().prepareToMapping(); return mySizeManager.getPreferredSize(); } /** * Returns preferred pixel width of the lines in range. * <p> * This method is currently used only with "idea.true.smooth.scrolling" experimental option. * * @param beginLine begin visual line (inclusive) * @param endLine end visual line (exclusive), may be greater than the actual number of lines * @return preferred pixel width */ public int getPreferredWidth(int beginLine, int endLine) { assertIsDispatchThread(); assert !myEditor.isPurePaintingMode(); myEditor.getSoftWrapModel().prepareToMapping(); return mySizeManager.getPreferredWidth(beginLine, endLine); } public int getPreferredHeight() { assertIsDispatchThread(); assert !myEditor.isPurePaintingMode(); myEditor.getSoftWrapModel().prepareToMapping(); return mySizeManager.getPreferredHeight(); } public int getMaxWidthInRange(int startOffset, int endOffset) { assertIsDispatchThread(); return getMaxWidthInLineRange(offsetToVisualLine(startOffset, false), offsetToVisualLine(endOffset, true)); } /** * If <code>quickEvaluationListener</code> is provided, quick approximate size evaluation becomes enabled, listener will be invoked * if approximation will in fact be used during width calculation. */ int getMaxWidthInLineRange(int startVisualLine, int endVisualLine) { myEditor.getSoftWrapModel().prepareToMapping(); int maxWidth = 0; VisualLinesIterator iterator = new VisualLinesIterator(myEditor, startVisualLine); while (!iterator.atEnd() && iterator.getVisualLine() <= endVisualLine) { int width = mySizeManager.getVisualLineWidth(iterator, null); maxWidth = Math.max(maxWidth, width); iterator.advance(); } return maxWidth; } public void reinitSettings() { assertIsDispatchThread(); synchronized (myLock) { myPlainSpaceWidth = -1; myTabSize = -1; } switch (EditorSettingsExternalizable.getInstance().getBidiTextDirection()) { case LTR: myBidiFlags = Bidi.DIRECTION_LEFT_TO_RIGHT; break; case RTL: myBidiFlags = Bidi.DIRECTION_RIGHT_TO_LEFT; break; default: myBidiFlags = Bidi.DIRECTION_DEFAULT_LEFT_TO_RIGHT; } setFontRenderContext(null); myLogicalPositionCache.reset(false); myTextLayoutCache.resetToDocumentSize(false); invalidateFoldRegionLayouts(); setPrefix(myPrefixText, myPrefixAttributes); // recreate prefix layout mySizeManager.reset(); } public void invalidateRange(int startOffset, int endOffset) { assertIsDispatchThread(); int textLength = myDocument.getTextLength(); if (startOffset > endOffset || startOffset >= textLength || endOffset < 0) { return; } int startLine = myDocument.getLineNumber(Math.max(0, startOffset)); int endLine = myDocument.getLineNumber(Math.min(textLength, endOffset)); myTextLayoutCache.invalidateLines(startLine, endLine); mySizeManager.invalidateRange(startOffset, endOffset); } /** * Invoked when document might have changed, but no notifications were sent (for a hacky document in EditorTextFieldCellRenderer) */ public void reset() { assertIsDispatchThread(); myLogicalPositionCache.reset(true); myTextLayoutCache.resetToDocumentSize(true); mySizeManager.reset(); } public boolean isRtlLocation(@NotNull VisualPosition visualPosition) { assertIsDispatchThread(); if (myDocument.getTextLength() == 0) return false; LogicalPosition logicalPosition = visualToLogicalPosition(visualPosition); int offset = logicalPositionToOffset(logicalPosition); if (myEditor.getSoftWrapModel().getSoftWrap(offset) != null) { VisualPosition beforeWrapPosition = offsetToVisualPosition(offset, true, true); if (visualPosition.line == beforeWrapPosition.line && (visualPosition.column > beforeWrapPosition.column || visualPosition.column == beforeWrapPosition.column && visualPosition.leansRight)) { return false; } VisualPosition afterWrapPosition = offsetToVisualPosition(offset, false, false); if (visualPosition.line == afterWrapPosition.line && (visualPosition.column < afterWrapPosition.column || visualPosition.column == afterWrapPosition.column && !visualPosition.leansRight)) { return false; } } int line = myDocument.getLineNumber(offset); LineLayout layout = myTextLayoutCache.getLineLayout(line); return layout.isRtlLocation(offset - myDocument.getLineStartOffset(line), logicalPosition.leansForward); } public boolean isAtBidiRunBoundary(@NotNull VisualPosition visualPosition) { assertIsDispatchThread(); int offset = visualPositionToOffset(visualPosition); int otherSideOffset = visualPositionToOffset(visualPosition.leanRight(!visualPosition.leansRight)); return offset != otherSideOffset; } /** * Offset of nearest boundary (not equal to <code>offset</code>) on the same line is returned. <code>-1</code> is returned if * corresponding boundary is not found. */ public int findNearestDirectionBoundary(int offset, boolean lookForward) { assertIsDispatchThread(); int textLength = myDocument.getTextLength(); if (textLength == 0 || offset < 0 || offset > textLength) return -1; int line = myDocument.getLineNumber(offset); LineLayout layout = myTextLayoutCache.getLineLayout(line); int lineStartOffset = myDocument.getLineStartOffset(line); int relativeOffset = layout.findNearestDirectionBoundary(offset - lineStartOffset, lookForward); return relativeOffset < 0 ? -1 : lineStartOffset + relativeOffset; } public int getPlainSpaceWidth() { synchronized (myLock) { initMetricsIfNeeded(); return myPlainSpaceWidth; } } public int getNominalLineHeight() { synchronized (myLock) { initMetricsIfNeeded(); return myLineHeight + myTopOverhang + myBottomOverhang; } } public int getLineHeight() { synchronized (myLock) { initMetricsIfNeeded(); return myLineHeight; } } private float getVerticalScalingFactor() { if (myEditor.isOneLineMode()) return 1; float lineSpacing = myEditor.getColorsScheme().getLineSpacing(); return lineSpacing > 0 ? lineSpacing : 1; } public int getDescent() { synchronized (myLock) { return myDescent; } } public int getCharHeight() { synchronized (myLock) { initMetricsIfNeeded(); return myCharHeight; } } int getMaxCharWidth() { synchronized (myLock) { initMetricsIfNeeded(); return myMaxCharWidth; } } public int getAscent() { synchronized (myLock) { initMetricsIfNeeded(); return myLineHeight - myDescent; } } public int getTopOverhang() { synchronized (myLock) { initMetricsIfNeeded(); return myTopOverhang; } } public int getBottomOverhang() { synchronized (myLock) { initMetricsIfNeeded(); return myBottomOverhang; } } // guarded by myLock private void initMetricsIfNeeded() { if (myPlainSpaceWidth >= 0) return; FontMetrics fm = myEditor.getContentComponent().getFontMetrics(myEditor.getColorsScheme().getFont(EditorFontType.PLAIN)); int width = FontLayoutService.getInstance().charWidth(fm, ' '); myPlainSpaceWidth = width > 0 ? width : 1; myCharHeight = FontLayoutService.getInstance().charWidth(fm, 'a'); float verticalScalingFactor = getVerticalScalingFactor(); int fontMetricsHeight = FontLayoutService.getInstance().getHeight(fm); myLineHeight = (int)Math.ceil(fontMetricsHeight * verticalScalingFactor); int descent = FontLayoutService.getInstance().getDescent(fm); myDescent = (int)Math.floor(descent * verticalScalingFactor); myTopOverhang = fontMetricsHeight - myLineHeight + myDescent - descent; myBottomOverhang = descent - myDescent; // assuming that bold italic 'W' gives a good approximation of font's widest character FontMetrics fmBI = myEditor.getContentComponent().getFontMetrics(myEditor.getColorsScheme().getFont(EditorFontType.BOLD_ITALIC)); myMaxCharWidth = FontLayoutService.getInstance().charWidth(fmBI, 'W'); } public int getTabSize() { synchronized (myLock) { if (myTabSize < 0) { myTabSize = EditorUtil.getTabSize(myEditor); } return myTabSize; } } private void setFontRenderContext(FontRenderContext context) { myFontRenderContext = context == null ? FontInfo.getFontRenderContext(myEditor.getContentComponent()) : context; } private void checkFontRenderContext(FontRenderContext context) { FontRenderContext oldContext = myFontRenderContext; setFontRenderContext(context); if (!myFontRenderContext.equals(oldContext)) { myTextLayoutCache.resetToDocumentSize(false); invalidateFoldRegionLayouts(); } } LineLayout getFoldRegionLayout(FoldRegion foldRegion) { LineLayout layout = foldRegion.getUserData(FOLD_REGION_TEXT_LAYOUT); if (layout == null) { TextAttributes placeholderAttributes = myEditor.getFoldingModel().getPlaceholderAttributes(); layout = LineLayout.create(this, StringUtil.replace(foldRegion.getPlaceholderText(), "\n", " "), placeholderAttributes == null ? Font.PLAIN : placeholderAttributes.getFontType()); foldRegion.putUserData(FOLD_REGION_TEXT_LAYOUT, layout); } return layout; } void invalidateFoldRegionLayouts() { for (FoldRegion region : myEditor.getFoldingModel().getAllFoldRegions()) { region.putUserData(FOLD_REGION_TEXT_LAYOUT, null); } } Insets getInsets() { return myEditor.getContentComponent().getInsets(); } int getBidiFlags() { return myBidiFlags; } private static void assertIsDispatchThread() { ApplicationManager.getApplication().assertIsDispatchThread(); } private static void assertIsReadAccess() { ApplicationManager.getApplication().assertReadAccessAllowed(); } @Override public void drawChars(@NotNull Graphics g, @NotNull char[] data, int start, int end, int x, int y, Color color, FontInfo fontInfo) { myPainter.drawChars(g, data, start, end, x, y, color, fontInfo); } @NotNull @Override public String dumpState() { String prefixText = myPrefixText; TextAttributes prefixAttributes = myPrefixAttributes; synchronized (myLock) { return "[prefix text: " + prefixText + ", prefix attributes: " + prefixAttributes + ", space width: " + myPlainSpaceWidth + ", line height: " + myLineHeight + ", descent: " + myDescent + ", char height: " + myCharHeight + ", max char width: " + myMaxCharWidth + ", tab size: " + myTabSize + " ,size manager: " + mySizeManager.dumpState() + " ,logical position cache: " + myLogicalPositionCache.dumpState() + "]"; } } @TestOnly public void validateState() { myLogicalPositionCache.validateState(); mySizeManager.validateState(); } private void assertNotInBulkMode() { if (myDocument instanceof DocumentImpl) ((DocumentImpl)myDocument).assertNotInBulkUpdate(); else if (myDocument.isInBulkUpdate()) throw new IllegalStateException("Current operation is not available in bulk mode"); } }