/*
* Copyright (c) 2013-2016 Chris Newland.
* Licensed under https://github.com/AdoptOpenJDK/jitwatch/blob/master/LICENSE-BSD
* Instructions: https://github.com/AdoptOpenJDK/jitwatch/wiki
*/
package org.adoptopenjdk.jitwatch.ui.triview;
import static org.adoptopenjdk.jitwatch.core.JITWatchConstants.DEBUG_LOGGING;
import static org.adoptopenjdk.jitwatch.core.JITWatchConstants.S_DOUBLE_SPACE;
import static org.adoptopenjdk.jitwatch.core.JITWatchConstants.S_EMPTY;
import static org.adoptopenjdk.jitwatch.core.JITWatchConstants.S_NEWLINE;
import static org.adoptopenjdk.jitwatch.core.JITWatchConstants.S_NEWLINE_CR;
import static org.adoptopenjdk.jitwatch.core.JITWatchConstants.S_TAB;
import static org.adoptopenjdk.jitwatch.util.UserInterfaceUtil.FONT_MONOSPACE_FAMILY;
import static org.adoptopenjdk.jitwatch.util.UserInterfaceUtil.FONT_MONOSPACE_SIZE;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.collections.ObservableList;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.scene.Node;
import javafx.scene.control.ContextMenu;
import javafx.scene.control.Label;
import javafx.scene.control.MenuItem;
import javafx.scene.control.ScrollPane;
import javafx.scene.input.Clipboard;
import javafx.scene.input.ClipboardContent;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import javafx.scene.input.MouseButton;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.VBox;
import org.adoptopenjdk.jitwatch.core.JITWatchConfig;
import org.adoptopenjdk.jitwatch.model.IMetaMember;
import org.adoptopenjdk.jitwatch.model.bytecode.LineAnnotation;
import org.adoptopenjdk.jitwatch.ui.main.IStageAccessProxy;
import org.adoptopenjdk.jitwatch.ui.triview.ILineListener.LineType;
import org.adoptopenjdk.jitwatch.ui.triview.assembly.AssemblyLabel;
import org.adoptopenjdk.jitwatch.ui.triview.bytecode.BytecodeLabel;
import org.adoptopenjdk.jitwatch.util.ParseUtil;
import org.adoptopenjdk.jitwatch.util.StringUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class Viewer extends VBox
{
private ScrollPane scrollPane;
protected VBox vBoxRows;
public static final String COLOUR_BLACK = "black";
public static final String COLOUR_RED = "red";
public static final String COLOUR_GREEN = "green";
public static final String COLOUR_BLUE = "blue";
private int scrollIndex = 0;
protected int lastScrollIndex = -1;
protected String originalSource;
private double lastKnownGoodLineHeight = 15;
private static final String FONT_STYLE = "-fx-font-family:" + FONT_MONOSPACE_FAMILY + "; -fx-font-size:" + FONT_MONOSPACE_SIZE
+ "px;";
public static final String STYLE_UNHIGHLIGHTED = FONT_STYLE + "-fx-background-color:white;";
public static final String STYLE_HIGHLIGHTED = FONT_STYLE + "-fx-background-color:red;";
public static final String STYLE_UNHIGHLIGHTED_SUGGESTION = FONT_STYLE + "-fx-background-color:yellow;";
public static final String STYLE_SAFEPOINT = FONT_STYLE + "-fx-background-color:yellow;";
protected Map<Integer, LineAnnotation> lineAnnotations = new HashMap<>();
protected static final Logger logger = LoggerFactory.getLogger(Viewer.class);
protected IStageAccessProxy stageAccessProxy;
protected ILineListener lineListener;
protected LineType lineType = LineType.PLAIN;
private boolean isHighlighting;
public Viewer(IStageAccessProxy stageAccessProxy, boolean highlighting)
{
this.stageAccessProxy = stageAccessProxy;
this.isHighlighting = highlighting;
lineListener = new NoOpLineListener();
setup();
}
public Viewer(IStageAccessProxy stageAccessProxy, ILineListener lineListener, LineType lineType, boolean highlighting)
{
this.stageAccessProxy = stageAccessProxy;
this.lineListener = lineListener;
this.lineType = lineType;
this.isHighlighting = highlighting;
setup();
}
public void clear()
{
lineAnnotations.clear();
vBoxRows.getChildren().clear();
lastScrollIndex = -1;
}
public LineType getLineType()
{
return lineType;
}
public JITWatchConfig getConfig()
{
return stageAccessProxy.getConfig();
}
private void setup()
{
vBoxRows = new VBox();
vBoxRows.heightProperty().addListener(new ChangeListener<Number>()
{
@Override
public void changed(ObservableValue<? extends Number> arg0, Number oldValue, Number newValue)
{
setScrollBar();
}
});
scrollPane = new ScrollPane();
scrollPane.setContent(vBoxRows);
scrollPane.setStyle("-fx-background:white");
scrollPane.setFitToHeight(true);
scrollPane.prefHeightProperty().bind(heightProperty());
EventHandler<KeyEvent> keyHandler = new EventHandler<KeyEvent>()
{
@Override
public void handle(KeyEvent event)
{
KeyCode code = event.getCode();
clearAllHighlighting();
switch (code)
{
case UP:
handleKeyUp();
break;
case DOWN:
handleKeyDown();
break;
case LEFT:
handleKeyLeft();
break;
case RIGHT:
handleKeyRight();
break;
case PAGE_UP:
handleKeyPageUp();
break;
case PAGE_DOWN:
handleKeyPageDown();
break;
default:
return;
}
event.consume();
}
};
focusedProperty().addListener(new ChangeListener<Boolean>()
{
@Override
public void changed(ObservableValue<? extends Boolean> observable, Boolean hadFocus, Boolean hasFocus)
{
if (hasFocus && !hadFocus)
{
scrollPane.requestFocus();
}
}
});
scrollPane.focusedProperty().addListener(new ChangeListener<Boolean>()
{
@Override
public void changed(ObservableValue<? extends Boolean> observable, Boolean hadFocus, Boolean hasFocus)
{
if (hasFocus && !hadFocus)
{
lineListener.lineHighlighted(scrollIndex, lineType);
highlightLine(scrollIndex, false);
}
}
});
scrollPane.setOnMouseEntered(new EventHandler<MouseEvent>()
{
@Override
public void handle(MouseEvent arg0)
{
if (getConfig().isTriViewMouseFollow())
{
lineListener.handleFocusSelf(lineType);
}
}
});
scrollPane.setOnMouseClicked(new EventHandler<MouseEvent>()
{
@Override
public void handle(MouseEvent arg0)
{
lineListener.handleFocusSelf(lineType);
}
});
scrollPane.setOnKeyPressed(keyHandler);
getChildren().add(scrollPane);
setUpContextMenu();
}
public void setContent(String inSource, boolean showLineNumbers, boolean canHighlight)
{
clear();
String source = inSource;
isHighlighting = canHighlight;
if (source == null)
{
source = "Empty";
}
originalSource = source;
source = source.replace(S_TAB, S_DOUBLE_SPACE);
String[] lines = source.split(S_NEWLINE);
int maxWidth = Integer.toString(lines.length).length();
List<Label> labels = new ArrayList<>();
for (int i = 0; i < lines.length; i++)
{
String row = lines[i];
if (showLineNumbers)
{
lines[i] = StringUtil.padLineNumber(i + 1, maxWidth) + S_DOUBLE_SPACE + row;
}
lines[i] = lines[i].replace(S_NEWLINE_CR, S_EMPTY);
Label lblLine = new Label(lines[i]);
lblLine.setStyle(STYLE_UNHIGHLIGHTED);
labels.add(lblLine);
}
setContent(labels);
}
public void setContent(List<Label> items)
{
lineAnnotations.clear();
lastScrollIndex = -1;
vBoxRows.getChildren().clear();
vBoxRows.getChildren().addAll(items);
int pos = 0;
if (!isHighlighting)
{
clearAllHighlighting();
}
for (final Label label : items)
{
final int finalPos = pos;
unhighlightLabel(label);
if (isHighlighting)
{
label.setOnMouseEntered(new EventHandler<MouseEvent>()
{
@Override
public void handle(MouseEvent mouseEvent)
{
if (getConfig().isTriViewMouseFollow())
{
handleLabelClicked(mouseEvent, finalPos);
}
}
});
if (label.getOnMouseClicked() == null)
{
label.setOnMouseClicked(new EventHandler<MouseEvent>()
{
@Override
public void handle(MouseEvent mouseEvent)
{
handleLabelClicked(mouseEvent, finalPos);
}
});
}
label.setOnMouseExited(new EventHandler<MouseEvent>()
{
@Override
public void handle(MouseEvent arg0)
{
if (getConfig().isTriViewMouseFollow())
{
unhighlightLabel(label);
}
}
});
}
label.minWidthProperty().bind(scrollPane.widthProperty());
pos++;
}
}
protected void handleLabelClicked(MouseEvent mouseEvent, int index)
{
clearAllHighlighting();
lineListener.lineHighlighted(index, lineType);
highlightLine(index, false);
}
private int checkBounds(int scrollIndex)
{
int min = 0;
int max = vBoxRows.getChildren().size() - 1;
return Math.min(Math.max(scrollIndex, min), max);
}
private void handleKeyUp()
{
scrollIndex--;
scrollIndex = checkBounds(scrollIndex);
lineListener.lineHighlighted(scrollIndex, lineType);
highlightLine(scrollIndex);
}
private void handleKeyDown()
{
scrollIndex++;
scrollIndex = checkBounds(scrollIndex);
lineListener.lineHighlighted(scrollIndex, lineType);
highlightLine(scrollIndex);
}
private void handleKeyLeft()
{
lineListener.handleFocusPrev();
}
private void handleKeyRight()
{
lineListener.handleFocusNext();
}
private void handleKeyPageUp()
{
scrollIndex -= linesPerPane();
scrollIndex = checkBounds(scrollIndex);
lineListener.lineHighlighted(scrollIndex, lineType);
highlightLine(scrollIndex);
}
private void handleKeyPageDown()
{
scrollIndex += linesPerPane();
scrollIndex = checkBounds(scrollIndex);
lineListener.lineHighlighted(scrollIndex, lineType);
highlightLine(scrollIndex);
}
private int linesPerPane()
{
return (int) (scrollPane.getHeight() / 10);
}
private void setUpContextMenu()
{
final ContextMenu contextMenu = new ContextMenu();
MenuItem menuItemCopyToClipboard = new MenuItem("Copy to Clipboard");
contextMenu.getItems().add(menuItemCopyToClipboard);
vBoxRows.addEventHandler(MouseEvent.MOUSE_CLICKED, new EventHandler<MouseEvent>()
{
@Override
public void handle(MouseEvent e)
{
if (e.getButton() == MouseButton.SECONDARY)
{
contextMenu.show(vBoxRows, e.getScreenX(), e.getScreenY());
}
}
});
menuItemCopyToClipboard.setOnAction(new EventHandler<ActionEvent>()
{
@Override
public void handle(ActionEvent e)
{
final Clipboard clipboard = Clipboard.getSystemClipboard();
final ClipboardContent content = new ClipboardContent();
ObservableList<Node> items = vBoxRows.getChildren();
content.putString(transformNodeItemsToTextUsing(items));
clipboard.setContent(content);
}
});
}
private String transformNodeItemsToTextUsing(ObservableList<Node> items)
{
StringBuilder builder = new StringBuilder();
for (Node item : items)
{
String line = ((Label) item).getText();
builder.append(line).append(S_NEWLINE);
}
return builder.toString();
}
public void jumpToMemberSource(IMetaMember member)
{
scrollIndex = -1;
int regexPos = findPosForRegex(member.getSourceMethodSignatureRegEx());
if (regexPos == -1)
{
List<String> lines = Arrays.asList(originalSource.split(S_NEWLINE));
scrollIndex = ParseUtil.findBestLineMatchForMemberSignature(member, lines);
}
else
{
scrollIndex = regexPos;
}
}
public void clearAllHighlighting()
{
for (Node item : vBoxRows.getChildren())
{
unhighlightLabel(item);
}
}
private void unhighlightLabel(Node node)
{
if (node instanceof BytecodeLabel)
{
node.setStyle(((BytecodeLabel) node).getUnhighlightedStyle());
}
else if (node instanceof AssemblyLabel)
{
node.setStyle(((AssemblyLabel) node).getUnhighlightedStyle());
}
else
{
node.setStyle(STYLE_UNHIGHLIGHTED);
}
}
public void unhighlightPrevious()
{
if (lastScrollIndex >= 0 && lastScrollIndex < vBoxRows.getChildren().size())
{
Label label = (Label) vBoxRows.getChildren().get(lastScrollIndex);
unhighlightLabel(label);
}
}
protected void highlightLine(int index)
{
highlightLine(index, true);
}
public void highlightLine(int index, boolean setScrollbar)
{
unhighlightPrevious();
if (index >= vBoxRows.getChildren().size())
{
index = vBoxRows.getChildren().size() - 1;
}
if (index >= 0)
{
// leave source position unchanged if not a known source line
Label label = (Label) vBoxRows.getChildren().get(index);
label.setStyle(STYLE_HIGHLIGHTED);
lastScrollIndex = index;
scrollIndex = index;
if (setScrollbar)
{
setScrollBar();
}
}
}
public Label getLabelAtIndex(int index)
{
ObservableList<Node> items = vBoxRows.getChildren();
Label result = null;
if (index >= 0 && index < items.size())
{
result = (Label) items.get(index);
}
if (DEBUG_LOGGING)
{
if (result == null)
{
logger.debug("No label at index {}", index);
}
}
return result;
}
private int findPosForRegex(String regex)
{
int result = -1;
ObservableList<Node> items = vBoxRows.getChildren();
Pattern pattern = Pattern.compile(regex);
int index = 0;
for (Node item : items)
{
String line = ((Label) item).getText();
Matcher matcher = pattern.matcher(line);
if (matcher.find())
{
result = index;
break;
}
index++;
}
return result;
}
public void setScrollBar()
{
if (vBoxRows.getChildren().size() > 0)
{
double scrollMin = scrollPane.getVmin();
double scrollMax = scrollPane.getVmax();
double scrollPaneHeight = scrollPane.getHeight();
double lineHeight = vBoxRows.getChildren().get(0).getBoundsInParent().getHeight();
if (lineHeight == 0.0)
{
lineHeight = lastKnownGoodLineHeight;
}
else
{
lastKnownGoodLineHeight = lineHeight;
}
double visibleLines = scrollPaneHeight / lineHeight;
double count = vBoxRows.getChildren().size() - visibleLines;
double scrollPercent = 0;
if (count > 0)
{
scrollPercent = Math.max(scrollIndex - (visibleLines / 2), 0) / count;
}
double scrollPos = scrollPercent * (scrollMax - scrollMin);
scrollPane.setVvalue(scrollPos);
}
}
}