Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Set 'step' location for debugger #165

Closed
bsutton opened this issue Aug 13, 2015 · 5 comments
Closed

Set 'step' location for debugger #165

bsutton opened this issue Aug 13, 2015 · 5 comments

Comments

@bsutton
Copy link

bsutton commented Aug 13, 2015

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;
        }
    }
}

@TomasMikula
Copy link
Member

Hi Brett,

you need to change this line

IntFunction<Node> arrowFactory = new ArrowFactory(scriptEditor.currentParagraphProperty());

to

IntFunction<Node> arrowFactory = new ArrowFactory(currentStep);

That requires you to make currentStep a property.

@bsutton
Copy link
Author

bsutton commented Aug 14, 2015

Got it working so thanks for the prompt answer.

For the record here are the required changes.

currentStep become:
CurrentStep currentStep = new CurrentStep();

The Arrow is constructed as:
IntFunction arrowFactory = new ArrowFactory(currentStep);

I had to implement an observable for Current Step as:


    class CurrentStep implements ObservableValue<Integer>
    {
        private HashMap<ChangeListener<? super Integer>, ChangeListener<? super Integer>> changeListeners = new HashMap<>();

        private HashMap<InvalidationListener, InvalidationListener> invalidateListeners = new HashMap<>();

        private int value = 0;

        public void setValue(int value)
        {
            int oldValue = this.value;
            this.value = value;

            if (value == -1)
            {
                if (oldValue != -1)
                    for (InvalidationListener listener : invalidateListeners.values())
                    {
                        listener.invalidated(this);
                    }
            }
            else
                for (ChangeListener<? super Integer> listener : changeListeners.values())
                {
                    listener.changed(this, oldValue, value);
                }
        }

        public int get()
        {
            return value;
        }

        @Override
        public void addListener(ChangeListener<? super Integer> listener)
        {
            changeListeners.put(listener, listener);

        }

        @Override
        public void removeListener(ChangeListener<? super Integer> listener)
        {
            changeListeners.remove(listener);

        }

        @Override
        public Integer getValue()
        {
            return new Integer(value);
        }

        @Override
        public void addListener(InvalidationListener listener)
        {
            invalidateListeners.put(listener, listener);
        }

        @Override
        public void removeListener(InvalidationListener listener)
        {
            invalidateListeners.remove(listener);

        }
    };

The full code 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.HashMap;
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.InvalidationListener;
import javafx.beans.value.ChangeListener;
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;

    CurrentStep currentStep = new CurrentStep();

    private MainUIController mainController;

    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(currentStep);
                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.setValue(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.get() == cmds.length)
        {
            scriptEditor.setStyleClass(0, scriptEditor.getText().length(), "black");
            display.showMessage("Sequence complete. Resetting to start");
            currentStep.setValue(0);
        }
        else
        {
            int startCharacter = 0;
            int endCharacter = cmds[0].length();
            // determine character position of current step
            for (int i = 1; i <= currentStep.get(); i++)
            {
                startCharacter = endCharacter + 1;
                endCharacter += cmds[i].length() + 1;
            }

            String cmd = cmds[currentStep.get()].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.get() >= 0)
            {
                stepSequence(cmd);
            }

            currentStep.setValue(currentStep.get() + 1);
            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;
        }
    }

    class CurrentStep implements ObservableValue<Integer>
    {
        private HashMap<ChangeListener<? super Integer>, ChangeListener<? super Integer>> changeListeners = new HashMap<>();

        private HashMap<InvalidationListener, InvalidationListener> invalidateListeners = new HashMap<>();

        private int value = 0;

        public void setValue(int value)
        {
            int oldValue = this.value;
            this.value = value;

            if (value == -1)
            {
                if (oldValue != -1)
                    for (InvalidationListener listener : invalidateListeners.values())
                    {
                        listener.invalidated(this);
                    }
            }
            else
                for (ChangeListener<? super Integer> listener : changeListeners.values())
                {
                    listener.changed(this, oldValue, value);
                }
        }

        public int get()
        {
            return value;
        }

        @Override
        public void addListener(ChangeListener<? super Integer> listener)
        {
            changeListeners.put(listener, listener);

        }

        @Override
        public void removeListener(ChangeListener<? super Integer> listener)
        {
            changeListeners.remove(listener);

        }

        @Override
        public Integer getValue()
        {
            return new Integer(value);
        }

        @Override
        public void addListener(InvalidationListener listener)
        {
            invalidateListeners.put(listener, listener);
        }

        @Override
        public void removeListener(InvalidationListener listener)
        {
            invalidateListeners.remove(listener);

        }
    };

}

@bsutton bsutton closed this as completed Aug 14, 2015
@bsutton bsutton reopened this Aug 14, 2015
@bsutton
Copy link
Author

bsutton commented Aug 14, 2015

I've re-opened this issue as its not quite working.

Whilst the triangle is now moved to the correct line it only gets re-displayed if you cause that area of the editor to re-draw.

I note that my CurrentStep ObservableValue class only ever gets InvalidationListeners added. I was expecting the CodeArea to add change listeners so that when I change the line number it would force a refresh of the old/new locations of the arrow indicataor.

I dug into the code of CodeArea but I really can work out what is going on. I thought looking at the code that updates the Carat position might provide some insight:

 @Override
    public void positionCaret(int pos) {
        try(Guard g = suspend(caretPosition, currentParagraph, caretColumn)) {
            internalCaretPosition.setValue(pos);
        }

But this code really didn't really help. I've got no idea what the suspend method does.

What do I need to do?

@TomasMikula
Copy link
Member

Hi Brett,

first of all, you don't need a special CurrentStep class. Just define currentStep like this:

Property<Integer> currentStep = new SimpleObjectProperty<>(0);

If that doesn't solve your problem, please try to provide a simple self-contained example that I can help troubleshoot.

@TomasMikula
Copy link
Member

Please, reopen with a SSCCE if the problem still persists.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants