/* * Copyright 2003-2015 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 jetbrains.mps.nodeEditor; import jetbrains.mps.errors.IErrorReporter; import jetbrains.mps.errors.MessageStatus; import jetbrains.mps.errors.messageTargets.MessageTarget; import jetbrains.mps.ide.util.ColorAndGraphicsUtil; import jetbrains.mps.nodeEditor.cells.EditorCell_Constant; import jetbrains.mps.nodeEditor.cells.EditorCell_Error; import jetbrains.mps.nodeEditor.cells.EditorCell_Property; import jetbrains.mps.nodeEditor.cells.PropertyAccessor; import jetbrains.mps.nodeEditor.messageTargets.EditorMessageWithTarget; import jetbrains.mps.openapi.editor.cells.EditorCell; import jetbrains.mps.openapi.editor.cells.EditorCell_Collection; import jetbrains.mps.openapi.editor.message.EditorMessageOwner; import jetbrains.mps.openapi.editor.message.SimpleEditorMessage; import jetbrains.mps.smodel.SNodeUtil; import org.jetbrains.mps.openapi.model.SNode; import java.awt.Color; import java.awt.Graphics; import java.util.ArrayList; import java.util.Collections; import java.util.Deque; import java.util.Iterator; import java.util.LinkedList; import java.util.List; public class HighlighterMessage extends EditorMessageWithTarget { private IErrorReporter myErrorReporter; public HighlighterMessage(SNode errorNode, MessageStatus status, MessageTarget target, Color color, String string, EditorMessageOwner owner) { super(errorNode, status, target, color, string, owner); } public void setErrorReporter(IErrorReporter errorReporter) { myErrorReporter = errorReporter; } public IErrorReporter getErrorReporter() { return myErrorReporter; } @Override public boolean sameAs(SimpleEditorMessage message) { if (!(message instanceof HighlighterMessage)) { return false; } return super.sameAs(message); } @Override public EditorCell getCellForParentNodeInMainEditor(EditorComponent editor) { return super.getCellForParentNodeInMainEditor(editor); } @Override public boolean isBackground() { return false; } @Override public boolean showInGutter() { return getStatus() != MessageStatus.OK; } @Override public void paint(Graphics g, EditorComponent editorComponent, EditorCell cell) { if (cell != null) { for (Region nextRegion : getHighlightedRegions(cell)) { nextRegion.drawWaveUnderCell(g, getColor()); } } } private List<Region> getHighlightedRegions(EditorCell cell) { Deque<Iterator<EditorCell>> iteratorsStack = new LinkedList<>(); if (cell instanceof EditorCell_Collection) { iteratorsStack.addLast(((EditorCell_Collection) cell).iterator()); } else { iteratorsStack.addLast(Collections.singletonList(cell).iterator()); } Region anchorRegion = null; AnchorCellType anchorCellType = AnchorCellType.NONE; List<Region> regions = new ArrayList<>(); boolean insidePrefix = true; while (!iteratorsStack.isEmpty()) { Iterator<EditorCell> currentIterator = iteratorsStack.peekLast(); if (!currentIterator.hasNext()) { iteratorsStack.removeLast(); continue; } EditorCell nextCell = currentIterator.next(); if (nextCell.getSNode() != cell.getSNode()) { insidePrefix = false; continue; } if (nextCell instanceof EditorCell_Collection) { iteratorsStack.addLast(((EditorCell_Collection) nextCell).iterator()); } else { Region nextRegion = new Region(nextCell); regions.add(nextRegion); AnchorCellType nextCellType = getAnchorCellType(nextCell, insidePrefix); if (nextCellType.ordinal() < anchorCellType.ordinal()) { anchorRegion = nextRegion; anchorCellType = nextCellType; } } } if (anchorRegion != null) { int anchorRegionIndex = regions.indexOf(anchorRegion); assert anchorRegionIndex != -1; Region result = anchorRegion; for (int i = anchorRegionIndex + 1; i < regions.size() && result.canMerge(regions.get(i)); i++) { result = result.merge(regions.get(i)); } for (int i = anchorRegionIndex - 1; i >= 0 && result.canMerge(regions.get(i)); i--) { result = result.merge(regions.get(i)); } return Collections.singletonList(result); } for (int i = 0; i < regions.size(); ) { if (i > 0 && regions.get(i - 1).canMerge(regions.get(i))) { regions.set(i - 1, regions.get(i - 1).merge(regions.get(i))); regions.remove(i); } else { i++; } } return highlightContainingCollection(regions) ? Collections.singletonList(new Region(cell)) : regions; } public static AnchorCellType getAnchorCellType(EditorCell cell, boolean prefixCell) { if (cell instanceof EditorCell_Property && ((EditorCell_Property) cell).getModelAccessor() instanceof PropertyAccessor) { PropertyAccessor accessor = (PropertyAccessor) ((EditorCell_Property) cell).getModelAccessor(); if (SNodeUtil.property_INamedConcept_name.getName().equals(accessor.getPropertyName())) { return AnchorCellType.NAME; } } if (cell instanceof EditorCell_Property && prefixCell) { return AnchorCellType.PROPERTY; } else if (cell instanceof EditorCell_Error && prefixCell) { return AnchorCellType.ERROR; } else if (cell instanceof EditorCell_Constant && prefixCell) { return AnchorCellType.CONSTANT; } else { return AnchorCellType.NONE; } } /** * return true if all regions are located on the same "line", so in this case we will underline * containing collection instead of drawing separate errors. * <p/> * In case of multi-line cells we are still drawing messages as merged cell regions in order to try to highlight editor lines.. */ private boolean highlightContainingCollection(List<Region> regions) { return regions.size() < 2; } private class Region { private int myX; private int myLeftInset; private int myWidth; private int myEffectiveWidth; private int myY; public Region(EditorCell cell) { this(cell.getX(), cell.getY() + cell.getHeight(), cell.getLeftInset(), cell.getEffectiveWidth(), cell.getWidth()); } private Region(int x, int y, int leftInset, int effectiveWidth, int width) { myX = x; myY = y; myLeftInset = leftInset; myEffectiveWidth = effectiveWidth; myWidth = width; } public boolean canMerge(Region another) { return myY == another.myY && (myX + myWidth == another.myX || myX == another.myX + another.myWidth); } public Region merge(Region another) { assert canMerge(another); int y = myY; boolean isFirst = myX + myWidth == another.myX; int x = isFirst ? myX : another.myX; int leftInset = isFirst ? myLeftInset : another.myLeftInset; int width = myWidth + another.myWidth; int effectiveWidth = isFirst ? myWidth - myLeftInset + another.myLeftInset + another.myEffectiveWidth : another.myWidth - another.myLeftInset + myLeftInset + myEffectiveWidth; return new Region(x, y, leftInset, effectiveWidth, width); } public void drawWaveUnderCell(Graphics g, Color color) { g.setColor(color); ColorAndGraphicsUtil.drawWave(g, myX + myLeftInset, myX + myLeftInset + myEffectiveWidth, myY - ColorAndGraphicsUtil.WAVE_HEIGHT); } } enum AnchorCellType { NAME, PROPERTY, ERROR, CONSTANT, NONE; AnchorCellType() { } } }