/*
* 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;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.editor.*;
import com.intellij.openapi.editor.colors.EditorColors;
import com.intellij.openapi.editor.ex.RangeHighlighterEx;
import com.intellij.openapi.editor.ex.util.EditorUIUtil;
import com.intellij.openapi.editor.ex.util.EditorUtil;
import com.intellij.openapi.editor.ex.util.LexerEditorHighlighter;
import com.intellij.openapi.editor.impl.view.FontLayoutService;
import com.intellij.openapi.editor.impl.view.IterationState;
import com.intellij.openapi.editor.markup.EffectType;
import com.intellij.openapi.editor.markup.HighlighterLayer;
import com.intellij.openapi.editor.markup.HighlighterTargetArea;
import com.intellij.openapi.editor.markup.TextAttributes;
import com.intellij.openapi.util.Disposer;
import com.intellij.openapi.util.registry.Registry;
import com.intellij.openapi.util.registry.RegistryValue;
import com.intellij.ui.EditorTextField;
import com.intellij.ui.Gray;
import com.intellij.ui.JBColor;
import com.intellij.util.Consumer;
import com.intellij.util.Processor;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.util.ui.JBUI;
import com.intellij.util.ui.UIUtil;
import javax.swing.*;
import java.awt.*;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.awt.image.BufferedImage;
import java.awt.image.VolatileImage;
import java.util.ArrayList;
import java.util.List;
/**
* @author Pavel Fatin
*/
class ImmediatePainter {
private static final int DEBUG_PAUSE_DURATION = 1000;
static final RegistryValue ENABLED = Registry.get("editor.zero.latency.rendering");
static final RegistryValue DOUBLE_BUFFERING = Registry.get("editor.zero.latency.rendering.double.buffering");
private static final RegistryValue PIPELINE_FLUSH = Registry.get("editor.zero.latency.rendering.pipeline.flush");
private static final RegistryValue DEBUG = Registry.get("editor.zero.latency.rendering.debug");
private final EditorImpl myEditor;
private Image myImage;
ImmediatePainter(EditorImpl editor) {
myEditor = editor;
Disposer.register(editor.getDisposable(), () -> {
if (myImage != null) {
myImage.flush();
}
});
}
void paint(final Graphics g, final EditorActionPlan plan) {
if (ENABLED.asBoolean() && canPaintImmediately(myEditor)) {
if (plan.getCaretShift() != 1) return;
final List<EditorActionPlan.Replacement> replacements = plan.getReplacements();
if (replacements.size() != 1) return;
final EditorActionPlan.Replacement replacement = replacements.get(0);
if (replacement.getText().length() != 1) return;
final int caretOffset = replacement.getBegin();
final char c = replacement.getText().charAt(0);
paintImmediately(g, caretOffset, c);
}
}
private static boolean canPaintImmediately(final EditorImpl editor) {
final CaretModel caretModel = editor.getCaretModel();
final Caret caret = caretModel.getPrimaryCaret();
final Document document = editor.getDocument();
return !(editor.getComponent().getParent() instanceof EditorTextField) &&
document instanceof DocumentImpl &&
editor.getHighlighter() instanceof LexerEditorHighlighter &&
!editor.getSelectionModel().hasSelection() &&
caretModel.getCaretCount() == 1 &&
!isInVirtualSpace(editor, caret) &&
!isInsertion(document, caret.getOffset()) &&
!caret.isAtRtlLocation() &&
!caret.isAtBidiRunBoundary();
}
private static boolean isInVirtualSpace(final Editor editor, final Caret caret) {
return caret.getLogicalPosition().compareTo(editor.offsetToLogicalPosition(caret.getOffset())) != 0;
}
private static boolean isInsertion(final Document document, final int offset) {
return offset < document.getTextLength() && document.getCharsSequence().charAt(offset) != '\n';
}
private void paintImmediately(final Graphics g, final int offset, final char c2) {
final EditorImpl editor = myEditor;
final Document document = editor.getDocument();
final LexerEditorHighlighter highlighter = (LexerEditorHighlighter)myEditor.getHighlighter();
final EditorSettings settings = editor.getSettings();
final boolean isBlockCursor = editor.isInsertMode() == settings.isBlockCursor();
final int lineHeight = editor.getLineHeight();
final int ascent = editor.getAscent();
final int topOverhang = editor.myView.getTopOverhang();
final int bottomOverhang = editor.myView.getBottomOverhang();
final char c1 = offset == 0 ? ' ' : document.getCharsSequence().charAt(offset - 1);
final List<TextAttributes> attributes = highlighter.getAttributesForPreviousAndTypedChars(document, offset, c2);
updateAttributes(editor, offset, attributes);
final TextAttributes attributes1 = attributes.get(0);
final TextAttributes attributes2 = attributes.get(1);
if (!(canRender(attributes1) && canRender(attributes2))) {
return;
}
FontLayoutService fontLayoutService = FontLayoutService.getInstance();
final float width1 = fontLayoutService.charWidth2D(editor.getFontMetrics(attributes1.getFontType()), c1);
final float width2 = fontLayoutService.charWidth2D(editor.getFontMetrics(attributes2.getFontType()), c2);
final Font font1 = EditorUtil.fontForChar(c1, attributes1.getFontType(), editor).getFont();
final Font font2 = EditorUtil.fontForChar(c1, attributes2.getFontType(), editor).getFont();
final Point2D p2 = editor.offsetToXY(offset, false);
float p2x = (float)p2.getX();
int p2y = (int)p2.getY();
int width1i = (int)(p2x) - (int)(p2x - width1);
int width2i = (int)(p2x + width2) - (int)p2x;
Caret caret = editor.getCaretModel().getPrimaryCaret();
//noinspection ConstantConditions
final int caretWidth = isBlockCursor ? editor.getCaretLocations(false)[0].myWidth
: JBUI.scale(caret.getVisualAttributes().getWidth(settings.getLineCursorWidth()));
final float caretShift = isBlockCursor ? 0 : caretWidth == 1 ? 0 : 1 / JBUI.sysScale((Graphics2D)g);
final Rectangle2D caretRectangle = new Rectangle2D.Float((int)(p2x + width2) - caretShift, p2y - topOverhang,
caretWidth, lineHeight + topOverhang + bottomOverhang + (isBlockCursor ? -1 : 0));
final Rectangle rectangle1 = new Rectangle((int)(p2x - width1), p2y, width1i, lineHeight);
final Rectangle rectangle2 = new Rectangle((int)p2x, p2y, (int)(width2i + caretWidth - caretShift), lineHeight);
final Consumer<Graphics> painter = graphics -> {
EditorUIUtil.setupAntialiasing(graphics);
fillRect(graphics, rectangle2, attributes2.getBackgroundColor());
drawChar(graphics, c2, p2x, p2y + ascent, font2, attributes2.getForegroundColor());
fillRect(graphics, caretRectangle, getCaretColor(editor));
fillRect(graphics, rectangle1, attributes1.getBackgroundColor());
drawChar(graphics, c1, p2x - width1, p2y + ascent, font1, attributes1.getForegroundColor());
};
final Shape originalClip = g.getClip();
g.setClip(new Rectangle2D.Float((int)p2x - caretShift, p2y, width2i + caretWidth, lineHeight));
if (DOUBLE_BUFFERING.asBoolean()) {
paintWithDoubleBuffering(g, painter);
}
else {
painter.consume(g);
}
g.setClip(originalClip);
if (PIPELINE_FLUSH.asBoolean()) {
Toolkit.getDefaultToolkit().sync();
}
if (DEBUG.asBoolean()) {
pause();
}
}
private static boolean canRender(final TextAttributes attributes) {
return attributes.getEffectType() != EffectType.BOXED || attributes.getEffectColor() == null;
}
private void paintWithDoubleBuffering(final Graphics graphics, final Consumer<Graphics> painter) {
final Rectangle bounds = graphics.getClipBounds();
createOrUpdateImageBuffer(myEditor.getComponent(), bounds.getSize());
final Graphics imageGraphics = myImage.getGraphics();
imageGraphics.translate(-bounds.x, -bounds.y);
painter.consume(imageGraphics);
imageGraphics.dispose();
graphics.drawImage(myImage, bounds.x, bounds.y, null);
}
private void createOrUpdateImageBuffer(final JComponent component, final Dimension size) {
if (ApplicationManager.getApplication().isUnitTestMode()) {
if (myImage == null || !isLargeEnough(myImage, size)) {
myImage = UIUtil.createImage(size.width, size.height, BufferedImage.TYPE_INT_ARGB);
}
}
else {
if (myImage == null) {
myImage = component.createVolatileImage(size.width, size.height);
}
else if (!isLargeEnough(myImage, size) ||
((VolatileImage)myImage).validate(component.getGraphicsConfiguration()) == VolatileImage.IMAGE_INCOMPATIBLE) {
myImage.flush();
myImage = component.createVolatileImage(size.width, size.height);
}
}
}
private static boolean isLargeEnough(final Image image, final Dimension size) {
final int width = image.getWidth(null);
final int height = image.getHeight(null);
if (width == -1 || height == -1) {
throw new IllegalArgumentException("Image size is undefined");
}
return width >= size.width && height >= size.height;
}
private static void fillRect(final Graphics g, final Rectangle2D r, final Color color) {
g.setColor(color);
((Graphics2D)g).fill(r);
}
private static void drawChar(final Graphics g,
final char c,
final float x, final float y,
final Font font, final Color color) {
g.setFont(font);
g.setColor(color);
((Graphics2D)g).drawString(String.valueOf(c), x, y);
}
private static Color getCaretColor(final Editor editor) {
Color overriddenColor = editor.getCaretModel().getPrimaryCaret().getVisualAttributes().getColor();
if (overriddenColor != null) return overriddenColor;
final Color caretColor = editor.getColorsScheme().getColor(EditorColors.CARET_COLOR);
return caretColor == null ? new JBColor(Gray._0, Gray._255) : caretColor;
}
private static void updateAttributes(final EditorImpl editor, final int offset, final List<TextAttributes> attributes) {
final List<RangeHighlighterEx> list1 = new ArrayList<>();
final List<RangeHighlighterEx> list2 = new ArrayList<>();
final Processor<RangeHighlighterEx> processor = highlighter -> {
if (!highlighter.isValid()) return true;
final boolean isLineHighlighter = highlighter.getTargetArea() == HighlighterTargetArea.LINES_IN_RANGE;
if (isLineHighlighter || highlighter.getStartOffset() < offset) {
list1.add(highlighter);
}
if (isLineHighlighter || highlighter.getEndOffset() > offset ||
(highlighter.getEndOffset() == offset && (highlighter.isGreedyToRight()))) {
list2.add(highlighter);
}
return true;
};
editor.getFilteredDocumentMarkupModel().processRangeHighlightersOverlappingWith(Math.max(0, offset - 1), offset, processor);
editor.getMarkupModel().processRangeHighlightersOverlappingWith(Math.max(0, offset - 1), offset, processor);
updateAttributes(editor, attributes.get(0), list1);
updateAttributes(editor, attributes.get(1), list2);
}
// TODO Unify with com.intellij.openapi.editor.impl.view.IterationState.setAttributes
private static void updateAttributes(final EditorImpl editor,
final TextAttributes attributes,
final List<RangeHighlighterEx> highlighters) {
if (highlighters.size() > 1) {
ContainerUtil.quickSort(highlighters, IterationState.BY_LAYER_THEN_ATTRIBUTES);
}
TextAttributes syntax = attributes;
TextAttributes caretRow = editor.getCaretModel().getTextAttributes();
final int size = highlighters.size();
//noinspection ForLoopReplaceableByForEach
for (int i = 0; i < size; i++) {
RangeHighlighterEx highlighter = highlighters.get(i);
if (highlighter.getTextAttributes() == TextAttributes.ERASE_MARKER) {
syntax = null;
}
}
final List<TextAttributes> cachedAttributes = new ArrayList<>();
//noinspection ForLoopReplaceableByForEach
for (int i = 0; i < size; i++) {
RangeHighlighterEx highlighter = highlighters.get(i);
if (caretRow != null && highlighter.getLayer() < HighlighterLayer.CARET_ROW) {
cachedAttributes.add(caretRow);
caretRow = null;
}
if (syntax != null && highlighter.getLayer() < HighlighterLayer.SYNTAX) {
cachedAttributes.add(syntax);
syntax = null;
}
TextAttributes textAttributes = highlighter.getTextAttributes();
if (textAttributes != null && textAttributes != TextAttributes.ERASE_MARKER) {
cachedAttributes.add(textAttributes);
}
}
if (caretRow != null) cachedAttributes.add(caretRow);
if (syntax != null) cachedAttributes.add(syntax);
Color foreground = null;
Color background = null;
Color effect = null;
EffectType effectType = null;
int fontType = 0;
//noinspection ForLoopReplaceableByForEach, Duplicates
for (int i = 0; i < cachedAttributes.size(); i++) {
TextAttributes attrs = cachedAttributes.get(i);
if (foreground == null) {
foreground = attrs.getForegroundColor();
}
if (background == null) {
background = attrs.getBackgroundColor();
}
if (fontType == Font.PLAIN) {
fontType = attrs.getFontType();
}
if (effect == null) {
effect = attrs.getEffectColor();
effectType = attrs.getEffectType();
}
}
if (foreground == null) foreground = editor.getForegroundColor();
if (background == null) background = editor.getBackgroundColor();
if (effectType == null) effectType = EffectType.BOXED;
TextAttributes defaultAttributes = editor.getColorsScheme().getAttributes(HighlighterColors.TEXT);
if (fontType == Font.PLAIN) fontType = defaultAttributes == null ? Font.PLAIN : defaultAttributes.getFontType();
attributes.setAttributes(foreground, background, effect, null, effectType, fontType);
}
private static void pause() {
try {
Thread.sleep(DEBUG_PAUSE_DURATION);
}
catch (InterruptedException e) {
// ...
}
}
}