Make multiple change firings lead to far fewer actions

好久不见. 提交于 2020-05-17 05:47:07

问题


I have a TextArea in which the user of my app can write things. A ChangeListener is also listening to the StringProperty "text" of this TextArea. Whenever the text content changes, ChangeListener.changed(), among other things, sets a "dirty" BooleanProperty to true on a central app object. Where "dirty" has the sense of "document needs saving".

But I've just implemented a thing in my app whereby any time that the "dirty" Property gets set to true triggers a save-file-to-disk action, automatically, so the user doesn't have to worry about manually saving things. NB the act of saving also sets dirty back to false of course.

One problem with this is, though, that it slows typing in the TextArea (particulary as this saving takes place on the FX thread). Each new character added or deleted therefore fires a save action.

I want to find a solution where every single change-of-text action is always followed by a save at most within, say, 1 second, but where saving never happens more than once per this 1 second ... obviously in a non-FX thread.

This isn't the first time I've encountered this situation and in the past I've tinkered with various queues of timers, and so on. But it's difficult to find a solution where both criteria are met and I'm just wondering if there's a well-known technique to deal with this... even maybe something from some library?

Here's an MRE/MCVE:

import javafx.application.Application;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.control.TextArea;
import javafx.stage.Stage;

public class Main extends Application {
    public static void main(String[] args) {
        launch(args);
    }

    BooleanProperty dirty;
    void setDirty(Boolean value) {
        dirtyProperty().set(value);
        if( value ){
            System.out.println( "serialise to file again...");
            // successful saving also means we become clean again:
            dirtyProperty().set( false );
        }
    }
    Boolean getDirty() { return dirtyProperty().get(); }
    BooleanProperty dirtyProperty() {
        if ( dirty == null) dirty = new SimpleBooleanProperty(this, "dirty");
        return dirty;
    }

    @Override
    public void start(Stage stage) throws Exception {
        Scene scene = new Scene(new Group());
        Group sceneRoot = (Group)scene.getRoot();
        TextArea textArea = new TextArea();

        textArea.textProperty().addListener(new ChangeListener<String>() {
            @Override
            public void changed(ObservableValue<? extends String> observableValue, String s, String t1) {
                setDirty( true );
            }
        });

        sceneRoot.getChildren().add( textArea );
        stage.setMinWidth( 600 );
        stage.setMinHeight( 400 );
        stage.setScene(scene);
        stage.show();
    }
}

Each keystroke causes a save...


回答1:


Instead of using background threads, use a pause transition:

PauseTransition pause = new PauseTransition(Duration.seconds(1));
pause.setOnFinished(e -> {
    // save action here
});

dirtyProperty().addListener((obs, wasDirty, isNowDirty) -> {
    if (isNowDirty && pause.getStatus() != Animation.Status.RUNNING) {
        pause.playFromStart();
    }
});

This will start the one-second pause when the dirtyProperty() changes to true, and at the end of the pause, will save the data. However, by checking the status of the pause, it will not schedule more than one save per second.

If you do want to use background threads, you can do something along the following lines:

BlockingQueue<String> textToSave = new ArrayBlockingQueue<>(1);
ScheduledExecutorService exec = Executors.newScheduledThreadPool(1);
exec.scheduleWithFixedDelay(() -> {
    try {
        String text = textToSave.take();
        // save text
    } catch (InterruptedException interrupt) {
        Thread.currentThread().interrupt();
    }
}, 0, 1, TimeUnit.SECONDS);

and then

dirtyProperty().addListener((obs, wasDirty, isNowDirty) -> {
    textToSave.clear();
    textToSave.offer(myTextArea.getText());
});

This assumes no threads other than the FX Application Thread is pushing data to the textToSave "queue" (not sure if something of size <= 1 is properly called a "queue"), which seems easy enough to ensure.

The advantage of this approach is that the IO happens on a background thread, which means there's no opportunity whatsoever to block the FX Application Thread by writing the file.



来源:https://stackoverflow.com/questions/61561250/make-multiple-change-firings-lead-to-far-fewer-actions

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!