/*
* 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() {
}
}
}