Search code examples
javafxlistenerjavafx-8scenegraph

How to listen to visible changes to the JavaFX SceneGraph for specific node


We created a small painting application in JavaFX. A new requirement arose, where we have to warn the user, that he made changes, which are not yet persisted and asking him, if the user might like to save first before closing.

Sample Snapshot:

Canvas Image

Unfortunately there are a lot of different Nodes, and Nodes can be changed in many ways, like for example a Polygon point can move. The Node itself can be dragged. They can be rotated and many more. So before firing a zillion events for every possible change of a Node object to the canvas I`d like to ask, if anyone might have an idea on how to simplify this approach. I am curious, if there are any listeners, that I can listen to any changes of the canvas object within the scene graph of JavaFX.

Especially since I just want to know if anything has changed and not really need to know the specific change.

Moreover, I also do not want to get every single event, like a simple select, which causes a border to be shown around the selected node (like shown on the image), which does not necessary mean, that the user has to save his application before leaving.

Anyone have an idea? Or do I really need to fire Events for every single change within a Node?


Solution

  • Benjamin's answer is probably the best one here: you should use an underlying model, and that model can easily check if relevant state has changed. At some point in the development of your application, you will come to the point where you realize this is the correct way to do things. It seems like you have reached that point.

    However, if you want to delay the inevitable redesign of your application a little further (and make it a bit more painful when you do get to that point ;) ), here's another approach you might consider.

    Obviously, you have some kind of Pane that is holding the objects that are being painted. The user must be creating those objects and you're adding them to the pane at some point. Just create a method that handles that addition, and registers an invalidation listener with the properties of interest when you do. The structure will look something like this:

    private final ReadOnlyBooleanWrapper unsavedChanges = 
        new ReadOnlyBooleanWrapper(this, "unsavedChanged", false);
    
    private final ChangeListener<Object> unsavedChangeListener = 
        (obs, oldValue, newValue) -> unsavedChanges.set(true);
    
    private Pane drawingPane ;
    
    // ...
    
    Button saveButton = new Button("Save");
    saveButton.disableProperty().bind(unsavedChanges.not());
    
    // ...
    @SafeVarArgs
    private final <T extends Node> void addNodeToDrawingPane(
            T node, Function<T, ObservableValue<?>>... properties) {
    
        Stream.of(properties).forEach(
            property -> property.apply(node).addListener(unsavedChangeListener));
        drawingPane.getChildren().add(node);
    }
    

    Now you can do things like

        Rectangle rect = new Rectangle();
    
        addNodeToDrawingPane(rect, 
                Rectangle::xProperty, Rectangle::yProperty, 
                Rectangle::widthProperty, Rectangle::heightProperty);
    

    and

        Text text = new Text();
        addNodeToDrawingPane(text, 
                Text::xProperty, Text::yProperty, Text::textProperty);
    

    I.e. you just specify the properties to observe when you add the new node. You can create a remove method which removes the listener too. The amount of extra code on top of what you already have is pretty minimal, as (probably, I haven't seen your code) is the refactoring.

    Again, you should really have a separate view model, etc. I wanted to post this to show that @kleopatra's first comment on the question ("Listen for invalidation of relevant state") doesn't necessarily involve a lot of work if you approach it in the right way. At first, I thought this approach was incompatible with @Tomas Mikula's mention of undo/redo functionality, but you may even be able to use this approach as a basis for that too.