/*
* Copyright 2003-2011 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.cellLayout;
import jetbrains.mps.editor.runtime.TextBuilderImpl;
import jetbrains.mps.editor.runtime.style.DefaultBaseLine;
import jetbrains.mps.editor.runtime.style.StyleAttributes;
import jetbrains.mps.nodeEditor.EditorSettings;
import jetbrains.mps.nodeEditor.cells.EditorCell_Basic;
import jetbrains.mps.nodeEditor.cells.EditorCell_Indent;
import jetbrains.mps.nodeEditor.cells.GeometryUtil;
import jetbrains.mps.openapi.editor.TextBuilder;
import jetbrains.mps.openapi.editor.cells.CellTraversalUtil;
import jetbrains.mps.openapi.editor.cells.EditorCell;
import jetbrains.mps.openapi.editor.cells.EditorCell_Collection;
import java.awt.Rectangle;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import java.util.Stack;
public class CellLayout_Indent extends AbstractCellLayout {
static boolean isOnNewLine(EditorCell root, EditorCell cell) {
for (EditorCell current = cell; current != root; current = current.getParent()) {
if (current.getStyle().get(StyleAttributes.INDENT_LAYOUT_ON_NEW_LINE)) {
return true;
}
if (current.getParent() == null || current.getParent().firstCell() != current) {
return false;
}
}
return false;
}
private static int getIndent(EditorCell root, EditorCell cell, boolean overflow) {
int result = 0;
if (overflow) {
result += 2;
}
while (cell != root) {
if (cell.getStyle().get(StyleAttributes.INDENT_LAYOUT_INDENT)) {
result++;
}
cell = cell.getParent();
}
return result;
}
public static boolean isNewLineAfter(EditorCell root, EditorCell cell) {
for (EditorCell current = cell; current != root; current = current.getParent()) {
if (current.getStyle().get(StyleAttributes.INDENT_LAYOUT_NEW_LINE)) {
return true;
}
EditorCell_Collection parent = current.getParent();
if (parent != null && parent.getStyle().get(StyleAttributes.INDENT_LAYOUT_CHILDREN_NEWLINE)) {
return true;
}
if (parent == null || parent.lastCell() != current) {
return false;
}
}
return false;
}
@Override
public int getAscent(EditorCell_Collection editorCells) {
for (EditorCell cell : editorCells) {
if (cell.getStyle().get(StyleAttributes.BASE_LINE_CELL)) {
return cell.getY() - editorCells.getY() + cell.getAscent();
}
}
DefaultBaseLine bL = editorCells.getStyle().get(StyleAttributes.DEFAULT_BASE_LINE);
int result = 0;
for (EditorCell cell : editorCells) {
result = cell.getAscent();
if (result > 0) {
break;
}
}
switch (bL) {
case FIRST: // default behavior
return result;
case CENTER:
return Math.max(result, editorCells.getHeight() / 2);
case LAST:
if (!editorCells.isEmpty()) {
EditorCell lastCell = editorCells.lastCell();
return lastCell.getY() - editorCells.getY() + lastCell.getAscent();
}
}
return 0;
}
@Override
public void move(EditorCell_Collection editorCells, int deltaX, int deltaY) {
if (editorCells.getParent() != null && editorCells.getParent().getCellLayout() instanceof CellLayout_Indent || deltaX == 0) {
return;
}
// Triggering re-layout process for top-level EditorCell_Collection with indent layout on move.
// Necessary to recalculate cell wrapping on moving EditorCell_Collection with indent layout enclosed
// inside another (non-indent layout) cell. Such move can be performed as a part of layout process for
// top-level cell with indent layout.
editorCells.requestRelayout();
}
@Override
public void doLayout(EditorCell_Collection editorCells) {
if (editorCells.getParent() != null && editorCells.getParent().getCellLayout() instanceof CellLayout_Indent) {
return;
}
new CellLayouter(editorCells, getMaxWidth(editorCells), getIndentSize()).layout();
}
private int getMaxWidth(EditorCell_Collection editorCells) {
if (editorCells.getStyle().isSpecified(StyleAttributes.MAX_WIDTH)) {
return editorCells.getX() + editorCells.getStyle().get(StyleAttributes.MAX_WIDTH);
}
return editorCells.getRootParent().getX() + EditorSettings.getInstance().getVerticalBoundWidth();
}
private int getIndentSize() {
EditorSettings settings = EditorSettings.getInstance();
return settings.getSpacesWidth(settings.getIndentSize());
}
@Override
public TextBuilder doLayoutText(Iterable<EditorCell> editorCells) {
Set<EditorCell> editorCellsSet = new HashSet<EditorCell>();
for (EditorCell editorCell : editorCells) {
editorCellsSet.add(editorCell);
}
TextBuilder result = new TextBuilderImpl();
Iterator<EditorCell> iterator = editorCells.iterator();
if (iterator.hasNext()) {
boolean newLineAfter = false;
EditorCell_Collection rootCell = iterator.next().getParent();
for (EditorCell current : getIndentLeafs(rootCell)) {
EditorCell childCell = current;
while (childCell.getParent() != rootCell) {
childCell = childCell.getParent();
}
if (!editorCellsSet.contains(childCell)) {
continue;
}
if (isOnNewLine(rootCell, current) || newLineAfter) {
newLineAfter = false;
result.appendToTheRight(new TextBuilderImpl("\n"), true);
for (int i = 0; i < getIndent(rootCell, current, false); i++) {
result.appendToTheRight(new TextBuilderImpl(EditorCell_Indent.getIndentText()), false);
}
}
result.appendToTheRight(current.renderText(), PunctuationUtil.hasLeftGap(current));
if (isNewLineAfter(rootCell, current)) {
newLineAfter = true;
}
}
}
return result;
}
@Override
public List<? extends EditorCell> getSelectionCells(EditorCell_Collection editorCells) {
return getIndentLeafs(editorCells);
}
@Override
public List<Rectangle> getSelectionBounds(EditorCell_Collection editorCells) {
List<Rectangle> result = new ArrayList<Rectangle>();
List<EditorCell> indentLeafs = getIndentLeafs(editorCells);
for (EditorCell leaf : indentLeafs) {
result.add(GeometryUtil.getBounds(leaf));
}
return result;
}
@Override
public boolean canBeFolded() {
return true;
}
private static List<EditorCell> getIndentLeafs(EditorCell_Collection current) {
List<EditorCell> result = new ArrayList<EditorCell>();
collectCells(current, result, null);
return result;
}
private static void collectCells(
EditorCell_Collection current,
List<EditorCell> frontier,
List<EditorCell_Collection> collections) {
for (EditorCell child : current) {
if (child instanceof EditorCell_Collection) {
EditorCell_Collection collection = (EditorCell_Collection) child;
if (isIndentCollection(collection)) {
collectCells(collection, frontier, collections);
} else {
frontier.add(child);
}
} else {
frontier.add(child);
}
if (collections != null) {
collections.add(current);
}
}
}
private static boolean isIndentCollection(EditorCell_Collection collection) {
return collection.getCellLayout() instanceof CellLayout_Indent && !collection.isEmpty();
}
private class CellLayouter {
private EditorCell_Collection myCell;
private final int myX;
private final int myMaxWidth;
private int myWidth;
private int myHeight;
private int myLineWidth;
private int myLineAscent;
private int myLineDescent;
private int myTopInset;
private int myBottomInset;
private boolean myOverflow;
private int myLineIndent = 0;
private List<EditorCell> myLineContent = new ArrayList<EditorCell>();
private List<Integer> myLineWrapIndent = new ArrayList<Integer>();
private int myIndentSize;
private Stack<Integer> myIndentStack = new Stack<Integer>();
private Stack<Integer> myWrapStack = new Stack<Integer>();
private int myCurrentIndent;
private int myCurrentIndentAfterWrap;
private CellLayouter(EditorCell_Collection cell, int maxWidth, int indentSize) {
myCell = cell;
myX = myCell.getX();
myWidth = 0;
myHeight = 0;
myLineWidth = 0;
myLineAscent = 0;
myLineDescent = 0;
myTopInset = 0;
myBottomInset = 0;
myCurrentIndent = 0;
myMaxWidth = maxWidth;
myIndentSize = indentSize;
myCurrentIndentAfterWrap = myIndentSize * 2;
}
public void layout() {
layoutCollection(myCell);
newLine(false);
updatePositions(myCell);
}
private void layout(final EditorCell cell) {
if (isOnNewLine(myCell, cell)) {
newLine(false);
}
if (cell.getStyle().get(StyleAttributes.INDENT_LAYOUT_INDENT)) {
withIndent(myCurrentIndent + myIndentSize, myCurrentIndent + 3 * myIndentSize, new Runnable() {
@Override
public void run() {
appendCell(cell, false);
}
});
} else {
appendCell(cell, false);
}
if (haveToSplit()) {
splitLineAt(findSplitPoint());
}
if (isNewLineAfter(myCell, cell)) {
newLine(false);
}
}
private void withIndent(int indent, int wrapIndent, Runnable r) {
try {
myIndentStack.push(myCurrentIndent);
myWrapStack.push(myCurrentIndentAfterWrap);
myCurrentIndent = indent;
myCurrentIndentAfterWrap = wrapIndent;
r.run();
} finally {
myCurrentIndent = myIndentStack.pop();
myCurrentIndentAfterWrap = myWrapStack.pop();
}
}
private void layoutCollection(final EditorCell_Collection collection) {
boolean hasIndent = collection != myCell && collection.getStyle().get(StyleAttributes.INDENT_LAYOUT_INDENT);
boolean hasAnchor = collection != myCell && collection.getStyle().get(StyleAttributes.INDENT_LAYOUT_INDENT_ANCHOR);
boolean hasWrapAnchor = collection.getStyle().get(StyleAttributes.INDENT_LAYOUT_WRAP_ANCHOR);
int indent = hasIndent && hasAnchor ? currentIndent() + myIndentSize :
hasIndent ? myCurrentIndent + myIndentSize :
hasAnchor ? currentIndent() + getFirstChildLeftGap(collection)
: myCurrentIndent;
int wrapIndent = hasWrapAnchor ? currentIndent() + getFirstChildLeftGap(collection) :
(hasAnchor || hasIndent) ? indent + 2 * myIndentSize
: myCurrentIndentAfterWrap;
withIndent(indent, wrapIndent, new Runnable() {
@Override
public void run() {
for (EditorCell child : collection) {
if (child instanceof EditorCell_Collection && isIndentCollection((EditorCell_Collection) child)) {
layoutCollection((EditorCell_Collection) child);
} else {
layout(child);
}
}
}
});
}
private int currentIndent() {
int indent = myLineWidth;
if (myLineContent.isEmpty()) {
indent += myOverflow ? myCurrentIndentAfterWrap : myCurrentIndent;
}
return indent;
}
private int getFirstChildLeftGap(EditorCell_Collection collection) {
EditorCell firstLeaf = CellTraversalUtil.getFirstLeaf(collection);
if (firstLeaf != null) {
return PunctuationUtil.getLeftGap(firstLeaf);
}
return 0;
}
private void updatePositions(EditorCell_Collection collection) {
for (EditorCell child : collection) {
if (child instanceof EditorCell_Collection && isIndentCollection((EditorCell_Collection) child)) {
updatePositions((EditorCell_Collection) child);
}
}
int x0 = Integer.MAX_VALUE;
int y0 = Integer.MAX_VALUE;
int x1 = Integer.MIN_VALUE;
int y1 = Integer.MIN_VALUE;
for (EditorCell child : collection) {
x0 = Math.min(x0, child.getX());
y0 = Math.min(y0, child.getY());
x1 = Math.max(x1, child.getX() + child.getWidth());
y1 = Math.max(y1, child.getY() + child.getHeight());
}
collection.setX(x0);
collection.setY(y0);
collection.setWidth(x1 - x0);
collection.setHeight(y1 - y0);
//collection is implicitly laid out
((EditorCell_Basic) collection).unrequestLayout();
if (collection != myCell) {
int ascent = getAscent(collection);
int descent = collection.getHeight() - ascent;
collection.setAscent(ascent);
collection.setDescent(descent);
}
}
private void appendCell(EditorCell cell, boolean last) {
if (myLineContent.isEmpty()) {
myLineIndent = myCurrentIndent;
indent();
}
PunctuationUtil.addGaps(cell, myLineContent.isEmpty(), last);
cell.moveTo(myX + myLineWidth, cell.getY());
cell.relayout();
myLineAscent = Math.max(myLineAscent, cell.getAscent());
myLineDescent = Math.max(myLineDescent, cell.getDescent());
myTopInset = Math.max(myTopInset, cell.getTopInset());
myBottomInset = Math.max(myBottomInset, cell.getBottomInset());
myLineWidth += cell.getWidth();
myLineContent.add(cell);
myLineWrapIndent.add(myCurrentIndentAfterWrap);
}
private void indent() {
myLineWidth += myOverflow ? myCurrentIndentAfterWrap : myCurrentIndent;
}
private void newLine(boolean overflow) {
int baseLine = myCell.getY() + myHeight + myTopInset + myLineAscent;
for (EditorCell cell : myLineContent) {
cell.setBaseline(baseLine);
cell.relayout();
}
myWidth = Math.max(myWidth, myLineWidth);
myHeight += myTopInset + myBottomInset + myLineAscent + myLineDescent;
myOverflow = overflow;
resetLine();
}
private void resetLine() {
myLineWidth = 0;
myLineAscent = 0;
myLineDescent = 0;
myTopInset = 0;
myBottomInset = 0;
myLineContent.clear();
myLineWrapIndent.clear();
}
private boolean haveToSplit() {
return myX + myLineWidth > myMaxWidth && myLineContent.size() > 1;
}
private EditorCell findSplitPoint() {
EditorCell lastCell = myLineContent.get(myLineContent.size() - 1);
EditorCell result = lastCell;
EditorCell current = result;
while (true) {
if (!isIndentCollection(current.getParent())) {
break;
}
EditorCell indentLeaf = getFirstIndentLeaf(current.getParent());
EditorCell unitStart = expandToUnitStart(indentLeaf);
if (myLineContent.contains(unitStart) && isOnRightSide(unitStart) &&
cellRangeFitsOnOneLine(unitStart, lastCell)) {
result = indentLeaf;
current = current.getParent();
} else {
break;
}
}
return expandToUnitStart(result);
}
private EditorCell expandToUnitStart(EditorCell cell) {
EditorCell result = cell;
while (true) {
EditorCell prevLeaf = CellTraversalUtil.getPrevLeaf(result);
// taking into account prevLeafs located inside collections with non-indent layouts:
// in this case topmost collection itself will be included into myLineContent as a
// child element
while (prevLeaf != null && !myLineContent.contains(prevLeaf)) {
prevLeaf = prevLeaf.getParent();
}
if (!myCell.isAncestorOf(prevLeaf)) {
break;
}
if (!myLineContent.contains(prevLeaf)) {
break;
}
if (isNoWrap(result) || result.getStyle().get(StyleAttributes.PUNCTUATION_LEFT)) {
result = prevLeaf;
} else {
break;
}
}
return result;
}
private Boolean isNoWrap(EditorCell current) {
while (current != null) {
if (current.getStyle().get(StyleAttributes.INDENT_LAYOUT_NO_WRAP)) {
return true;
}
if (current.getParent().firstCell() == current) {
current = current.getParent();
} else {
return false;
}
}
return false;
}
private boolean cellRangeFitsOnOneLine(EditorCell firstCell, EditorCell lastCell) {
return lastCell.getX() + lastCell.getWidth() - firstCell.getX() <
myMaxWidth - myX - myCurrentIndentAfterWrap;
}
private boolean isOnRightSide(EditorCell cell) {
return cell.getX() + cell.getWidth() - myX > myMaxWidth / 2;
}
private EditorCell getFirstIndentLeaf(EditorCell_Collection collection) {
if (!isIndentCollection(collection)) {
return collection;
}
EditorCell firstChild = collection.firstCell();
if (firstChild instanceof EditorCell_Collection) {
return getFirstIndentLeaf((EditorCell_Collection) firstChild);
}
return firstChild;
}
private void splitLineAt(EditorCell splitAt) {
int index = myLineContent.indexOf(splitAt);
if (index == -1) {
throw new IllegalStateException();
}
final List<EditorCell> oldLine = new ArrayList<EditorCell>(myLineContent.subList(0, index));
final List<EditorCell> newLine = new ArrayList<EditorCell>(myLineContent.subList(index, myLineContent.size()));
withIndent(myLineIndent, myLineWrapIndent.get(index), new Runnable() {
@Override
public void run() {
resetLine();
for (EditorCell cell : oldLine) {
appendCell(cell, cell == oldLine.get(oldLine.size() - 1));
}
if (!oldLine.isEmpty()) {
newLine(true);
}
for (EditorCell cell : newLine) {
appendCell(cell, false);
}
}
});
}
}
}