Home > other >  Implement undo/redo button in JavaFX
Implement undo/redo button in JavaFX

Time:11-05

Ive been scrolling and scrolling and i dont seem to find any solution. I am making a simple java fx program where i draw different shapes. The problem is that i dont know how to implement an undo button. I have found that you can use stacks, command pattern, arraydeque and so on... But i cant fix it..

Here is some of the code.

public class shapeModel {

    //private final List<Shape> undoHistory = new ArrayList<>();
    //private final Deque<Deque<Shape>> redoHistory;


    private final ObservableList<Shape> shapeList;
    private final ObjectProperty<Color> colorPickerSelect;

    private final int historyIndex = -1;
    private ShapeType currentShape;

    public shapeModel() {

        //this.redoHistory = new ArrayDeque<>();
        this.colorPickerSelect = new SimpleObjectProperty<>(Color.GREEN);
        this.shapeList = FXCollections.observableArrayList(shape -> new Observable[]{
                shape.colorProperty()


        });
    }


    public ShapeType getCurrentShape() {
        return currentShape;
    }

    public void setCurrentShape(ShapeType currentShape) {
        this.currentShape = currentShape;
    }


    public void addShapes(Shape shape) {

        if (!(shape == null))
            this.shapeList.add(shape);

    }


    public Color getColorPickerSelect() {
        return colorPickerSelect.get();
    }

    public ObjectProperty<Color> colorPickerSelectProperty() {
        return colorPickerSelect;
    }

    public ObservableList<Shape> getShapeObservableList() {
        return shapeList;
    }

    public void undo() {



    }
}

Controller class:

public class HelloController {

    public Button eraserButton;
    public Button undoButton;
    public BorderPane scenePane;
    public ColorPicker myColorPicker;
    public ChoiceBox<String> myChoiceBox;
    public GraphicsContext context;
    public Canvas canvas;
    public shapeModel model;



    public void initialize() {
        context = canvas.getGraphicsContext2D();
        model.getShapeObservableList().addListener((ListChangeListener<Shape>) e -> listChanged());   
        myColorPicker.valueProperty().bindBidirectional(model.colorPickerSelectProperty());
    }

    public HelloController() {

        this.model = new shapeModel();

    }

//
//    private int count = 0;
//
//    private final Shape[] shapes = new Shape[30];

   

    public void canvasClicked(MouseEvent mouseEvent) {

        Shape shape = Shape.creatingShapes(model.getCurrentShape(),mouseEvent.getX(),mouseEvent.getY(),50,50,myColorPicker.getValue());

        model.addShapes(shape);

//        redraw();
//        shapes[count] = shape;
//        count  ;
      //  paintCanvas();

    }
    public void listChanged() {

        var context = canvas.getGraphicsContext2D();
        model.getShapeObservableList().forEach(s -> s.draw(context));

    }



    public void undoClicked() {



    }

//    public void redo() {
//
//        if(historyIndex < model.getShapeObservableList().size()-1) {
//            historyIndex  ;
//            model.getShapeObservableList(),(historyIndex).
//        }
//
//    }

    public void handleClick() {

        FileChooser chooser = new FileChooser();
        File file = chooser.showSaveDialog(scenePane.getScene().getWindow());




    }


    public void onCircleClicked(ActionEvent e) {

        model.setCurrentShape(ShapeType.CIRCLE);


    }

    public void onRectangleClicked(ActionEvent e) {


        model.setCurrentShape(ShapeType.RECTANGLE);

    }

    public void exitAction(ActionEvent actionEvent) {
        Platform.exit();

    }

    public void eraser() {

        context.setFill(Color.WHITE);
        context.fillRect(0,0,canvas.getWidth(), canvas.getHeight());
       // context.clearRect(0, 0, canvas.getWidth(), canvas.getHeight());
    }



}

CodePudding user response:

Maybe this can help. This is my first time attempting something like this. The experts may have a better, more efficient way of doing this.

My idea was to keep up with a List of WritableImages as you draw. Use that list to go forward and backward through history.


Main

