package org.octabyte.fxshell.javafx;
import org.octabyte.fxshell.terminal.TerminalEvenProcessor;
import org.octabyte.fxshell.terminal.TerminalState;
import org.octabyte.fxshell.terminal.TerminalStateUpdateListener;
import org.octabyte.fxshell.terminal.event.CharacterEvent;
import org.octabyte.fxshell.terminal.event.KeyboardEvent;
import org.octabyte.fxshell.terminal.event.NewLineEvent;
import org.octabyte.fxshell.utils.Pair;
import org.octabyte.fxshell.utils.PairUtils;
import com.sun.javafx.tk.Toolkit;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.event.EventHandler;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.scene.canvas.GraphicsContext;
import javafx.scene.effect.BlendMode;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import javafx.scene.input.MouseEvent;
import javafx.scene.paint.Color;
import javafx.scene.text.Font;
import javafx.scene.text.FontWeight;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.awt.datatransfer.StringSelection;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
/**
* @author kobylsa on 03/02/14.
*/
public class TerminalScene extends Scene implements TerminalStateUpdateListener {
private static final Logger log = LoggerFactory.getLogger(TerminalScene.class);
private final static Font font = Font.font("Lucida Console", FontWeight.BOLD, 16);
private final static float lineHeight = Toolkit.getToolkit().getFontLoader().getFontMetrics(font).getLineHeight();
private final static float chWidth = Toolkit.getToolkit().getFontLoader().computeStringWidth(Character.toString(' '), font);
private final static float fontAscentDiff = lineHeight - Toolkit.getToolkit().getFontLoader().getFontMetrics(font).getAscent();
private final TerminalState termState;
private final TerminalEvenProcessor eventProcessor;
private final GraphicsContext gc;
private List<Line> lines = Collections.emptyList();
private TermPoint selStartPoint = null;
private TermPoint selCurPoint = null;
public TerminalScene(Parent root, final GraphicsContext gc, TerminalState termState, TerminalEvenProcessor eventProcessor) {
super(root);
this.termState = termState;
this.eventProcessor = eventProcessor;
this.gc = gc;
fullRefresh(gc);
this.setOnKeyPressed(new EventHandler<KeyEvent>() {
@Override
public void handle(KeyEvent keyEvent) {
handleKeyEvent(keyEvent);
}
});
this.setOnMousePressed(new EventHandler<MouseEvent>() {
@Override
public void handle(MouseEvent mouseEvent) {
handleMousePressed(mouseEvent);
}
});
this.setOnMouseReleased(new EventHandler<MouseEvent>() {
@Override
public void handle(MouseEvent mouseEvent) {
handleMouseRelease(mouseEvent);
}
});
this.setOnMouseDragged(new EventHandler<MouseEvent>() {
@Override
public void handle(MouseEvent mouseEvent) {
handleMouseDragged(mouseEvent);
}
});
this.widthProperty().addListener(new ChangeListener<Number>() {
@Override
public void changed(ObservableValue<? extends Number> observableValue, Number oldSceneWidth, Number newSceneWidth) {
gc.getCanvas().setWidth(newSceneWidth.doubleValue());
fullRefresh(gc);
}
});
this.heightProperty().addListener(new ChangeListener<Number>() {
@Override
public void changed(ObservableValue<? extends Number> observableValue, Number oldSceneHeight, Number newSceneHeight) {
gc.getCanvas().setHeight(newSceneHeight.doubleValue());
fullRefresh(gc);
}
});
this.termState.addUpdateListener(this);
}
private void handleMouseDragged(MouseEvent mouseEvent) {
if ( selStartPoint != null ) {
selCurPoint = getTermPoint(mouseEvent.getSceneY(), mouseEvent.getSceneX());
drawSelection(gc);
}
}
private void handleMouseRelease(MouseEvent mouseEvent) {
final TermPoint releasePoint = getTermPoint(mouseEvent.getSceneY(), mouseEvent.getSceneX());
Pair<Integer, Integer> xpair = PairUtils.getAscendingPair(releasePoint.x, selStartPoint.x);
Pair<Integer, Integer> ypair = PairUtils.getAscendingPair(releasePoint.y, selStartPoint.y);
if ( xpair.left < 0 ) xpair = new Pair<>(0, xpair.right);
if ( ypair.left < 0 ) ypair = new Pair<>(0, ypair.right);
StringBuilder selection = new StringBuilder();
for(int yidx = ypair.left; yidx <= ypair.right; ++yidx) {
for(int xidx = xpair.left; xidx <= xpair.right; ++xidx) {
if ( yidx < lines.size() && xidx < lines.get(yidx).getLine().length() ) {
selection.append( lines.get(yidx).getLine().charAt(xidx) );
} else {
selection.append(' ');
}
}
if ( yidx != ypair.right ) {
selection.append('\n');
}
}
StringSelection stringSelection = new StringSelection(selection.toString());
java.awt.Toolkit.getDefaultToolkit().getSystemClipboard().setContents(stringSelection, null);
selStartPoint = null;
drawScreen(gc);
}
private void handleMousePressed(MouseEvent mouseEvent) {
log.debug("Mouse pressed: " + mouseEvent);
selStartPoint = getTermPoint(mouseEvent.getSceneY(), mouseEvent.getSceneX());
drawScreen(gc);
}
private static TermPoint getTermPoint(double sceneY, double sceneX) {
final int chY = (int)(sceneY /lineHeight);
final int chX = (int)(sceneX /chWidth);
return new TermPoint(chX, chY);
}
private void handleKeyEvent(KeyEvent keyEvent) {
if ( keyEvent.getCode() == KeyCode.ENTER ) {
eventProcessor.onTerminalEvent(new NewLineEvent());
} else if ( keyEvent.isControlDown() ) {
eventProcessor.onTerminalEvent(
KeyboardEvent.builder(keyEvent.getCode())
.ctrlDown(keyEvent.isControlDown())
.shiftDown(keyEvent.isShiftDown())
.build()
);
} else if ( !keyEvent.getText().isEmpty() ) {
eventProcessor.onTerminalEvent(
new CharacterEvent(keyEvent.getCode(), keyEvent.getText().charAt(0))
);
} else {
eventProcessor.onTerminalEvent(
KeyboardEvent.builder(keyEvent.getCode())
.ctrlDown(keyEvent.isControlDown())
.shiftDown(keyEvent.isShiftDown())
.build()
);
}
keyEvent.consume();
}
@Override
public void onUpdate(TerminalState state) {
fullRefresh(gc);
}
private void fullRefresh(GraphicsContext gc) {
refreshLines(gc.getCanvas().getWidth(), gc.getCanvas().getHeight());
drawScreen(gc);
}
private void drawScreen(GraphicsContext gc) {
final Color bgColor = Color.BLACK;
final Color textColor = Color.rgb(0x00, 0xFF, 0x00);
gc.setFill(bgColor);
gc.fillRect(0, 0, gc.getCanvas().getWidth(), gc.getCanvas().getHeight());
gc.setFont(font);
gc.setFill(textColor);
float yPos = lineHeight;
for(Line line: lines) {
final StringBuilder currTextBuffer = line.getLine();
final int lineCurPos = line.getCursorPos();
gc.fillText(currTextBuffer.toString(), 0, yPos);
if ( lineCurPos != -1 ) {
final float curXPos = lineCurPos * chWidth;
final float curYPos = yPos-(lineHeight)+fontAscentDiff;
gc.fillRect(curXPos, curYPos, chWidth, lineHeight);
if ( lineCurPos != currTextBuffer.length() ) {
gc.setFill(bgColor);
gc.fillText(
currTextBuffer.charAt(lineCurPos) + "",
curXPos,
yPos);
gc.setFill(textColor);
}
}
yPos += lineHeight;
}
}
private void drawSelection(GraphicsContext gc) {
if ( selStartPoint != null ) {
this.drawScreen(gc);
final Pair<Integer, Integer> xpair = PairUtils.getAscendingPair(selCurPoint.x, selStartPoint.x);
final Pair<Integer, Integer> ypair = PairUtils.getAscendingPair(selCurPoint.y, selStartPoint.y);
double width = ((xpair.right - xpair.left)+1)*chWidth;
double height = ((ypair.right - ypair.left)+1)*lineHeight+fontAscentDiff;
final float selStartX = xpair.left*chWidth;
final float selStartY = ypair.left*lineHeight;
gc.save();
try {
gc.setGlobalBlendMode(BlendMode.EXCLUSION);
gc.fillRect(selStartX, selStartY, width, height);
} finally {
gc.restore();
}
}
}
private void refreshLines(double width, double height) {
final List<Line> screenLines = new LinkedList<Line>();
// start from the end, because the lines which are visible on the screen are the N last ones.
int lastIdx = termState.getLines().size() - 1;
for (int i = lastIdx; i != -1 && (screenLines.size()*lineHeight+lineHeight) <= height; --i) {
// as the order is reverse, grow from the head
screenLines.add(0, new Line(new StringBuilder(), -1));
float lineWidth = 0;
final String termLine = termState.getLines().get(i).getCompleteLine();
int printLine = 0;
for(int j = 0; j != termLine.length(); ++j) {
final char ch = termLine.charAt(j);
final float chWidth = Toolkit.getToolkit().getFontLoader().computeStringWidth(Character.toString(ch), font);
// line is too big to fit into one screen line
if ( (lineWidth+chWidth) > width ) {
screenLines.add(++printLine, new Line(new StringBuilder(), -1));
lineWidth = chWidth;
} else {
lineWidth += chWidth;
}
screenLines.get(printLine).getLine().append(ch);
}
// the first line, calculate cursor position
if ( i == lastIdx ) {
int curLine = 0;
int curPos = termState.getCursorPos();
while ( curLine != printLine
&& (curPos - screenLines.get(curLine).getLine().length()) >= 0 ) {
curPos -= screenLines.get(curLine).getLine().length();
++curLine;
}
screenLines.get(curLine).setCursorPos(curPos);
}
}
this.lines = screenLines;
}
}