package ch.hsr.ifs.pasta;
import java.util.LinkedList;
import org.eclipse.cdt.core.dom.ast.IASTFileLocation;
import org.eclipse.cdt.core.dom.ast.IASTNode;
import org.eclipse.cdt.core.dom.ast.IASTPreprocessorIncludeStatement;
import org.eclipse.cdt.core.dom.ast.IASTTranslationUnit;
import org.eclipse.cdt.core.dom.ast.cpp.ICPPASTTranslationUnit;
import org.eclipse.cdt.ui.CUIPlugin;
import org.eclipse.jface.preference.IPreferenceStore;
import org.eclipse.jface.text.TextSelection;
import org.eclipse.swt.SWT;
import org.eclipse.swt.custom.ScrolledComposite;
import org.eclipse.swt.events.MouseAdapter;
import org.eclipse.swt.events.MouseEvent;
import org.eclipse.swt.events.MouseTrackListener;
import org.eclipse.swt.events.PaintEvent;
import org.eclipse.swt.events.PaintListener;
import org.eclipse.swt.graphics.Color;
import org.eclipse.swt.graphics.Cursor;
import org.eclipse.swt.graphics.GC;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.graphics.Rectangle;
import org.eclipse.swt.widgets.Button;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.Display;
import ch.hsr.ifs.pasta.plugin.preferences.PreferenceConstants;
import ch.hsr.ifs.pasta.tree.Node;
import ch.hsr.ifs.pasta.tree.NodeVisitor;
public class ASTWidget extends ScrolledComposite {
private static final Color GOLDEN_YELLOW = new Color(Display.getCurrent(), 255, 168, 0);
private static final Color WHITE = Display.getCurrent().getSystemColor(SWT.COLOR_WHITE);
private static final int DEFAULT_NODE_HEIGHT = 20;
private static final float SIBLING_DISTANCE = 4;
private static final float BRANCH_DISTANCE = 15;
private static final int GAP_SIZE = 20;
private final ASTScrolledCanvas canvas;
private int nodeHeight = DEFAULT_NODE_HEIGHT;
private NodeSelectionListener listener;
private Node<Pair<Button, IASTNode>> root;
private Node<Pair<Button, IASTNode>> lastControl = null;
private final IPreferenceStore prefStore;
public ASTWidget(final Composite parent) {
super(parent, SWT.V_SCROLL | SWT.H_SCROLL | SWT.BORDER);
prefStore = PastaPlugin.getDefault().getPreferenceStore();
canvas = new ASTScrolledCanvas(this, SWT.BACKGROUND);
setupScrolledComposite(parent);
setupCanvas();
}
void showSelectedNode(final IASTNode sParent) {
IASTNode parent = sParent;
if (!root.isTreatedAsLeaf()) {
buildChildrenAndRefresh(root);
}
final LinkedList<IASTNode> nodeList = new LinkedList<>();
while (parent.getParent() != null) {
nodeList.add(parent);
parent = parent.getParent();
}
IASTNode listNode;
Node<Pair<Button, IASTNode>> currentNode = root;
while (!nodeList.isEmpty()) {
listNode = nodeList.removeLast();
buildChildrenAndRefresh(currentNode);
for (final Node<Pair<Button, IASTNode>> child : currentNode.getChildren()) {
if (child.data().getSecond().getRawSignature().equals(listNode.getRawSignature())) {
currentNode = child;
break;
}
}
}
buildChildrenAndRefresh(currentNode);
}
private void setupScrolledComposite(final Composite parent) {
setAlwaysShowScrollBars(true);
setBackground(WHITE);
setContent(canvas);
setExpandHorizontal(true);
setExpandVertical(true);
}
private void setupCanvas() {
resizeCanvas();
canvas.setBackground(WHITE);
canvas.addPaintListener(new PaintListener() {
@Override
public void paintControl(final PaintEvent e) {
if (lastControl != null) {
adjustView(lastControl);
}
if (root != null) {
root.visit(node -> {
if (node.data().getFirst().isVisible()) {
if (node.parent() != null) {
drawArrowFromParent(e.gc, node);
}
return NodeVisitor.AfterVisitBehaviour.Continue;
}
return NodeVisitor.AfterVisitBehaviour.Abort;
});
}
}
});
}
protected void resizeCanvas() {
final Point widgetSize = getSize();
final Point computedSize = canvas.computeSize(SWT.DEFAULT, SWT.DEFAULT, true);
final Point newSize = new Point(Math.max(computedSize.x, widgetSize.x), Math.max(computedSize.y, widgetSize.y));
setMinSize(newSize);
canvas.setSize(newSize);
}
public void drawAST(final IASTTranslationUnit ast) {
clear();
root = constructTree(ast, canvas);
root.adjust(ASTWidget.SIBLING_DISTANCE, ASTWidget.BRANCH_DISTANCE);
setOrigin(0, 0);
updateNodePositions(root);
resizeCanvas();
final Button rootButton = root.data().getFirst();
rootButton.setLocation(new Point(getSize().x / 2 - rootButton.getBounds().x / 2, 0));
rootButton.setVisible(true);
refresh();
}
public void setListener(final NodeSelectionListener listener) {
this.listener = listener;
}
private void clear() {
root = null;
for (final Control child : canvas.getChildren()) {
child.dispose();
}
}
private void drawArrowFromParent(final GC gc, final Node<?> node) {
final int parentX = (int) (getXCoord(node.parent()) + ((node.parent().width()) / 2));
final int parentY = getYCoord(node.parent()) + nodeHeight;
final int nodeX = (int) (getXCoord(node) + ((node.width()) / 2));
final int nodeY = getYCoord(node);
drawArrow(gc, parentX, parentY, nodeX, nodeY);
}
public static void drawArrow(final GC gc, final int x1, final int y1, int x2, final int y2) {
final double arrowAngle = Math.toRadians(30.0);
final double arrowLength = 8.0;
double theta = Math.atan2(y2 - y1, x2 - x1);
/* Arrow line */
if (Math.abs(x1 - x2) > GAP_SIZE) {
final int centerY = (y1 + y2) / 2;
if (x1 > x2) {
gc.drawArc(x1 - 30, y1 - GAP_SIZE/2, 30, GAP_SIZE, 0, -90);
gc.drawLine(x1 - 15, centerY, x2 + 15, centerY);
gc.drawArc(x2, y1 + GAP_SIZE/2, 30, GAP_SIZE, 180, -90);
theta = Math.toRadians(124);
x2--;
} else {
gc.drawArc(x1, y1 - GAP_SIZE/2, 30, GAP_SIZE, 180, 90);
gc.drawLine(x1 + 15, centerY, x2 - 15, centerY);
gc.drawArc(x2 - 30, y1 + GAP_SIZE/2, 30, GAP_SIZE, 90, -90);
theta = Math.toRadians(60);
x2++;
}
} else {
gc.drawLine(x1, y1, x2, y2);
}
/* Arrow head */
gc.drawLine((int) (x2 - arrowLength * Math.cos(theta - arrowAngle)),
(int) (y2 - arrowLength * Math.sin(theta - arrowAngle)), x2, y2);
gc.drawLine((int) (x2 - arrowLength * Math.cos(theta + arrowAngle)),
(int) (y2 - arrowLength * Math.sin(theta + arrowAngle)), x2, y2);
}
private void updateNodePositions(final Node<Pair<Button, IASTNode>> node) {
if (node.parent() != null && !node.parent().data().getFirst().isVisible()) {
node.data().getFirst().setVisible(false);
}
node.data().getFirst().setBounds(getXCoord(node), getYCoord(node), (int) (node.width()), nodeHeight);
for (final Node<Pair<Button, IASTNode>> child : node.getChildren()) {
updateNodePositions(child);
}
}
private int getYCoord(final Node<?> node) {
return (int) (node.y() * (nodeHeight + GAP_SIZE));
}
private int getXCoord(final Node<?> node) {
return (int) node.x();
}
private void refresh() {
canvas.redraw();
canvas.update();
}
private void adjustView(final Node<Pair<Button, IASTNode>> node) {
final Rectangle buttonBounds = node.data().getFirst().getBounds();
final int leftmostIndex;
final int bottommostIndex;
if (node.leftMostChild() == null) {
leftmostIndex = buttonBounds.x;
bottommostIndex = buttonBounds.y + buttonBounds.height;
} else {
final Rectangle bounds = node.leftMostChild().data().getFirst().getBounds();
leftmostIndex = Math.min(bounds.x, buttonBounds.x);
bottommostIndex = bounds.y + bounds.height;
}
final int rightmostIndex;
if (node.rightMostChild() == null) {
rightmostIndex = buttonBounds.x + buttonBounds.width;
} else {
final Rectangle bounds = node.rightMostChild().data().getFirst().getBounds();
rightmostIndex = Math.max(bounds.x + bounds.width, buttonBounds.x + buttonBounds.width);
}
int correctedX = getOrigin().x;
int correctedY = getOrigin().y;
lastControl = null;
if (!(leftmostIndex > getOrigin().x && rightmostIndex < getOrigin().x + getBounds().width)) {
correctedX = buttonBounds.x - (getBounds().width / 2 - buttonBounds.width / 2);
}
if (!(buttonBounds.y > getOrigin().y && bottommostIndex < getOrigin().y + getBounds().height)) {
correctedY = buttonBounds.y - (getBounds().height / 2 - buttonBounds.height / 2);
}
this.setOrigin(correctedX, correctedY);
}
private Node<Pair<Button, IASTNode>> constructTree(final IASTNode astNode, final Composite parent) {
final Button button = createButton(astNode.getClass().getSimpleName(), parent);
final Node<Pair<Button, IASTNode>> node = createNode(button, astNode);
if (astNode.getChildren().length == 0) {
final Button leafButton = createButton(astNode.getRawSignature(), parent);
leafButton.setEnabled(false);
final Node<Pair<Button, IASTNode>> leafNode = createNode(leafButton, astNode);
node.addChild(leafNode);
}
return node;
}
private Button createButton(final String text, final Composite parent) {
final Button button = new Button(parent, SWT.FLAT);
button.getFont().getFontData()[0].setHeight(10);
button.setText(text.replaceAll("\\{[\\S\\s]*\\}", "{ ... }"));
button.setVisible(false);
button.pack();
button.setCursor(new Cursor(getDisplay(), SWT.CURSOR_ARROW));
return button;
}
private Node<Pair<Button, IASTNode>> createNode(final Button button, final IASTNode astNode) {
final Node<Pair<Button, IASTNode>> node = new Node<>(new Pair<>(button, astNode));
final Point minButtonSize = button.computeSize(SWT.DEFAULT, SWT.DEFAULT, true);
nodeHeight = Math.max(nodeHeight, minButtonSize.y);
node.setWidth(minButtonSize.x);
node.treatAsLeaf(true);
if (astNode instanceof ICPPASTTranslationUnit) {
button.setBackground(GOLDEN_YELLOW);
button.getFont().getFontData()[0].setStyle(SWT.BOLD);
}
button.addMouseListener(new MouseAdapter() {
@Override
public void mouseDown(final MouseEvent e) {
int selectKey = 0;
if (prefStore.getString(PreferenceConstants.P_HOW_TO_SELECT)
.equals(PreferenceConstants.P_SELECT_BY_RIGHT_CLICK)) {
selectKey = 3;
} else if (prefStore.getString(PreferenceConstants.P_HOW_TO_SELECT)
.equals(PreferenceConstants.P_SELECT_BY_LEFT_CLICK)) {
selectKey = 1;
}
if (e.button == selectKey) {
setNodeInNodeView(astNode);
}
if (e.button == 1) {
buildChildrenAndRefresh(node);
}
}
});
button.addMouseTrackListener(new MouseTrackListener() {
@Override
public void mouseHover(final MouseEvent e) {
if (prefStore.getString(PreferenceConstants.P_HOW_TO_SELECT)
.equals(PreferenceConstants.P_SELECT_BY_MOUSE_OVER)) {
setNodeInNodeView(astNode);
}
IASTFileLocation fileLocation = astNode.getFileLocation();
while (fileLocation.getContextInclusionStatement() != null) {
final IASTPreprocessorIncludeStatement contextInclusionStatement = fileLocation
.getContextInclusionStatement();
fileLocation = contextInclusionStatement.getFileLocation();
}
final TextSelection textSelection = new TextSelection(fileLocation.getNodeOffset(),
fileLocation.getNodeLength());
CUIPlugin.getActivePage().getActiveEditor().getEditorSite().getSelectionProvider()
.setSelection(textSelection);
}
@Override
public void mouseExit(final MouseEvent e) {
}
@Override
public void mouseEnter(final MouseEvent e) {
}
});
return node;
}
private void setNodeInNodeView(final IASTNode astNode) {
if (listener != null) {
listener.nodeSelected(astNode);
}
}
private void buildChildrenAndRefresh(final Node<Pair<Button, IASTNode>> node) {
lastControl = node;
node.treatAsLeaf(!node.isTreatedAsLeaf());
if (!node.isTreatedAsLeaf() && node.getChildren().size() == 0) {
for (final IASTNode child : node.data().getSecond().getChildren()) {
node.addChild(constructTree(child, canvas));
}
}
for (final Node<Pair<Button, IASTNode>> child : node.getChildren()) {
if (!node.isTreatedAsLeaf()) {
child.treatAsLeaf(true);
}
child.data().getFirst().setVisible(!node.isTreatedAsLeaf());
}
if (!node.isTreatedAsLeaf()) {
root.adjust(ASTWidget.SIBLING_DISTANCE, ASTWidget.BRANCH_DISTANCE);
}
updateNodePositions(root);
resizeCanvas();
refresh();
}
}