import java.io.File;
import java.io.FileInputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
import javafx.application.Application;
import javafx.embed.swing.SwingFXUtils;
import javafx.scene.Scene;
import javafx.scene.SnapshotParameters;
import javafx.scene.canvas.Canvas;
import javafx.scene.canvas.GraphicsContext;
import javafx.scene.control.Button;
import javafx.scene.image.Image;
import javafx.scene.image.WritableImage;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.Background;
import javafx.scene.layout.BackgroundImage;
import javafx.scene.layout.BackgroundPosition;
import javafx.scene.layout.BackgroundRepeat;
import javafx.scene.layout.BackgroundSize;
import javafx.scene.layout.HBox;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import javafx.scene.paint.Color;
import javafx.stage.Stage;
import javax.imageio.ImageIO;

/**
 * Code from: @web http://java-buddy.blogspot.com/
 */
public class App extends Application {

    Canvas canvas = new Canvas(400, 400);
    StackPane spCanvasHolder = new StackPane(canvas);
    List<WritableImage> history = new ArrayList();
    AtomicInteger currentlyDisplaying = new AtomicInteger();
    
    @Override
    public void start(Stage primaryStage) {

        
        final GraphicsContext graphicsContext = canvas.getGraphicsContext2D();
        initDraw(graphicsContext);

        canvas.addEventHandler(MouseEvent.MOUSE_PRESSED, (MouseEvent event) ->
        {
            graphicsContext.beginPath();
            graphicsContext.moveTo(event.getX(), event.getY());
            graphicsContext.stroke();
            history.add(spCanvasHolder.snapshot(new SnapshotParameters(), null));
            System.out.println("Current Display: "   currentlyDisplaying.incrementAndGet());
        });

        canvas.addEventHandler(MouseEvent.MOUSE_DRAGGED, (MouseEvent event) ->
        {
            graphicsContext.lineTo(event.getX(), event.getY());
            graphicsContext.stroke();
            history.add(spCanvasHolder.snapshot(new SnapshotParameters(), null));
            System.out.println("Current Display: "   currentlyDisplaying.incrementAndGet());
        });

        canvas.addEventHandler(MouseEvent.MOUSE_RELEASED, (MouseEvent event) ->
        {
            System.out.println("History size: "   history.size());
        });

        Button btnForwardHistory = new Button(">");
        btnForwardHistory.setOnAction((t) ->
        {
            if(currentlyDisplaying.get() < history.size())
            {
                File file = new File("CanvasTemp.png");
                try 
                {
                    canvas.getGraphicsContext2D().clearRect(0, 0, canvas.getWidth(), canvas.getHeight());
                    ImageIO.write(SwingFXUtils.fromFXImage(history.get(currentlyDisplaying.incrementAndGet()), null), "png", file);
                    BackgroundImage bI = new BackgroundImage(new Image(new FileInputStream(file)), BackgroundRepeat.NO_REPEAT, BackgroundRepeat.NO_REPEAT, BackgroundPosition.DEFAULT, BackgroundSize.DEFAULT);
                    spCanvasHolder.setBackground(new Background(bI));
                }
                catch (Exception s) {
                    System.out.println(s.toString());
                }   
            }
        });
        
        Button btnBackwardHistory = new Button("<");
        btnBackwardHistory.setOnAction((t) ->
        {
            System.out.println("Current Display: "   currentlyDisplaying.get());
            if(currentlyDisplaying.get() > 0)
            {
                File file = new File("CanvasTemp.png");
                try 
                {
                    canvas.getGraphicsContext2D().clearRect(0, 0, canvas.getWidth(), canvas.getHeight());
                    ImageIO.write(SwingFXUtils.fromFXImage(history.get(currentlyDisplaying.decrementAndGet()), null), "png", file);
                    BackgroundImage bI = new BackgroundImage(new Image(new FileInputStream(file)), BackgroundRepeat.NO_REPEAT, BackgroundRepeat.NO_REPEAT, BackgroundPosition.DEFAULT, BackgroundSize.DEFAULT);
                    spCanvasHolder.setBackground(new Background(bI));
                }
                catch (Exception s) {
                    System.out.println(s.toString());
                }   
            }
        });
        
        VBox vbHistoryControls = new VBox(btnBackwardHistory, btnForwardHistory);
        StackPane root = new StackPane();
        root.getChildren().add(new HBox(vbHistoryControls, spCanvasHolder));
        Scene scene = new Scene(root);
        primaryStage.setTitle("java-buddy.blogspot.com");
        primaryStage.setScene(scene);
        primaryStage.show();
    }

