/* * 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.application.options.colors; import com.intellij.application.options.colors.highlighting.HighlightData; import com.intellij.application.options.colors.highlighting.HighlightsExtractor; import com.intellij.codeHighlighting.RainbowHighlighter; import com.intellij.codeInsight.daemon.UsedColors; import com.intellij.ide.highlighter.HighlighterFactory; import com.intellij.openapi.editor.*; import com.intellij.openapi.editor.colors.EditorColorsScheme; import com.intellij.openapi.editor.colors.EditorSchemeAttributeDescriptor; import com.intellij.openapi.editor.colors.TextAttributesKey; import com.intellij.openapi.editor.event.CaretEvent; import com.intellij.openapi.editor.event.CaretListener; import com.intellij.openapi.editor.ex.EditorEx; import com.intellij.openapi.editor.highlighter.EditorHighlighter; import com.intellij.openapi.editor.highlighter.HighlighterIterator; import com.intellij.openapi.editor.impl.EditorImpl; import com.intellij.openapi.fileTypes.SyntaxHighlighter; import com.intellij.openapi.options.colors.ColorSettingsPage; import com.intellij.openapi.options.colors.EditorHighlightingProvidingColorSettingsPage; import com.intellij.openapi.options.colors.RainbowColorSettingsPage; import com.intellij.openapi.util.Disposer; import com.intellij.openapi.util.TextRange; import com.intellij.psi.tree.IElementType; import com.intellij.util.Alarm; import com.intellij.util.EventDispatcher; import com.intellij.util.ui.UIUtil; import org.intellij.lang.annotations.JdkConstants; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import javax.swing.*; import java.awt.*; import java.awt.event.MouseEvent; import java.awt.event.MouseMotionAdapter; import java.util.*; import java.util.List; import static com.intellij.openapi.editor.colors.CodeInsightColors.BLINKING_HIGHLIGHTS_ATTRIBUTES; public class SimpleEditorPreview implements PreviewPanel { private static final Map<String, TextAttributesKey> INLINE_ELEMENTS = Collections.singletonMap("parameter_hint", DefaultLanguageHighlighterColors.INLINE_PARAMETER_HINT); private final ColorSettingsPage myPage; private final EditorEx myEditor; private final Alarm myBlinkingAlarm; private final List<HighlightData> myHighlightData = new ArrayList<>(); private final ColorAndFontOptions myOptions; private final EventDispatcher<ColorAndFontSettingsListener> myDispatcher = EventDispatcher.create(ColorAndFontSettingsListener.class); private final HighlightsExtractor myHighlightsExtractor; public SimpleEditorPreview(final ColorAndFontOptions options, final ColorSettingsPage page) { this(options, page, true); } public SimpleEditorPreview(final ColorAndFontOptions options, final ColorSettingsPage page, final boolean navigatable) { myOptions = options; myPage = page; myHighlightsExtractor = new HighlightsExtractor(page.getAdditionalHighlightingTagToDescriptorMap(), INLINE_ELEMENTS); myEditor = (EditorEx)FontEditorPreview.createPreviewEditor( myHighlightsExtractor.extractHighlights(page.getDemoText(), myHighlightData), // text without tags 10, 3, -1, myOptions.getSelectedScheme(), false); FontEditorPreview.installTrafficLights(myEditor); myBlinkingAlarm = new Alarm().setActivationComponent(myEditor.getComponent()); if (navigatable) { myEditor.getContentComponent().addMouseMotionListener(new MouseMotionAdapter() { @Override public void mouseMoved(MouseEvent e) { navigate(false, myEditor.xyToLogicalPosition(new Point(e.getX(), e.getY()))); } }); myEditor.getCaretModel().addCaretListener(new CaretListener() { @Override public void caretPositionChanged(CaretEvent e) { navigate(true, e.getNewPosition()); } }); } } public EditorEx getEditor() { return myEditor; } public void setDemoText(final String text) { myEditor.getSelectionModel().removeSelection(); myHighlightData.clear(); myEditor.getDocument().setText(myHighlightsExtractor.extractHighlights(text, myHighlightData)); } private void navigate(boolean select, @NotNull final LogicalPosition pos) { int offset = myEditor.logicalPositionToOffset(pos); final SyntaxHighlighter highlighter = myPage.getHighlighter(); String type; HighlightData highlightData = getDataFromOffset(offset); if (highlightData != null) { // tag-based navigation first type = RainbowHighlighter.isRainbowTempKey(highlightData.getHighlightKey()) ? RainbowHighlighter.RAINBOW_TYPE : highlightData.getHighlightType(); } else { // if failed, try the highlighter-based navigation type = selectItem(myEditor.getHighlighter().createIterator(offset), highlighter); } setCursor(type == null ? Cursor.TEXT_CURSOR : Cursor.HAND_CURSOR); if (select && type != null) { myDispatcher.getMulticaster().selectionInPreviewChanged(type); } } @Nullable private HighlightData getDataFromOffset(int offset) { for (HighlightData highlightData : myHighlightData) { if (offset >= highlightData.getStartOffset() && offset <= highlightData.getEndOffset()) { return highlightData; } } return null; } @Nullable private static String selectItem(HighlighterIterator itr, SyntaxHighlighter highlighter) { IElementType tokenType = itr.getTokenType(); if (tokenType == null) return null; TextAttributesKey[] highlights = highlighter.getTokenHighlights(tokenType); String s = null; for (int i = highlights.length - 1; i >= 0; i--) { if (highlights[i] != HighlighterColors.TEXT) { s = highlights[i].getExternalName(); break; } } return s == null ? HighlighterColors.TEXT.getExternalName() : s; } @Override public JComponent getPanel() { return myEditor.getComponent(); } @Override public void updateView() { EditorColorsScheme scheme = myOptions.getSelectedScheme(); myEditor.setColorsScheme(scheme); EditorHighlighter highlighter = null; if (myPage instanceof EditorHighlightingProvidingColorSettingsPage) { highlighter = ((EditorHighlightingProvidingColorSettingsPage)myPage).createEditorHighlighter(scheme); } if (highlighter == null) { final SyntaxHighlighter pageHighlighter = myPage.getHighlighter(); highlighter = HighlighterFactory.createHighlighter(pageHighlighter, scheme); } myEditor.setHighlighter(highlighter); updateHighlighters(); myEditor.reinitSettings(); } private void updateHighlighters() { UIUtil.invokeLaterIfNeeded(() -> { if (myEditor.isDisposed()) return; removeDecorations(myEditor); final Map<TextAttributesKey, String> displayText = ColorSettingsUtil.keyToDisplayTextMap(myPage); for (final HighlightData data : myHighlightData) { data.addHighlToView(myEditor, myOptions.getSelectedScheme(), displayText); } }); } private static void removeDecorations(Editor editor) { editor.getMarkupModel().removeAllHighlighters(); for (Inlay inlay : editor.getInlayModel().getInlineElementsInRange(0, editor.getDocument().getTextLength())) { Disposer.dispose(inlay); } } private static final int BLINK_COUNT = 3 * 2; @Override public void blinkSelectedHighlightType(Object description) { if (description instanceof EditorSchemeAttributeDescriptor) { String type = ((EditorSchemeAttributeDescriptor)description).getType(); List<HighlightData> highlights = startBlinkingHighlights(myEditor, type, myPage.getHighlighter(), true, myBlinkingAlarm, BLINK_COUNT, myPage); scrollHighlightInView(highlights); } } void scrollHighlightInView(@Nullable final List<HighlightData> highlightDatas) { if (highlightDatas == null) return; boolean needScroll = true; int minOffset = Integer.MAX_VALUE; for (HighlightData data : highlightDatas) { if (isOffsetVisible(data.getStartOffset())) { needScroll = false; break; } minOffset = Math.min(minOffset, data.getStartOffset()); } if (needScroll && minOffset != Integer.MAX_VALUE) { LogicalPosition pos = myEditor.offsetToLogicalPosition(minOffset); myEditor.getScrollingModel().scrollTo(pos, ScrollType.MAKE_VISIBLE); } } private boolean isOffsetVisible(final int startOffset) { return myEditor .getScrollingModel() .getVisibleArea() .contains(myEditor.logicalPositionToXY(myEditor.offsetToLogicalPosition(startOffset))); } public void stopBlinking() { myBlinkingAlarm.cancelAllRequests(); } private List<HighlightData> startBlinkingHighlights(final EditorEx editor, final String attrKey, final SyntaxHighlighter highlighter, final boolean show, final Alarm alarm, final int count, final ColorSettingsPage page) { if (show && count <= 0) return Collections.emptyList(); removeDecorations(editor); boolean found = false; List<HighlightData> highlights = new ArrayList<>(); List<HighlightData> matchingHighlights = new ArrayList<>(); for (HighlightData highlightData : myHighlightData) { boolean highlight = show && highlightData.getHighlightType().equals(attrKey); highlightData.addToCollection(highlights, highlight); if (highlight) { matchingHighlights.add(highlightData); found = true; } } if (!found && highlighter != null) { HighlighterIterator iterator = editor.getHighlighter().createIterator(0); do { IElementType tokenType = iterator.getTokenType(); TextAttributesKey[] tokenHighlights = highlighter.getTokenHighlights(tokenType); for (final TextAttributesKey tokenHighlight : tokenHighlights) { String type = tokenHighlight.getExternalName(); if (show && type != null && type.equals(attrKey)) { HighlightData highlightData = new HighlightData(iterator.getStart(), iterator.getEnd(), BLINKING_HIGHLIGHTS_ATTRIBUTES); highlights.add(highlightData); matchingHighlights.add(highlightData); } } iterator.advance(); } while (!iterator.atEnd()); } final Map<TextAttributesKey, String> displayText = ColorSettingsUtil.keyToDisplayTextMap(page); // sort highlights to avoid overlappings Collections.sort(highlights, Comparator.comparingInt(HighlightData::getStartOffset)); for (int i = highlights.size() - 1; i >= 0; i--) { HighlightData highlightData = highlights.get(i); int startOffset = highlightData.getStartOffset(); HighlightData prevHighlightData = i == 0 ? null : highlights.get(i - 1); if (prevHighlightData != null && startOffset <= prevHighlightData.getEndOffset() && highlightData.getHighlightType().equals(prevHighlightData.getHighlightType())) { prevHighlightData.setEndOffset(highlightData.getEndOffset()); } else { highlightData.addHighlToView(editor, myOptions.getSelectedScheme(), displayText); } } alarm.cancelAllRequests(); alarm.addComponentRequest(() -> startBlinkingHighlights(editor, attrKey, highlighter, !show, alarm, count - 1, page), 400); return matchingHighlights; } @Override public void addListener(@NotNull final ColorAndFontSettingsListener listener) { myDispatcher.addListener(listener); } @Override public void disposeUIResources() { EditorFactory editorFactory = EditorFactory.getInstance(); editorFactory.releaseEditor(myEditor); stopBlinking(); } private void setCursor(@JdkConstants.CursorType int type) { final Cursor cursor = type == Cursor.TEXT_CURSOR ? UIUtil.getTextCursor(myEditor.getBackgroundColor()) : Cursor.getPredefinedCursor(type); myEditor.getContentComponent().setCursor(cursor); } public void setupRainbow(@NotNull EditorColorsScheme colorsScheme, @NotNull RainbowColorSettingsPage page) { final List<HighlightData> initialMarkup = new ArrayList<>(); myHighlightsExtractor.extractHighlights(page.getDemoText(), initialMarkup); final List<HighlightData> rainbowMarkup = setupRainbowHighlighting( page, initialMarkup, new RainbowHighlighter(colorsScheme).getRainbowTempKeys(), RainbowHighlighter.isRainbowEnabledWithInheritance(colorsScheme, page.getLanguage())); myHighlightData.clear(); myHighlightData.addAll(rainbowMarkup); } @NotNull private List<HighlightData> setupRainbowHighlighting(@NotNull final RainbowColorSettingsPage page, @NotNull final List<HighlightData> initialMarkup, @NotNull final TextAttributesKey[] rainbowTempKeys, boolean isRainbowOn) { int colorCount = rainbowTempKeys.length; if (colorCount == 0) { return initialMarkup; } List<HighlightData> rainbowMarkup = new ArrayList<>(); int tempKeyIndex = 0; boolean repeatAnchor = true; for (HighlightData d : initialMarkup) { final TextAttributesKey highlightKey = d.getHighlightKey(); final boolean rainbowType = page.isRainbowType(highlightKey); final boolean rainbowDemoType = highlightKey == RainbowHighlighter.RAINBOW_GRADIENT_DEMO; if (rainbowType || rainbowDemoType) { final HighlightData rainbowAnchor = new HighlightData(d.getStartOffset(), d.getEndOffset(), RainbowHighlighter.RAINBOW_ANCHOR); if (isRainbowOn) { // rainbow on HighlightData rainbowTemp; if (rainbowType) { rainbowTemp = getRainbowTemp(rainbowTempKeys, d.getStartOffset(), d.getEndOffset()); } else { rainbowTemp = new HighlightData(d.getStartOffset(), d.getEndOffset(), rainbowTempKeys[tempKeyIndex % colorCount]); if (repeatAnchor && tempKeyIndex == colorCount/2) { // anchor [Color#3] colored twice: it the end and in the beginning of rainbow-demo string repeatAnchor = false; } else { ++tempKeyIndex; } } // TODO: <remove the hack> // At some point highlighting data is applied in reversed order. To ensure rainbow highlighting is always on top, we add it twice. rainbowMarkup.add(rainbowTemp); rainbowMarkup.add(rainbowAnchor); rainbowMarkup.add(d); rainbowMarkup.add(rainbowAnchor); rainbowMarkup.add(rainbowTemp); } else { // rainbow off if (rainbowType) { // TODO: <remove the hack> // See above rainbowMarkup.add(d); rainbowMarkup.add(rainbowAnchor); rainbowMarkup.add(d); } else { rainbowMarkup.add(rainbowAnchor); } } } else if (!(RainbowHighlighter.isRainbowTempKey(highlightKey) || highlightKey == RainbowHighlighter.RAINBOW_ANCHOR)) { // filter rainbow RAINBOW_TEMP and RAINBOW_ANCHOR rainbowMarkup.add(d); } } return rainbowMarkup; } @NotNull private HighlightData getRainbowTemp(@NotNull TextAttributesKey[] rainbowTempKeys, int startOffset, int endOffset) { String id = myEditor.getDocument().getText(TextRange.create(startOffset, endOffset)); int index = UsedColors.getOrAddColorIndex((EditorImpl)myEditor, id, rainbowTempKeys.length); return new HighlightData(startOffset, endOffset, rainbowTempKeys[index]); } }