3

I'm trying to implement undo/redo in JavaFX - I draw all my shapes using graphicsContext(). I have looked around and found that there's a save method on Graphics Context but it just saves attributes and not the actual shape/state of the canvas. What would be the best way of going about this?

This is one of my code snippets when I create a circle, for instance:

 public CircleDraw(Canvas canvas, Scene scene, BorderPane borderPane) {
        this.borderPane = borderPane;
        this.scene = scene;
        this.graphicsContext = canvas.getGraphicsContext2D();

        ellipse = new Ellipse();
        ellipse.setStrokeWidth(1.0);
        ellipse.setFill(Color.TRANSPARENT);
        ellipse.setStroke(Color.BLACK);

        pressedDownMouse = event -> {
            startingPosX = event.getX();
            startingPosY = event.getY();
            ellipse.setCenterX(startingPosX);
            ellipse.setCenterY(startingPosY);
            ellipse.setRadiusX(0);
            ellipse.setRadiusY(0);
            borderPane.getChildren().add(ellipse);

        };

        releasedMouse = event -> {
            borderPane.getChildren().remove(ellipse);
            double width = Math.abs(event.getX() - startingPosX);
            double height = Math.abs(event.getY() - startingPosY);
            graphicsContext.setStroke(Color.BLACK);
            graphicsContext.strokeOval(Math.min(startingPosX, event.getX()),    Math.min(startingPosY, event.getY()), width, height);
        removeListeners();
        };

        draggedMouse = event -> {
            ellipse.setCenterX((event.getX() + startingPosX) / 2);
            ellipse.setCenterY((event.getY() + startingPosY) / 2);
            ellipse.setRadiusX(Math.abs((event.getX() - startingPosX) / 2));
            ellipse.setRadiusY(Math.abs((event.getY() - startingPosY) / 2));

        };

    }
xn139
  • 375
  • 1
  • 4
  • 16
  • 2
    The [UndoFX library](https://github.com/TomasMikula/UndoFX) might assist you in solving your problem. The library just provides an abstract Undo/Redo state manager, so it doesn't solve the problem out of the box, you would need quite a bit of some custom code within your application to make appropriate use of it (e.g. things such as the `EllipseDrawOperation` from fabian's solution would still be required the UndoFX library just provides a place for storing and manipulating the history of such operations). – jewelsea Nov 16 '16 at 23:28

1 Answers1

7

The problem here is that there is that information like this is not saved in a Canvas. Furthermore there is no inverse operation that allows you to get back to the previous state for every draw information. Surely you could stroke the same oval, but with backgrund color, however the information from previous drawing information could have been overwritten, e.g. if you're drawing multiple intersecting ovals.

You could store the drawing operations using the command pattern however. This allows you to redraw everything.

public interface DrawOperation {
    void draw(GraphicsContext gc);
}

public class DrawBoard {
    private final List<DrawOperation> operations = new ArrayList<>();
    private final GraphicsContext gc;
    private int historyIndex = -1;

    public DrawBoard(GraphicsContext gc) {
        this.gc = gc;
    }

    public void redraw() {
        Canvas c = gc.getCanvas();
        gc.clearRect(0, 0, c.getWidth(), c.getHeight());
        for (int i = 0; i <= historyIndex; i++) {
            operations.get(i).draw(gc);
        }
    }

    public void addDrawOperation(DrawOperation op) {
        // clear history after current postion
        operations.subList(historyIndex+1, operations.size()).clear();

        // add new operation
        operations.add(op);
        historyIndex++;
        op.draw(gc);
    }

    public void undo() {
        if (historyIndex >= 0) {
            historyIndex--;
            redraw();
        }
    }

    public void redo() {
        if (historyIndex < operations.size()-1) {
            historyIndex++;
            operations.get(historyIndex).draw(gc);
        }
    }
}

class EllipseDrawOperation implements DrawOperation {

    private final double minX;
    private final double minY;
    private final double width;
    private final double height;
    private final Paint stroke;

    public EllipseDrawOperation(double minX, double minY, double width, double height, Paint stroke) {
        this.minX = minX;
        this.minY = minY;
        this.width = width;
        this.height = height;
        this.stroke = stroke;
    }

    @Override
    public void draw(GraphicsContext gc) {
        gc.setStroke(stroke);
        gc.strokeOval(minX, minY, width, height);
    }

}

Pass a DrawBoard instance to your class instead of the Canvas and replace

graphicsContext.setStroke(Color.BLACK);
graphicsContext.strokeOval(Math.min(startingPosX, event.getX()),    Math.min(startingPosY, event.getY()), width, height);

with

drawBoard.addDrawOperation(new EllipseDrawOperation(
                             Math.min(startingPosX, event.getX()),
                             Math.min(startingPosY, event.getY()),
                             width,
                             height,
                             Color.BLACK));

The undo and redo to move through the history.

fabian
  • 80,457
  • 12
  • 86
  • 114
  • This is so so helpful fabian, thank you - I have one question though. I keep on getting an error if I draw another circle (after I've drawn the first circle) - thrown on this line: `operations.subList(historyIndex + 1, operations.size()).clear();` I get a: `"JavaFX Application Thread" java.lang.IllegalArgumentException: fromIndex(1) > toIndex(0)`. I'm not too sure why this is happening? – xn139 Nov 17 '16 at 00:39
  • @xn139: Sorry, but currently I cannot find a sequence of operations to reproduce this right now. Besides: The methods modifying `historyIndex` (`undo`/`redo`/`addDrawOperation`)/ the list (`addDrawOperation`) size should ensure `historyIndex` is always a valid index or `-1`, so `historyIndex <= size` should be ensured. – fabian Nov 17 '16 at 01:06
  • @fabian Drawing with a transparent color over something else does not change anything. That's the purpose of a transparent color. – mipa Nov 17 '16 at 07:20
  • Has been corrected in the answer by the author now. (Just to avoid confusion.) – mipa Nov 17 '16 at 09:34
  • @fabian, perfect thank you - I've fixed the issue I was having. Thanks for your help! – xn139 Nov 17 '16 at 09:42