    public static void main(String[] args) {
        launch(args);
    }

    private void initDraw(GraphicsContext gc){
        double canvasWidth = gc.getCanvas().getWidth();
        double canvasHeight = gc.getCanvas().getHeight();

        gc.setFill(Color.LIGHTGRAY);
        gc.setStroke(Color.BLACK);
        gc.setLineWidth(5);

        gc.fill();
        gc.strokeRect(
                0,              //x of the upper left corner
                0,              //y of the upper left corner
                canvasWidth,    //width of the rectangle
                canvasHeight);  //height of the rectangle

        gc.setFill(Color.RED);
        gc.setStroke(Color.BLUE);
        gc.setLineWidth(1);

    }

}

module-info

module com.mycompany.javafxsimpletest {
    requires javafx.controls;
    requires javafx.swing;
    
    
    exports com.mycompany.javafxsimpletest;
}

Output

enter image description here

I would not use this code in a production app, but maybe the ideas can get you in the right direction.

CodePudding user response:

This example is based on the UndoFX framework:

  • example

    The example:

    1. Uses the scene graph instead of a Canvas.
    2. Only adds shapes to a pane.
    3. Provides the ability to remove the added shapes and add them back again.

    The example is probably overcomplicated for the limited functionality it provides but might be structured in such a way that it is more easily adaptable to adding additional functionality.

    The example does not demonstrate manipulating shapes (such as selecting them, changing their border and fill colors, changing their sizes, etc) and undoing or redoing such changes. But if you wanted to extend functionality for that, you could use the app as a basis and look into the UndoFX demo app which shows how to apply undo and redo to multiple properties on a shape. Depending on the functionality added, this could be quite a difficult task as generic undo/redo across a wide range of functionality is inherently difficult (IMO).

    com/example/shapechanger/ShapeChangerApp.java

    package com.example.shapechanger;
    
    import javafx.application.Application;
    import javafx.beans.property.*;
    import javafx.scene.Node;
    import javafx.scene.Scene;
    import javafx.scene.control.*;
    import javafx.scene.layout.*;
    import javafx.scene.paint.Color;
    import javafx.scene.shape.*;
    import javafx.stage.Stage;
    import org.fxmisc.undo.UndoManager;
    import org.fxmisc.undo.UndoManagerFactory;
    import org.reactfx.EventSource;
    
    import java.util.List;
    import java.util.Map;
    import java.util.Objects;
    
    public class ShapeChangerApp extends Application {
    
        @Override
        public void start(Stage stage) {
            Model model = new Model();
            EventSource<ShapeChange> changes = new EventSource<>();
            UndoManager<ShapeChange> undoManager = new UndoProvider(changes).getUndoManager();
    
            Ribbon ribbon = new Ribbon(model, undoManager);
            DrawingPane drawingPane = new DrawingPane(model, changes);
    
            ScrollPane scrollPane = new ScrollPane(drawingPane);
            scrollPane.setPrefSize(300, 300);
    
            VBox layout = new VBox(ribbon, scrollPane);
            VBox.setVgrow(scrollPane, Priority.ALWAYS);
    
            Scene scene = new Scene(layout);
            stage.setScene(scene);
            stage.show();
        }
    
        public static void main(String[] args) {
            launch();
        }
    }
    
    class DrawingPane extends Pane {
        private static final double DRAWING_PANE_SIZE = 1_000;
        private final Model model;
        private final EventSource<ShapeChange> changes;
        private final ShapeFactory shapeFactory = new ShapeFactory();
    
        public DrawingPane(Model model, EventSource<ShapeChange> changes) {
            this.model = model;
            this.changes = changes;
            setOnMouseClicked(e -> addShape(e.getX(), e.getY()));
    
            setMinSize(Region.USE_PREF_SIZE, Region.USE_PREF_SIZE);
            setPrefSize(DRAWING_PANE_SIZE, DRAWING_PANE_SIZE);
            setMaxSize(Region.USE_PREF_SIZE, Region.USE_PREF_SIZE);
        }
    
        private void addShape(double x, double y) {
            DrawableShape drawableShape = model.selectedShapeProperty().get();
            Shape shape = shapeFactory.create(
                    drawableShape, x, y
            );
    
            ShapeChange shapeChange = new ShapeChange(
                    changes, this, shape, true
            );
            shapeChange.apply();
        }
    }
    
    class Model {
        private final ObjectProperty<DrawableShape> selectedShape = new SimpleObjectProperty<>();
    
        public ObjectProperty<DrawableShape> selectedShapeProperty() {
            return selectedShape;
        }
    }
    
    class Ribbon extends ToolBar {
        public Ribbon(Model model, UndoManager<ShapeChange> undoManager) {
            UndoControls undoControls = new UndoControls(undoManager);
            ShapeSelector shapeSelector = new ShapeSelector();
            model.selectedShapeProperty().bindBidirectional(
                    shapeSelector.selectedShapeProperty()
            );
    
            getItems().addAll(undoControls.getControls());
            getItems().add(createSpacer());
            getItems().addAll(shapeSelector.getControls());
        }
    
        private Node createSpacer() {
            Pane spacer = new Pane();
            spacer.setPrefWidth(10);
    
            return spacer;
        }
    }
    
    class UndoProvider {
        private final UndoManager<ShapeChange> undoManager;
    
        public UndoProvider(EventSource<ShapeChange> changes) {
            undoManager = UndoManagerFactory.unlimitedHistorySingleChangeUM(
                    changes,
                    ShapeChange::invert,
                    ShapeChange::apply
            );
        }
    
        public UndoManager<ShapeChange> getUndoManager() {
            return undoManager;
        }
    }
    
    class ShapeChange {
        private final EventSource<ShapeChange> changes;
        private final DrawingPane drawingPane;
        private final Shape shape;
        private final boolean adding;
    
        public ShapeChange(EventSource<ShapeChange> changes, DrawingPane drawingPane, Shape shape, boolean adding) {
            this.changes = changes;
            this.drawingPane = drawingPane;
            this.shape = shape;
            this.adding = adding;
        }
    
        public void apply() {
            if (adding) {
                drawingPane.getChildren().add(shape);
            } else {
                drawingPane.getChildren().remove(shape);
            }
    
            changes.push(this);
        }
    
        public ShapeChange invert() {
            return new ShapeChange(
                    this.changes,
                    this.drawingPane,
                    this.shape,
                    !adding
            );
        }
    
        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (o == null || getClass() != o.getClass()) return false;
            ShapeChange that = (ShapeChange) o;
            return adding == that.adding && Objects.equals(changes, that.changes) && Objects.equals(drawingPane, that.drawingPane) && Objects.equals(shape, that.shape);
        }
    
        @Override
        public int hashCode() {
            return Objects.hash(changes, drawingPane, shape, adding);
        }
    }
    
    enum DrawableShape {
        SQUARE, CIRCLE
    }
    
    class UndoControls {
        private final Button undo = new Button("<");
        private final Button redo = new Button(">");
    
        public UndoControls(UndoManager<ShapeChange> undoManager) {
            undo.disableProperty().bind(undoManager.undoAvailableProperty().map(x -> !x));
            redo.disableProperty().bind(undoManager.redoAvailableProperty().map(x -> !x));
    
            undo.setOnAction(evt -> undoManager.undo());
            redo.setOnAction(evt -> undoManager.redo());
        }
    
        public List<Node> getControls() {
            return List.of(undo, redo);
        }
    }
    
    class ShapeSelector {
        private final ObjectProperty<DrawableShape> selectedShape = new SimpleObjectProperty<>(DrawableShape.SQUARE);
        private final RadioButton squareSelector = looksLikeToggleButton(new RadioButton());
    
        private final RadioButton circleSelector = looksLikeToggleButton(new RadioButton());
    
        public ShapeSelector() {
            ShapeFactory shapeFactory = new ShapeFactory();
            Rectangle square = shapeFactory.createSquare(0, 0, Color.LIGHTGRAY);
            Circle circle = shapeFactory.createCircle(0, 0, Color.LIGHTGRAY);
    
            ToggleGroup toggleGroup = new ToggleGroup();
    
            squareSelector.setGraphic(square);
            squareSelector.setToggleGroup(toggleGroup);
            squareSelector.setSelected(true);
    
            circleSelector.setGraphic(circle);
            circleSelector.setToggleGroup(toggleGroup);
    
            Map<Toggle,DrawableShape> shapeMap = Map.of(
                    squareSelector, DrawableShape.SQUARE,
                    circleSelector, DrawableShape.CIRCLE
            );
    
            toggleGroup.selectedToggleProperty().addListener((observable, oldValue, newValue) ->
                    selectedShape.set(
                        shapeMap.get(newValue)
                    )
            );
        }
    
        public ObjectProperty<DrawableShape> selectedShapeProperty() {
            return selectedShape;
        }
    
        public List<Node> getControls() {
            return List.of(squareSelector, circleSelector);
        }
    
        private RadioButton looksLikeToggleButton(RadioButton radioButton) {
            radioButton.getStyleClass().remove("radio-button");
            radioButton.getStyleClass().add("toggle-button");
    
            return radioButton;
        }
    }
    
    class ShapeFactory {
        private static final double BASE_SIZE = 20;
        private static final double STROKE_WIDTH = 3;
    
        private static final List<Color> palette = List.of(
                Color.LIGHTBLUE,
                Color.LIGHTGREEN,
                Color.LIGHTCORAL,
                Color.LIGHTGOLDENRODYELLOW,
                Color.LIGHTPINK,
                Color.LIGHTSALMON,
                Color.LIGHTSEAGREEN,
                Color.LIGHTYELLOW
        );
    
        private int colorIdx = 0;
    
        public Rectangle createSquare(double x, double y, Color color) {
            Rectangle square = new Rectangle(BASE_SIZE, BASE_SIZE);
    
            square.setFill(color);
            square.setStroke(color.darker());
            square.setStrokeWidth(STROKE_WIDTH);
    
            square.relocate(x, y);
    
            return square;
        }
    
        public Circle createCircle(double x, double y, Color color) {
            Circle circle = new Circle(BASE_SIZE / 2);
    
            circle.setFill(color);
            circle.setStroke(color.darker());
            circle.setStrokeWidth(STROKE_WIDTH);
    
            circle.setCenterX(x);
            circle.setCenterY(y);
    
            return circle;
        }
    
        public Shape create(DrawableShape drawableShape, double x, double y) {
            return switch (drawableShape) {
                case SQUARE -> createSquare(x, y, nextColor());
                case CIRCLE -> createCircle(x, y, nextColor());
            };
        }
    
        private Color nextColor() {
            Color chosenColor = palette.get(colorIdx);
            colorIdx = (colorIdx   1) % palette.size();
    
            return chosenColor;
        }
    }
    

    module-info.java

    module com.example.shapechanger {
        requires javafx.controls;
        requires org.fxmisc.undo;
        requires reactfx;
    
        exports com.example.shapechanger;
    }
    

    pom.xml

    <?xml version="1.0" encoding="UTF-8"?>
    <project xmlns="http://maven.apache.org/POM/4.0.0"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
        <modelVersion>4.0.0</modelVersion>
    
        <groupId>com.example</groupId>
        <artifactId>ShapeChanger</artifactId>
        <version>1.0-SNAPSHOT</version>
        <name>ShapeChanger</name>
    
        <properties>
            <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        </properties>
    
        <dependencies>
            <dependency>
                <groupId>org.openjfx</groupId>
                <artifactId>javafx-controls</artifactId>
                <version>19</version>
            </dependency>
            <dependency>
                <groupId>org.fxmisc.undo</groupId>
                <artifactId>undofx</artifactId>
                <version>2.1.1</version>
            </dependency>
        </dependencies>
    
        <build>
            <plugins>
                <plugin>
                    <groupId>org.apache.maven.plugins</groupId>
                    <artifactId>maven-compiler-plugin</artifactId>
                    <version>3.10.1</version>
                    <configuration>
                        <source>19</source>
                        <target>19</target>
                        <compilerArgs>--enable-preview</compilerArgs>
                    </configuration>
                </plugin>
            </plugins>
        </build>
    </project>
    
  • Related