xn139
xn139

Reputation: 395

Implementing Undo/Redo in JavaFX

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

        };

    }

Upvotes: 4

Views: 8525

Answers (1)

fabian
fabian

Reputation: 82451

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.

Upvotes: 7

Related Questions