2

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?

Oliver Jan Krylow
  • 1,758
  • 15
  • 22
crusam
  • 6,140
  • 6
  • 40
  • 68
  • "I really need to fire Events" it's not _you_ (nor your code) that fires an event :-) Listen for invalidation of relevant state – kleopatra Nov 07 '14 at 10:22
  • That´s indeed another possible way to implement it, without creating a specific view model, but still it seems so much effort for a little feature like this. – crusam Nov 07 '14 at 10:40
  • 2
    If you are ever going to support Undo/Redo, you will need to track the exact changes anyway. – Tomas Mikula Nov 07 '14 at 13:28

2 Answers2

6

I think you are approaching this problem in the wrong way. The nodes displayed on screen should just be a visual representation of an underlying model. All you really need to know is that the underlying model has changed.

If, for example, you were writing a text editor, the text displayed on the screen would be backed by some sort of model. Let's assume the model is a String. You wouldn't need to check if any of the text nodes displayed on screen had changed you would just need to compare the original string data with the current string data to determine if you need to prompt the user to save.

Benjamin Gale
  • 12,977
  • 6
  • 62
  • 100
  • That´s something we considered as well, but currently we just serialize the canvas in xml with some optimized serializers, so an underlying model does not exist, since it was never really necessary yet. It might be possible, that its needed one day, but for a state like "Don`t you wonna save your map?" it seems so much overhead. – crusam Nov 07 '14 at 10:36
  • 1
    @ymene Can you use the xml as your underlying model? Cache the original loaded xml file and when the user tries to close the window, serialize the current canvas and compare them to determine if you should notify the user. – Benjamin Gale Nov 07 '14 at 10:48
  • That´s an interesting idea. I am gonna give it a try. We considered something simular, but since we first wanted to display the "dirty state" in form of disabling the save button, we searched for different strategies. But without displaying the "dirty state", this is a really simple solution, which might be satisfying for the moment. Thank you a lot! – crusam Nov 07 '14 at 11:01
  • @ymene hmm .. even with a listener to some unspecific "dirty" state that button would be in enabled nearly always, wouldn't it? Without some kind of controller of what _is_ a relevant change (like changing the position of a shape. f.i.) and what _is not_ (like f.i. selecting an item) dirty is always true. With such a controller you are nearing a view model :-) – kleopatra Nov 07 '14 at 11:59
  • @kleopatra you are absolutely right! This solution is indeed a compromise without showing the user that the canvas is currently in a dirty state, but at least I can make sure, that the user won´t loose data, when closing the application, because of the check Benjamin suggested. To get fast results I`ll go that way. The dirty state might be an enhancement for the future, which doesnt really matter for the moment. I just thought it would be something, I might would gain for free without much effort. Unfortunately time currently matters, so the Button always stays enabled for the moment. – crusam Nov 07 '14 at 12:22
  • @ymene You could use [Parent.needsLayoutProperty()](http://docs.oracle.com/javase/8/javafx/api/javafx/scene/Parent.html#needsLayoutProperty--) to track the dirty state (set "dirty" every time `needsLayout` changed to `true`, set "clean" on save), but as @kleopatra noted, it would be dirty almost all the time. – Tomas Mikula Nov 07 '14 at 13:41
  • Interesting property, did not know about it yet, thanks for the hint, but yeah, indeed, I understand where this is going. Since James is closest to answering the original question with example code, I will mark his implementation suggestion as answer. But still the way described here is the way I implement it for the moment, since time mattered, but I keep your concerns in mind, and hope there will be time to change it back again in the future. – crusam Nov 07 '14 at 14:36
2

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.

James_D
  • 201,275
  • 16
  • 291
  • 322
  • First of all, I agree in every point you are mentioning here. Thanks for sharing your ideas on how you would implement it. I really see where this is going and I like what I am seeing here. As soon as there will be enough time, I will use your ideas to include the funktionality. Really appreciate your effort, thanks! – crusam Nov 07 '14 at 14:30