So I've got a little scripting language for a robotic arm and I'm trying to implement the ability to 'step' through the code.
I need to highlight the current line that the 'debug stepper' is currently on.
I've been trying to adapt code which implements a green arrow to indicate a break point.
The problem is that the code always moves the green arrow to the 'current cursor' location within a CodeArea.
I need to be able to 'select' the line using a 'next step' button.
The pertinent code is:
scriptEditor = new CodeArea();
numberFactory = LineNumberFactory.get(scriptEditor);
IntFunction<Node> arrowFactory = new ArrowFactory(scriptEditor.currentParagraphProperty());
IntFunction<Node> graphicFactory = line -> {
HBox hbox = new HBox(numberFactory.apply(line), arrowFactory.apply(line));
hbox.setAlignment(Pos.CENTER_LEFT);
return hbox;
};
scriptEditor.setParagraphGraphicFactory(graphicFactory);
and the arrow function:
class ArrowFactory implements IntFunction<Node>
{
private final ObservableValue<Integer> shownLine;
ArrowFactory(ObservableValue<Integer> shownLine)
{
this.shownLine = shownLine;
}
@Override
public Node apply(int lineNumber)
{
Polygon triangle = new Polygon(0.0, 0.0, 10.0, 5.0, 0.0, 10.0);
triangle.setFill(Color.GREEN);
ObservableValue<Boolean> visible = Val.map(shownLine, sl -> sl == lineNumber);
triangle.visibleProperty().bind(Val.flatMap(triangle.sceneProperty(), scene -> {
return scene != null ? visible : Val.constant(false);
}));
return triangle;
}
}
I've played with the 'lineNumber' changing it to use a field which contains the current 'step line' but this just results in every line getting an arrow as I step through the code.
Any assistance would be greatly appreciated.
The full class is:
package controller;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.net.URL;
import java.nio.file.Files;
import java.time.Duration;
import java.util.Collection;
import java.util.Collections;
import java.util.Optional;
import java.util.ResourceBundle;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeoutException;
import java.util.function.IntFunction;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Stream;
import javafx.application.Platform;
import javafx.beans.value.ObservableValue;
import javafx.concurrent.Task;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.control.Alert;
import javafx.scene.control.Alert.AlertType;
import javafx.scene.control.ButtonType;
import javafx.scene.control.TextArea;
import javafx.scene.control.TextField;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox;
import javafx.scene.paint.Color;
import javafx.scene.shape.Polygon;
import javafx.stage.FileChooser;
import org.fxmisc.richtext.CodeArea;
import org.fxmisc.richtext.LineNumberFactory;
import org.fxmisc.richtext.PlainTextChange;
import org.fxmisc.richtext.StyleSpans;
import org.fxmisc.richtext.StyleSpansBuilder;
import org.reactfx.EventStream;
import org.reactfx.util.Try;
import org.reactfx.value.Val;
import robot.IllegalCommandException;
import robot.InvaidMotorFrequency;
import robot.InvalidMotorConfiguration;
import robot.InvalidMotorException;
import robot.NotConnectedException;
import robot.iRobot;
import application.iDisplay;
public class ScriptController implements iController, Initializable
{
private static final String[] KEYWORDS = new String[]
{ "mov", "on", "set", "wait", "stop" };
private static final String KEYWORD_PATTERN = "\\b(" + String.join("|", KEYWORDS) + ")\\b";
private static final String COMMENT_PATTERN = "//[^\n]*" + "|" + "/\\*(.|\\R)*?\\*/";
private static final Pattern PATTERN = Pattern.compile("(?<KEYWORD>" + KEYWORD_PATTERN + ")" + "|(?<COMMENT>"
+ COMMENT_PATTERN + ")");
@FXML
TextArea scriptEditorPlaceHolder;
CodeArea scriptEditor;
private ExecutorService executor;
@FXML
HBox nextCmdLine;
@FXML
TextField nextCmd;
private File activeFile = null;
private iDisplay display;
private iRobot robot;
private File lastDir;
private MainUIController mainController;
private int currentStep = 0;
private IntFunction<Node> numberFactory;
@Override
public void init(MainUIController mainController, iDisplay display)
{
this.mainController = mainController;
this.display = display;
this.robot = mainController.getRobot();
this.lastDir = robot.getLastSaveDirectory();
VBox parent = (VBox) scriptEditorPlaceHolder.getParent();
int pos = 0;
for (Node child : parent.getChildrenUnmodifiable())
{
if (child == scriptEditorPlaceHolder)
{
parent.getChildren().remove(child);
executor = Executors.newSingleThreadExecutor();
scriptEditor = new CodeArea();
numberFactory = LineNumberFactory.get(scriptEditor);
IntFunction<Node> arrowFactory = new ArrowFactory(scriptEditor.currentParagraphProperty());
IntFunction<Node> graphicFactory = line -> {
HBox hbox = new HBox(numberFactory.apply(line), arrowFactory.apply(line));
hbox.setAlignment(Pos.CENTER_LEFT);
return hbox;
};
scriptEditor.setParagraphGraphicFactory(graphicFactory);
scriptEditor.textProperty().addListener(
(obs, oldText, newText) -> {
scriptEditor.setStyleSpans(0, computeHighlighting(newText));
EventStream<PlainTextChange> textChanges = scriptEditor.plainTextChanges();
textChanges.successionEnds(Duration.ofMillis(500))
.supplyTask(this::computeHighlightingAsync).awaitLatest(textChanges).map(Try::get)
.subscribe(this::applyHighlighting);
});
VBox.setVgrow(scriptEditor, Priority.ALWAYS);
parent.getChildren().add(pos, scriptEditor);
break;
}
}
}
private Task<StyleSpans<Collection<String>>> computeHighlightingAsync()
{
String text = scriptEditor.getText();
Task<StyleSpans<Collection<String>>> task = new Task<StyleSpans<Collection<String>>>()
{
@Override
protected StyleSpans<Collection<String>> call() throws Exception
{
return computeHighlighting(text);
}
};
executor.execute(task);
return task;
}
private void applyHighlighting(StyleSpans<Collection<String>> highlighting)
{
scriptEditor.setStyleSpans(0, highlighting);
}
private static StyleSpans<Collection<String>> computeHighlighting(String text)
{
Matcher matcher = PATTERN.matcher(text);
int lastKwEnd = 0;
StyleSpansBuilder<Collection<String>> spansBuilder = new StyleSpansBuilder<>();
while (matcher.find())
{
String styleClass = matcher.group("KEYWORD") != null ? "keyword"
: matcher.group("COMMENT") != null ? "comment" : null; /*
* never
* happens
*/
assert styleClass != null;
spansBuilder.add(Collections.emptyList(), matcher.start() - lastKwEnd);
spansBuilder.add(Collections.singleton(styleClass), matcher.end() - matcher.start());
lastKwEnd = matcher.end();
}
spansBuilder.add(Collections.emptyList(), text.length() - lastKwEnd);
return spansBuilder.create();
}
@Override
public void initialize(URL location, ResourceBundle resources)
{
nextCmdLine.setDisable(true);
}
@FXML
void onOpen(ActionEvent event)
{
// Create a file chooser
final FileChooser fc = new FileChooser();
fc.setInitialDirectory(lastDir);
FileChooser.ExtensionFilter filter = new FileChooser.ExtensionFilter("Robot Move files", "rmf");
fc.setSelectedExtensionFilter(filter);
// In response to a button click:
File openedFile = fc.showOpenDialog(null);
if (openedFile != null)
{
try
{
activeFile = openedFile;
lastDir = activeFile.getParentFile();
robot.setLastSaveDirectory(fc.getInitialDirectory());
scriptEditor.clear();
try (Stream<String> lines = Files.lines(activeFile.toPath()))
{
lines.forEach(s -> scriptEditor.appendText(s + "\n"));
}
}
catch (IOException e)
{
display.showException(e);
}
}
}
@FXML
void onSave(ActionEvent event)
{
if (activeFile != null)
{
BufferedWriter writer;
try
{
writer = new BufferedWriter(new FileWriter(activeFile));
writer.write(scriptEditor.getText());
writer.close();
}
catch (IOException e)
{
display.showException(e);
}
}
else
onSaveAs(event);
}
@FXML
void onSaveAs(ActionEvent event)
{
// Create a file chooser
final FileChooser fc = new FileChooser();
fc.setInitialDirectory(lastDir);
FileChooser.ExtensionFilter filter = new FileChooser.ExtensionFilter("Robot Move files", "rmf");
fc.setSelectedExtensionFilter(filter);
// In response to a button click:
File savedFile = fc.showSaveDialog(null);
if (savedFile != null)
{
activeFile = savedFile;
lastDir = activeFile.getParentFile();
if (!activeFile.getName().endsWith(".rmf"))
activeFile = new File(activeFile.getParentFile(), activeFile.getName() + ".rmf");
onSave(event);
robot.setLastSaveDirectory(fc.getInitialDirectory());
}
}
@FXML
void onNew(ActionEvent event)
{
Alert alert = new Alert(AlertType.CONFIRMATION);
alert.setTitle("Confirm New action");
alert.setHeaderText(null);
alert.setContentText("Are you sure? You will loose any unsaved changes");
Optional<ButtonType> result = alert.showAndWait();
if (result.get() == ButtonType.OK)
{
activeFile = null;
scriptEditor.clear();
}
}
@FXML
void onRun(ActionEvent event)
{
runSequence(scriptEditor.getText());
}
@FXML
void onRestart(ActionEvent event)
{
currentStep = 0;
scriptEditor.setStyleClass(0, scriptEditor.getText().length(), "black");
}
@FXML
void onStep(ActionEvent event)
{
// not very efficient but means we can handle editing whilst stepping
String[] cmds = scriptEditor.getText().split("\n");
if (currentStep == cmds.length)
{
scriptEditor.setStyleClass(0, scriptEditor.getText().length(), "black");
display.showMessage("Sequence complete. Resetting to start");
currentStep = 0;
}
else
{
int startCharacter = 0;
int endCharacter = cmds[0].length();
// determine character position of current step
for (int i = 1; i <= currentStep; i++)
{
startCharacter = endCharacter + 1;
endCharacter += cmds[i].length() + 1;
}
String cmd = cmds[currentStep].trim();
scriptEditor.setStyleClass(0, startCharacter, "black");
scriptEditor.setStyleClass(startCharacter, endCharacter, "blue");
// scriptEditor.setStyle(startCharacter, endCharacter,
// "-fx-strikethrough");
// First time step is click we prime the pump
if (currentStep >= 0)
{
// stepSequence(cmd);
}
currentStep++;
this.nextCmd.setText(cmd);
}
}
private void stepSequence(String nextCommand)
{
try
{
this.robot.sendCmd(nextCommand, display);
}
catch (Exception e1)
{
display.showException(e1);
}
}
public void runSequence(String text)
{
if (!robot.isConnected())
this.display.showError("Device is not connected");
else
{
// push into the background so we don't lock the UI.
ExecutorService executor = Executors.newSingleThreadExecutor();
executor.submit(() -> {
try
{
String[] cmds = text.split("\n");
int line = 1;
for (String cmd : cmds)
{
scriptEditor.setStyleClass(line, line, ".step-highlite");
if (cmd != null)
this.robot.sendCmd(cmd.trim(), display);
line++;
}
Platform.runLater(() -> this.display.append("Sequence sent in full\n"));
}
catch (NotConnectedException | IOException | TimeoutException | InvalidMotorConfiguration
| InvaidMotorFrequency | InvalidMotorException | IllegalCommandException e)
{
Platform.runLater(() -> display.showException(e));
}
});
}
}
class ArrowFactory implements IntFunction<Node>
{
private final ObservableValue<Integer> shownLine;
ArrowFactory(ObservableValue<Integer> shownLine)
{
this.shownLine = shownLine;
}
@Override
public Node apply(int lineNumber)
{
Polygon triangle = new Polygon(0.0, 0.0, 10.0, 5.0, 0.0, 10.0);
triangle.setFill(Color.GREEN);
ObservableValue<Boolean> visible = Val.map(shownLine, sl -> sl == lineNumber);
triangle.visibleProperty().bind(Val.flatMap(triangle.sceneProperty(), scene -> {
return scene != null ? visible : Val.constant(false);
}));
return triangle;
}
}
}
So I've got a little scripting language for a robotic arm and I'm trying to implement the ability to 'step' through the code.
I need to highlight the current line that the 'debug stepper' is currently on.
I've been trying to adapt code which implements a green arrow to indicate a break point.
The problem is that the code always moves the green arrow to the 'current cursor' location within a CodeArea.
I need to be able to 'select' the line using a 'next step' button.
The pertinent code is:
and the arrow function:
I've played with the 'lineNumber' changing it to use a field which contains the current 'step line' but this just results in every line getting an arrow as I step through the code.
Any assistance would be greatly appreciated.
The full class is: