Ultimate Zero Ultimate Zero - 2 months ago 20
Java Question

ReactFX - "lazy" real-time search text area

New to Reactive Programming here.

I'm trying to implement a "lazy" real-time search text area in JavaFX with ReactFX. By lazy here I mean it performs the search once the user stops typing for one second. The code for that was pretty simple:

EventStream<Change<String>> textEvents = EventStreams.changesOf(textArea.textProperty())
.successionEnds(Duration.ofSeconds(1));


Then subscribe to that event stream and voilĂ .

But I also want it to perform the search instantly if the user presses Enter. I'm not sure how to do that in a "reactive" way. Simply performing the search on Enter key events causes the search to fire twice (one for key event and one for text change), so this is my current solution:

BooleanProperty hasSearched = new SimpleBooleanProperty(false);
EventStream<KeyEvent> enterKeyPressedEvents = EventStreams.eventsOf(textArea, KeyEvent.KEY_PRESSED)
.filter(k -> k.getCode() == KeyCode.ENTER);
AwaitingEventStream<Change<String>> textEvents = EventStreams.changesOf(textArea.textProperty())
.successionEnds(Duration.ofSeconds(1));

subs = Subscription.multi(
//Text changed
textEvents.subscribe(e -> {
if (hasSearched.get()) {
hasSearched.set(false);
System.out.println("ignored text event");
} else {
performSearch(textArea.getText());
}
}),

//Enter key pressed
enterKeyPressedEvents.subscribe(e -> {
e.consume();
if (e.isShiftDown()) {
textArea.insertText(textArea.getCaretPosition(), "\n");
} else {
hasSearched.set(true);
System.out.println("enter pressed");
performSearch(textArea.getText());
if (!textEvents.isPending()) {
hasSearched.set(false);
}
}
})
);


I've tried using
SuspendableEventStream.suspend()
thinking it would "drop" all pending events, but it didn't work as expected, the pending event is still emitted:

EventStream<KeyEvent> enterKeyPressedEvents = EventStreams.eventsOf(textArea, KeyEvent.KEY_PRESSED)
.filter(k -> k.getCode() == KeyCode.ENTER);
SuspendableEventStream<Change<String>> textEvents = EventStreams.changesOf(textArea.textProperty())
.successionEnds(Duration.ofSeconds(1)).suppressible();

subs = Subscription.multi(
//Text changed
textEvents.subscribe(e -> {
performSearch(textArea.getText());
}),

//Enter key pressed
enterKeyPressedEvents.subscribe(e -> {
e.consume();
if (e.isShiftDown()) {
textArea.insertText(textArea.getCaretPosition(), "\n");
} else {
Guard guard = textEvents.suspend();
System.out.println("enter pressed");
performSearch(textArea.getText());
guard.close();
}
})
);


How can I think of a better (more reactive?) solution?

Answer

Here is a solution. The key part in this solution is observing text changes inside flatMap, which has the effect of "resetting" the stream of text changes.

import java.time.Duration;

import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.TextArea;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import javafx.stage.Stage;

import org.reactfx.EventStream;
import org.reactfx.EventStreams;

public class AutoSearch extends Application {

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

    @Override
    public void start(Stage stage) throws Exception {
        TextArea area = new TextArea();

        EventStream<KeyEvent> enterPresses = EventStreams.eventsOf(area, KeyEvent.KEY_PRESSED)
                .filter(k -> k.getCode() == KeyCode.ENTER)
                .map(k -> { k.consume(); return k; });

        EventStream<?> searchImpulse = enterPresses.withDefaultEvent(null) // emit an event even before Enter is pressed
                .flatMap(x -> EventStreams.changesOf(area.textProperty())
                                          .successionEnds(Duration.ofSeconds(1))
                                          .withDefaultEvent(null));

        searchImpulse.subscribe(x -> System.out.println("Search now!"));

        stage.setScene(new Scene(area));
        stage.show();
    }

}